feat(agent): scrape linked pages to ground answers in real content
Adds `_fetch_page_content()`, which downloads a URL with a browser-like User-Agent and uses BeautifulSoup with the lxml parser to strip scripts, navigation, and other non-content markup, returning up to 3000 characters of readable text. The response validator now also compares generated replies against this scraped text, flagging named entities and claims that do not appear in the retrieved page content. Dependencies: adds beautifulsoup4==4.12.3 and pins lxml==5.3.0 for parser compatibility.
This commit is contained in:
@@ -233,20 +233,6 @@ cdef int _setNodeNamespaces(xmlNode* c_node, _Document doc,
|
||||
c_ns.href is NULL or \
|
||||
tree.xmlStrcmp(c_ns.href, c_href) != 0:
|
||||
c_ns = tree.xmlNewNs(c_node, c_href, c_prefix)
|
||||
if c_ns is NULL:
|
||||
# libxml2 has two error conditions: "out of memory" and "prefix exists already".
|
||||
# We ignore the latter for compatibility reasons. It currently only appears
|
||||
# during namespace cleanup.
|
||||
c_ns = c_node.nsDef
|
||||
while c_ns is not NULL:
|
||||
if c_prefix is NULL:
|
||||
if c_ns.prefix is NULL:
|
||||
break
|
||||
elif tree.xmlStrcmp(c_ns.prefix, c_prefix) == 0:
|
||||
break
|
||||
c_ns = c_ns.next
|
||||
else:
|
||||
raise MemoryError()
|
||||
if href_utf == node_ns_utf:
|
||||
tree.xmlSetNs(c_node, c_ns)
|
||||
node_ns_utf = None
|
||||
@@ -289,7 +275,8 @@ cdef _iter_nsmap(nsmap):
|
||||
return nsmap.items()
|
||||
if len(nsmap) <= 1:
|
||||
return nsmap.items()
|
||||
if isinstance(nsmap, OrderedDict):
|
||||
# nsmap will usually be a plain unordered dict => avoid type checking overhead
|
||||
if type(nsmap) is not dict and isinstance(nsmap, OrderedDict):
|
||||
return nsmap.items() # keep existing order
|
||||
if None not in nsmap:
|
||||
return sorted(nsmap.items())
|
||||
@@ -348,13 +335,12 @@ cdef int _addAttributeToNode(xmlNode* c_node, _Document doc, bint is_html,
|
||||
_attributeValidOrRaise(name_utf)
|
||||
value_utf = _utf8(value)
|
||||
if ns_utf is None:
|
||||
new_attr = tree.xmlNewProp(c_node, _xcstr(name_utf), _xcstr(value_utf))
|
||||
tree.xmlNewProp(c_node, _xcstr(name_utf), _xcstr(value_utf))
|
||||
else:
|
||||
_uriValidOrRaise(ns_utf)
|
||||
c_ns = doc._findOrBuildNodeNs(c_node, _xcstr(ns_utf), NULL, 1)
|
||||
new_attr = tree.xmlNewNsProp(c_node, c_ns, _xcstr(name_utf), _xcstr(value_utf))
|
||||
if new_attr is NULL:
|
||||
raise MemoryError()
|
||||
tree.xmlNewNsProp(c_node, c_ns,
|
||||
_xcstr(name_utf), _xcstr(value_utf))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -453,7 +439,7 @@ cdef int _removeUnusedNamespaceDeclarations(xmlNode* c_element, set prefixes_to_
|
||||
c_nsdef = c_nsdef.next
|
||||
c_nsdef.next = c_nsdef.next.next
|
||||
tree.xmlFreeNs(c_ns_list[i].ns)
|
||||
|
||||
|
||||
if c_ns_list is not NULL:
|
||||
python.lxml_free(c_ns_list)
|
||||
return 0
|
||||
@@ -699,7 +685,7 @@ cdef unicode _collectText(xmlNode* c_node):
|
||||
"""Collect all text nodes and return them as a unicode string.
|
||||
|
||||
Start collecting at c_node.
|
||||
|
||||
|
||||
If there was no text to collect, return None
|
||||
"""
|
||||
cdef Py_ssize_t scount
|
||||
@@ -800,7 +786,6 @@ cdef inline Py_ssize_t _countElements(xmlNode* c_node) noexcept:
|
||||
c_node = c_node.next
|
||||
return count
|
||||
|
||||
|
||||
cdef int _findChildSlice(
|
||||
slice sliceobject, xmlNode* c_parent,
|
||||
xmlNode** c_start_node, Py_ssize_t* c_step, Py_ssize_t* c_length) except -1:
|
||||
@@ -819,17 +804,14 @@ cdef int _findChildSlice(
|
||||
else:
|
||||
python._PyEval_SliceIndex(sliceobject.step, c_step)
|
||||
return 0
|
||||
|
||||
python.PySlice_GetIndicesEx(
|
||||
sliceobject, childcount, &start, &stop, c_step, c_length)
|
||||
|
||||
if start > childcount // 2:
|
||||
c_start_node[0] = _findChildBackwards(c_parent, childcount - start - 1)
|
||||
else:
|
||||
c_start_node[0] = _findChild(c_parent, start)
|
||||
return 0
|
||||
|
||||
|
||||
cdef bint _isFullSlice(slice sliceobject) except -1:
|
||||
"""Conservative guess if this slice is a full slice as in ``s[:]``.
|
||||
"""
|
||||
@@ -863,7 +845,7 @@ cdef inline xmlNode* _findChild(xmlNode* c_node, Py_ssize_t index) noexcept:
|
||||
return _findChildBackwards(c_node, -index - 1)
|
||||
else:
|
||||
return _findChildForwards(c_node, index)
|
||||
|
||||
|
||||
cdef inline xmlNode* _findChildForwards(xmlNode* c_node, Py_ssize_t index) noexcept:
|
||||
"""Return child element of c_node with index, or return NULL if not found.
|
||||
"""
|
||||
@@ -894,7 +876,7 @@ cdef inline xmlNode* _findChildBackwards(xmlNode* c_node, Py_ssize_t index) noex
|
||||
c += 1
|
||||
c_child = c_child.prev
|
||||
return NULL
|
||||
|
||||
|
||||
cdef inline xmlNode* _textNodeOrSkip(xmlNode* c_node) noexcept nogil:
|
||||
"""Return the node if it's a text node. Skip over ignorable nodes in a
|
||||
series of text nodes. Return NULL if a non-ignorable node is found.
|
||||
@@ -1049,31 +1031,23 @@ cdef Py_ssize_t _mapTagsToQnameMatchArray(xmlDoc* c_doc, list ns_tags,
|
||||
Note that each qname struct in the array owns its href byte string object
|
||||
if it is not NULL.
|
||||
"""
|
||||
cdef Py_ssize_t count = 0, i, c_tag_len
|
||||
cdef Py_ssize_t count = 0, i
|
||||
cdef bytes ns, tag
|
||||
cdef const_xmlChar* c_tag
|
||||
|
||||
for ns, tag in ns_tags:
|
||||
if tag is None:
|
||||
c_tag = <const_xmlChar*> NULL
|
||||
c_tag = <const_xmlChar*>NULL
|
||||
elif force_into_dict:
|
||||
c_tag = tree.xmlDictLookup(c_doc.dict, _xcstr(tag), len(tag))
|
||||
if c_tag is NULL:
|
||||
# clean up before raising the error
|
||||
for i in xrange(count):
|
||||
cpython.ref.Py_XDECREF(c_ns_tags[i].href)
|
||||
raise MemoryError()
|
||||
else:
|
||||
c_tag_len = len(tag)
|
||||
if c_tag_len > limits.INT_MAX:
|
||||
# too long, not in the dict => not in the document
|
||||
c_tag = tree.xmlDictExists(c_doc.dict, _xcstr(tag), len(tag))
|
||||
if c_tag is NULL:
|
||||
# not in the dict => not in the document
|
||||
continue
|
||||
elif force_into_dict:
|
||||
c_tag = tree.xmlDictLookup(c_doc.dict, _xcstr(tag), <int> c_tag_len)
|
||||
if c_tag is NULL:
|
||||
# clean up before raising the error
|
||||
for i in xrange(count):
|
||||
cpython.ref.Py_XDECREF(c_ns_tags[i].href)
|
||||
raise MemoryError()
|
||||
else:
|
||||
c_tag = tree.xmlDictExists(c_doc.dict, _xcstr(tag), <int> c_tag_len)
|
||||
if c_tag is NULL:
|
||||
# not in the dict => not in the document
|
||||
continue
|
||||
|
||||
c_ns_tags[count].c_name = c_tag
|
||||
if ns is None:
|
||||
c_ns_tags[count].href = NULL
|
||||
@@ -1121,7 +1095,7 @@ cdef int _removeSiblings(xmlNode* c_element, tree.xmlElementType node_type, bint
|
||||
|
||||
cdef void _moveTail(xmlNode* c_tail, xmlNode* c_target) noexcept:
|
||||
cdef xmlNode* c_next
|
||||
# tail support: look for any text nodes trailing this node and
|
||||
# tail support: look for any text nodes trailing this node and
|
||||
# move them too
|
||||
c_tail = _textNodeOrSkip(c_tail)
|
||||
while c_tail is not NULL:
|
||||
@@ -1188,7 +1162,7 @@ cdef int _deleteSlice(_Document doc, xmlNode* c_node,
|
||||
if step > 0:
|
||||
next_element = _nextElement
|
||||
else:
|
||||
step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX
|
||||
step = -step
|
||||
next_element = _previousElement
|
||||
# now start deleting nodes
|
||||
c = 0
|
||||
@@ -1218,7 +1192,7 @@ cdef int _replaceSlice(_Element parent, xmlNode* c_node,
|
||||
cdef _Element element
|
||||
cdef Py_ssize_t seqlength, i, c
|
||||
cdef _node_to_node_function next_element
|
||||
assert step > 0, step
|
||||
assert step > 0
|
||||
if left_to_right:
|
||||
next_element = _nextElement
|
||||
else:
|
||||
@@ -1442,6 +1416,15 @@ cdef int _addSibling(_Element element, _Element sibling, bint as_next) except -1
|
||||
moveNodeToDocument(element._doc, c_source_doc, c_node)
|
||||
return 0
|
||||
|
||||
cdef inline bint isutf8(const_xmlChar* s) noexcept:
|
||||
cdef xmlChar c = s[0]
|
||||
while c != c'\0':
|
||||
if c & 0x80:
|
||||
return True
|
||||
s += 1
|
||||
c = s[0]
|
||||
return False
|
||||
|
||||
cdef bint isutf8l(const_xmlChar* s, size_t length) noexcept:
|
||||
"""
|
||||
Search for non-ASCII characters in the string, knowing its length in advance.
|
||||
|
||||
Reference in New Issue
Block a user