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:
@@ -19,7 +19,6 @@ __all__ = [
|
||||
'FallbackElementClassLookup', 'FunctionNamespace', 'HTML', 'HTMLParser',
|
||||
'ICONV_COMPILED_VERSION',
|
||||
'LIBXML_COMPILED_VERSION', 'LIBXML_VERSION',
|
||||
'LIBXML_FEATURES',
|
||||
'LIBXSLT_COMPILED_VERSION', 'LIBXSLT_VERSION',
|
||||
'LXML_VERSION',
|
||||
'LxmlError', 'LxmlRegistryError', 'LxmlSyntaxError',
|
||||
@@ -300,101 +299,6 @@ cdef extern from *:
|
||||
ICONV_COMPILED_VERSION = __unpackIntVersion(LIBICONV_HEX_VERSION, base=0x100)[:2]
|
||||
|
||||
|
||||
cdef extern from "libxml/xmlversion.h":
|
||||
"""
|
||||
static const char* const _lxml_lib_features[] = {
|
||||
#ifdef LIBXML_HTML_ENABLED
|
||||
"html",
|
||||
#endif
|
||||
#ifdef LIBXML_FTP_ENABLED
|
||||
"ftp",
|
||||
#endif
|
||||
#ifdef LIBXML_HTTP_ENABLED
|
||||
"http",
|
||||
#endif
|
||||
#ifdef LIBXML_CATALOG_ENABLED
|
||||
"catalog",
|
||||
#endif
|
||||
#ifdef LIBXML_XPATH_ENABLED
|
||||
"xpath",
|
||||
#endif
|
||||
#ifdef LIBXML_ICONV_ENABLED
|
||||
"iconv",
|
||||
#endif
|
||||
#ifdef LIBXML_ICU_ENABLED
|
||||
"icu",
|
||||
#endif
|
||||
#ifdef LIBXML_REGEXP_ENABLED
|
||||
"regexp",
|
||||
#endif
|
||||
#ifdef LIBXML_SCHEMAS_ENABLED
|
||||
"xmlschema",
|
||||
#endif
|
||||
#ifdef LIBXML_SCHEMATRON_ENABLED
|
||||
"schematron",
|
||||
#endif
|
||||
#ifdef LIBXML_ZLIB_ENABLED
|
||||
"zlib",
|
||||
#endif
|
||||
#ifdef LIBXML_LZMA_ENABLED
|
||||
"lzma",
|
||||
#endif
|
||||
0
|
||||
};
|
||||
"""
|
||||
const char* const* _LXML_LIB_FEATURES "_lxml_lib_features"
|
||||
|
||||
|
||||
cdef set _copy_lib_features():
|
||||
features = set()
|
||||
feature = _LXML_LIB_FEATURES
|
||||
while feature[0]:
|
||||
features.add(feature[0].decode('ASCII'))
|
||||
feature += 1
|
||||
return features
|
||||
|
||||
LIBXML_COMPILED_FEATURES = _copy_lib_features()
|
||||
LIBXML_FEATURES = {
|
||||
feature_name for feature_id, feature_name in [
|
||||
#XML_WITH_THREAD = 1
|
||||
#XML_WITH_TREE = 2
|
||||
#XML_WITH_OUTPUT = 3
|
||||
#XML_WITH_PUSH = 4
|
||||
#XML_WITH_READER = 5
|
||||
#XML_WITH_PATTERN = 6
|
||||
#XML_WITH_WRITER = 7
|
||||
#XML_WITH_SAX1 = 8
|
||||
(xmlparser.XML_WITH_FTP, "ftp"), # XML_WITH_FTP = 9
|
||||
(xmlparser.XML_WITH_HTTP, "http"), # XML_WITH_HTTP = 10
|
||||
#XML_WITH_VALID = 11
|
||||
(xmlparser.XML_WITH_HTML, "html"), # XML_WITH_HTML = 12
|
||||
#XML_WITH_LEGACY = 13
|
||||
#XML_WITH_C14N = 14
|
||||
(xmlparser.XML_WITH_CATALOG, "catalog"), # XML_WITH_CATALOG = 15
|
||||
(xmlparser.XML_WITH_XPATH, "xpath"), # XML_WITH_XPATH = 16
|
||||
#XML_WITH_XPTR = 17
|
||||
#XML_WITH_XINCLUDE = 18
|
||||
(xmlparser.XML_WITH_ICONV, "iconv"), # XML_WITH_ICONV = 19
|
||||
#XML_WITH_ISO8859X = 20
|
||||
#XML_WITH_UNICODE = 21
|
||||
(xmlparser.XML_WITH_REGEXP, "regexp"), # XML_WITH_REGEXP = 22
|
||||
#XML_WITH_AUTOMATA = 23
|
||||
#XML_WITH_EXPR = 24
|
||||
(xmlparser.XML_WITH_SCHEMAS, "xmlschema"), # XML_WITH_SCHEMAS = 25
|
||||
(xmlparser.XML_WITH_SCHEMATRON, "schematron"), # XML_WITH_SCHEMATRON = 26
|
||||
#XML_WITH_MODULES = 27
|
||||
#XML_WITH_DEBUG = 28
|
||||
#XML_WITH_DEBUG_MEM = 29
|
||||
#XML_WITH_DEBUG_RUN = 30 # unused
|
||||
(xmlparser.XML_WITH_ZLIB, "zlib"), # XML_WITH_ZLIB = 31
|
||||
(xmlparser.XML_WITH_ICU, "icu"), # XML_WITH_ICU = 32
|
||||
(xmlparser.XML_WITH_LZMA, "lzma"), # XML_WITH_LZMA = 33
|
||||
] if xmlparser.xmlHasFeature(feature_id)
|
||||
}
|
||||
|
||||
cdef bint HAS_ZLIB_COMPRESSION = xmlparser.xmlHasFeature(xmlparser.XML_WITH_ZLIB)
|
||||
|
||||
|
||||
# class for temporary storage of Python references,
|
||||
# used e.g. for XPath results
|
||||
@cython.final
|
||||
@@ -417,12 +321,10 @@ cdef class _TempStore:
|
||||
@cython.internal
|
||||
cdef class _ExceptionContext:
|
||||
cdef object _exc_info
|
||||
|
||||
cdef int clear(self) except -1:
|
||||
self._exc_info = None
|
||||
return 0
|
||||
|
||||
@cython.final
|
||||
cdef void _store_raised(self) noexcept:
|
||||
try:
|
||||
self._exc_info = sys.exc_info()
|
||||
@@ -431,16 +333,13 @@ cdef class _ExceptionContext:
|
||||
finally:
|
||||
return # and swallow any further exceptions
|
||||
|
||||
@cython.final
|
||||
cdef int _store_exception(self, exception) except -1:
|
||||
self._exc_info = (exception, None, None)
|
||||
return 0
|
||||
|
||||
@cython.final
|
||||
cdef bint _has_raised(self) except -1:
|
||||
return self._exc_info is not None
|
||||
|
||||
@cython.final
|
||||
cdef int _raise_if_stored(self) except -1:
|
||||
if self._exc_info is None:
|
||||
return 0
|
||||
@@ -478,7 +377,6 @@ cdef public class _Document [ type LxmlDocumentType, object LxmlDocument ]:
|
||||
cdef int _ns_counter
|
||||
cdef bytes _prefix_tail
|
||||
cdef xmlDoc* _c_doc
|
||||
# -- End of public part, '_c_doc' is the only publicly exposed struct field.
|
||||
cdef _BaseParser _parser
|
||||
|
||||
def __dealloc__(self):
|
||||
@@ -621,15 +519,13 @@ cdef public class _Document [ type LxmlDocumentType, object LxmlDocument ]:
|
||||
c_ns = self._findOrBuildNodeNs(c_node, c_href, NULL, 0)
|
||||
tree.xmlSetNs(c_node, c_ns)
|
||||
|
||||
|
||||
cdef tuple __initPrefixCache():
|
||||
cdef int i
|
||||
return tuple([ python.PyBytes_FromFormat("ns%d", i)
|
||||
for i in range(26) ])
|
||||
for i in range(30) ])
|
||||
|
||||
cdef tuple _PREFIX_CACHE = __initPrefixCache()
|
||||
|
||||
|
||||
cdef _Document _documentFactory(xmlDoc* c_doc, _BaseParser parser):
|
||||
cdef _Document result
|
||||
result = _Document.__new__(_Document)
|
||||
@@ -663,7 +559,7 @@ cdef class DocInfo:
|
||||
return root_name
|
||||
|
||||
@cython.final
|
||||
cdef tree.xmlDtd* _get_c_dtd(self) noexcept:
|
||||
cdef tree.xmlDtd* _get_c_dtd(self):
|
||||
""""Return the DTD. Create it if it does not yet exist."""
|
||||
cdef xmlDoc* c_doc = self._doc._c_doc
|
||||
cdef xmlNode* c_root_node
|
||||
@@ -872,7 +768,7 @@ cdef public class _Element [ type LxmlElementType, object LxmlElement ]:
|
||||
left_to_right = 1
|
||||
else:
|
||||
left_to_right = 0
|
||||
step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX
|
||||
step = -step
|
||||
_replaceSlice(self, c_node, slicelength, step, left_to_right, value)
|
||||
return
|
||||
else:
|
||||
@@ -1287,7 +1183,7 @@ cdef public class _Element [ type LxmlElementType, object LxmlElement ]:
|
||||
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
|
||||
result = []
|
||||
c = 0
|
||||
@@ -1318,8 +1214,7 @@ cdef public class _Element [ type LxmlElementType, object LxmlElement ]:
|
||||
"""__bool__(self)"""
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"Truth-testing of elements was a source of confusion and will always "
|
||||
"return True in future versions. "
|
||||
"The behavior of this method will change in future versions. "
|
||||
"Use specific 'len(elem)' or 'elem is not None' test instead.",
|
||||
FutureWarning
|
||||
)
|
||||
@@ -1741,6 +1636,11 @@ cdef public class _Element [ type LxmlElementType, object LxmlElement ]:
|
||||
return CSSSelector(expr, translator=translator)(self)
|
||||
|
||||
|
||||
cdef extern from "includes/etree_defs.h":
|
||||
# macro call to 't->tp_new()' for fast instantiation
|
||||
cdef object NEW_ELEMENT "PY_NEW" (object t)
|
||||
|
||||
|
||||
@cython.linetrace(False)
|
||||
cdef _Element _elementFactory(_Document doc, xmlNode* c_node):
|
||||
cdef _Element result
|
||||
@@ -1750,15 +1650,12 @@ cdef _Element _elementFactory(_Document doc, xmlNode* c_node):
|
||||
if c_node is NULL:
|
||||
return None
|
||||
|
||||
element_class = <type> LOOKUP_ELEMENT_CLASS(
|
||||
element_class = LOOKUP_ELEMENT_CLASS(
|
||||
ELEMENT_CLASS_LOOKUP_STATE, doc, c_node)
|
||||
if type(element_class) is not type:
|
||||
if not isinstance(element_class, type):
|
||||
raise TypeError(f"Element class is not a type, got {type(element_class)}")
|
||||
if hasProxy(c_node):
|
||||
# prevent re-entry race condition - we just called into Python
|
||||
return getProxy(c_node)
|
||||
result = element_class.__new__(element_class)
|
||||
result = NEW_ELEMENT(element_class)
|
||||
if hasProxy(c_node):
|
||||
# prevent re-entry race condition - we just called into Python
|
||||
result._c_node = NULL
|
||||
@@ -3162,7 +3059,7 @@ cdef class ElementTextIterator:
|
||||
return result
|
||||
|
||||
|
||||
cdef xmlNode* _createElement(xmlDoc* c_doc, object name_utf) noexcept:
|
||||
cdef xmlNode* _createElement(xmlDoc* c_doc, object name_utf) except NULL:
|
||||
cdef xmlNode* c_node
|
||||
c_node = tree.xmlNewDocNode(c_doc, NULL, _xcstr(name_utf), NULL)
|
||||
return c_node
|
||||
@@ -3184,34 +3081,18 @@ cdef xmlNode* _createEntity(xmlDoc* c_doc, const_xmlChar* name) noexcept:
|
||||
|
||||
# module-level API for ElementTree
|
||||
|
||||
from abc import ABC
|
||||
|
||||
class Element(ABC):
|
||||
def Element(_tag, attrib=None, nsmap=None, **_extra):
|
||||
"""Element(_tag, attrib=None, nsmap=None, **_extra)
|
||||
|
||||
Element factory, as a class.
|
||||
|
||||
An instance of this class is an object implementing the
|
||||
Element factory. This function returns an object implementing the
|
||||
Element interface.
|
||||
|
||||
>>> element = Element("test")
|
||||
>>> type(element)
|
||||
<class 'lxml.etree._Element'>
|
||||
>>> isinstance(element, Element)
|
||||
True
|
||||
>>> issubclass(_Element, Element)
|
||||
True
|
||||
|
||||
Also look at the `_Element.makeelement()` and
|
||||
`_BaseParser.makeelement()` methods, which provide a faster way to
|
||||
create an Element within a specific document or parser context.
|
||||
"""
|
||||
def __new__(cls, _tag, attrib=None, nsmap=None, **_extra):
|
||||
return _makeElement(_tag, NULL, None, None, None, None,
|
||||
attrib, nsmap, _extra)
|
||||
|
||||
# Register _Element as a virtual subclass of Element
|
||||
Element.register(_Element)
|
||||
return _makeElement(_tag, NULL, None, None, None, None,
|
||||
attrib, nsmap, _extra)
|
||||
|
||||
|
||||
def Comment(text=None):
|
||||
@@ -3323,41 +3204,32 @@ def SubElement(_Element _parent not None, _tag,
|
||||
"""
|
||||
return _makeSubElement(_parent, _tag, None, None, attrib, nsmap, _extra)
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
def ElementTree(_Element element=None, *, file=None, _BaseParser parser=None):
|
||||
"""ElementTree(element=None, file=None, parser=None)
|
||||
|
||||
class ElementTree(ABC, Generic[T]):
|
||||
def __new__(cls, _Element element=None, *, file=None, _BaseParser parser=None):
|
||||
"""ElementTree(element=None, file=None, parser=None)
|
||||
ElementTree wrapper class.
|
||||
"""
|
||||
cdef xmlNode* c_next
|
||||
cdef xmlNode* c_node
|
||||
cdef xmlNode* c_node_copy
|
||||
cdef xmlDoc* c_doc
|
||||
cdef _ElementTree etree
|
||||
cdef _Document doc
|
||||
|
||||
ElementTree wrapper class.
|
||||
"""
|
||||
cdef xmlNode* c_next
|
||||
cdef xmlNode* c_node
|
||||
cdef xmlNode* c_node_copy
|
||||
cdef xmlDoc* c_doc
|
||||
cdef _ElementTree etree
|
||||
cdef _Document doc
|
||||
if element is not None:
|
||||
doc = element._doc
|
||||
elif file is not None:
|
||||
try:
|
||||
doc = _parseDocument(file, parser, None)
|
||||
except _TargetParserResult as result_container:
|
||||
return result_container.result
|
||||
else:
|
||||
c_doc = _newXMLDoc()
|
||||
doc = _documentFactory(c_doc, parser)
|
||||
|
||||
if element is not None:
|
||||
doc = element._doc
|
||||
elif file is not None:
|
||||
try:
|
||||
doc = _parseDocument(file, parser, None)
|
||||
except _TargetParserResult as result_container:
|
||||
return result_container.result
|
||||
else:
|
||||
c_doc = _newXMLDoc()
|
||||
doc = _documentFactory(c_doc, parser)
|
||||
return _elementTreeFactory(doc, element)
|
||||
|
||||
return _elementTreeFactory(doc, element)
|
||||
|
||||
# Register _ElementTree as a virtual subclass of ElementTree
|
||||
ElementTree.register(_ElementTree)
|
||||
|
||||
# Remove "ABC" and typing helpers from module dict
|
||||
del ABC, Generic, TypeVar, T
|
||||
|
||||
def HTML(text, _BaseParser parser=None, *, base_url=None):
|
||||
"""HTML(text, parser=None, base_url=None)
|
||||
|
||||
Reference in New Issue
Block a user