gather-engine 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gather_engine-2.2.0/src/gather_engine.egg-info → gather_engine-2.3.0}/PKG-INFO +4 -4
- {gather_engine-2.2.0 → gather_engine-2.3.0}/README.md +3 -3
- {gather_engine-2.2.0 → gather_engine-2.3.0}/pyproject.toml +1 -1
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/__init__.py +1 -1
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/report.py +56 -10
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/report_cmd.py +14 -1
- {gather_engine-2.2.0 → gather_engine-2.3.0/src/gather_engine.egg-info}/PKG-INFO +4 -4
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather_engine.egg-info/SOURCES.txt +1 -0
- gather_engine-2.3.0/tests/test_report_quotes.py +119 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/LICENSE +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/setup.cfg +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/__main__.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/_vendor/__init__.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/_vendor/safe_spawn.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/api.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/arxiv.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/availability.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/backends.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/backends_browser.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/benchmark.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/browser.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/browser_evidence.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/cache.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/captions.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/channel.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/channel_ledger.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/citecheck.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/cli_ext.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/client_mcp.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/client_network.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/commands.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/context.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/corpus_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/crawl.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/credentials.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/derive.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/digest.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/docs.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/docs_tool.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/dom.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/export.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/extract.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/fastparse.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/federation.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/federation_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/federation_policy.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/federation_receipt.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/feed.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/fetch.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/filter_ledger.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/flagship.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/grant_checks.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/grants.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/http_call.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/interop.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/item.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/ledger_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/ledger_verify.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/local_model.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/localpath.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/mcp.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/method.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/model.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/monitor.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/net.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/ocr.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pacing.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/payloads.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pdf.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot_bundle.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot_commands.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot_manifest.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot_report.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/pilot_sources.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/provenance.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/recall.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/reddit.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/reddit_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/reddit_items.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/route.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/run.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/run_config.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/schema_extract.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/scholar.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/scope.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/search.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/source.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/spawn.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/store.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/stream.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/track.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/transcribe.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/video.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/video_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/video_source.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/web.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/web_commands.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/youtube_api.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/youtube_cmd.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/youtube_probe.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/youtube_route.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather/ytdlp.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather_engine.egg-info/dependency_links.txt +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather_engine.egg-info/entry_points.txt +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather_engine.egg-info/requires.txt +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/src/gather_engine.egg-info/top_level.txt +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_api.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_arxiv.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_availability.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_backends.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_backends_browser.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_backends_card.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_benchmark.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_browser_evidence.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_cache.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_captions.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_channel.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_channel_ledger.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_citecheck.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_cli_web.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_disclosure.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_network.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_package.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_profile.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_setup.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_client_vendored.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_context.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_context_mountinfo.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_crawl.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_credentials.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_demo.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_derive.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_device_names.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_digest.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_docs.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_dom.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_export.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_extract.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_fastparse.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_federation.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_federation_cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_federation_receipt.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_feed.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_fetch.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_filter_ledger.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_flagship_cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_grants.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_hard_sources.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_http_call.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_interop.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_item.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_localpath.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_mcp.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_mcp_annotations.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_method.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_model.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_monitor.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_net.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_nonlocal_paths.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pacing.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_package.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pdf.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_bundle.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_manifest.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_refresh.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_report.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_sample.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_showcase.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_pilot_sources.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_provenance.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_recall.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_reddit.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_reddit_grants.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_release_workflow.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_repo_art.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_repo_card.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_report.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_route.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_run.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_schema_extract.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_scholar.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_scope.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_search.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_spawn_children.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_spawn_working_folder.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_store.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_stream.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_track.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_vendored.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_verify_pilot_cli.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_video.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_video_source.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_video_spawn.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_web.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_web_refine.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_xml_safety.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_youtube_api.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_youtube_grants.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_youtube_probe.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_youtube_route.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_ytdlp.py +0 -0
- {gather_engine-2.2.0 → gather_engine-2.3.0}/tests/test_ytdlp_real.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gather-engine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Accountable research intake for difficult sources, with provenance receipts and witnessed digests.
|
|
5
5
|
Author: Zain Dana Harper
|
|
6
6
|
License-Expression: LicenseRef-FSL-1.1-MIT
|
|
@@ -34,7 +34,7 @@ Dynamic: license-file
|
|
|
34
34
|
[](https://pypi.org/project/gather-engine/)
|
|
35
35
|

|
|
36
36
|

|
|
37
|
-

|
|
38
38
|
|
|
39
39
|
gather pulls research out of the places most tools break on: arXiv papers, authenticated JSON APIs, JavaScript-rendered pages via a real headless browser, scanned images through OCR, and audio through transcription, alongside video, web, feeds, and local docs. The core runs with zero third-party runtime dependencies, and the same engine is reachable from the CLI, MCP tools, and plain Python. Every run writes a receipt you can re-check.
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ gather pulls research out of the places most tools break on: arXiv papers, authe
|
|
|
42
42
|
|
|
43
43
|
## Current status
|
|
44
44
|
|
|
45
|
-
`gather-engine 2.
|
|
45
|
+
`gather-engine 2.3.0` is the current source version. Core intake,
|
|
46
46
|
content-addressed corpus storage, exact UTF-8 source-byte receipts, readable
|
|
47
47
|
context selection, descriptor handoff, pilot monitoring, CLI, Python API, and MCP
|
|
48
48
|
surfaces are present in this checkout. Browser rendering, OCR, audio, and fast
|
|
@@ -83,7 +83,7 @@ fabricated field cannot reach the corpus by being plausible.
|
|
|
83
83
|
- **Structured extraction with a hallucination check.** `gather.schema_extract` binds schema fields to source nodes, and `verify_record` rejects any LLM-proposed field value not grounded in the fetched content.
|
|
84
84
|
- **Streaming extraction.** `gather.stream` parses an HTML stream chunk by chunk and emits partial-update commits as blocks complete, each folded into a hash chain, so a streamed extraction is replayable.
|
|
85
85
|
- **Hard-source adapters behind one shape.** Video with captions and comments (`yt-dlp` first, with the official YouTube Data API as a fallback when you supply your own key; each item records which path served it and how fast), Reddit through its official Data API with your own app credentials, static web, RSS/Atom feeds, local docs, arXiv, PDFs (`pdftotext`), authenticated JSON APIs (token from env, never logged), JS-rendered pages (headless Chromium), scanned images (`tesseract`), and audio (`whisper`). Each external tool is optional and only needed for its own adapter.
|
|
86
|
-
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify.
|
|
86
|
+
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify. An answer that cites an excerpt without quoting it is sent back for a rewrite, up to twice.
|
|
87
87
|
- **Filter ledgers.** `--scope TERMS --ledger DIR` writes one row per dropped item with its reason code, plus the input, so `gather ledger verify` can recompute every count.
|
|
88
88
|
- **Channel and playlist intake.** `gather channel <url> --store DIR` lists a channel's videos, shorts, and streams tabs (or one playlist) and gathers every entry with bounded concurrency, resumable from a per-pass ledger. Metadata and comments run as one pass and captions as another (`--no-captions`, `--captions-only`), with polite pacing, bounded backoff on HTTP 429 and session rate limits, a stop at the first bot check (never retried or answered), one caption track per video, and a run summary that counts every outcome by reason.
|
|
89
89
|
- **Scholarly-graph federation.** `gather scholar` queries OpenAlex, Semantic Scholar, and Crossref in one call, dedupes results by normalized DOI (never a fuzzy title match), and can capture citation edges as first-class records with `--edges`.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://pypi.org/project/gather-engine/)
|
|
9
9
|

|
|
10
10
|

|
|
11
|
-

|
|
12
12
|
|
|
13
13
|
gather pulls research out of the places most tools break on: arXiv papers, authenticated JSON APIs, JavaScript-rendered pages via a real headless browser, scanned images through OCR, and audio through transcription, alongside video, web, feeds, and local docs. The core runs with zero third-party runtime dependencies, and the same engine is reachable from the CLI, MCP tools, and plain Python. Every run writes a receipt you can re-check.
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ gather pulls research out of the places most tools break on: arXiv papers, authe
|
|
|
16
16
|
|
|
17
17
|
## Current status
|
|
18
18
|
|
|
19
|
-
`gather-engine 2.
|
|
19
|
+
`gather-engine 2.3.0` is the current source version. Core intake,
|
|
20
20
|
content-addressed corpus storage, exact UTF-8 source-byte receipts, readable
|
|
21
21
|
context selection, descriptor handoff, pilot monitoring, CLI, Python API, and MCP
|
|
22
22
|
surfaces are present in this checkout. Browser rendering, OCR, audio, and fast
|
|
@@ -57,7 +57,7 @@ fabricated field cannot reach the corpus by being plausible.
|
|
|
57
57
|
- **Structured extraction with a hallucination check.** `gather.schema_extract` binds schema fields to source nodes, and `verify_record` rejects any LLM-proposed field value not grounded in the fetched content.
|
|
58
58
|
- **Streaming extraction.** `gather.stream` parses an HTML stream chunk by chunk and emits partial-update commits as blocks complete, each folded into a hash chain, so a streamed extraction is replayable.
|
|
59
59
|
- **Hard-source adapters behind one shape.** Video with captions and comments (`yt-dlp` first, with the official YouTube Data API as a fallback when you supply your own key; each item records which path served it and how fast), Reddit through its official Data API with your own app credentials, static web, RSS/Atom feeds, local docs, arXiv, PDFs (`pdftotext`), authenticated JSON APIs (token from env, never logged), JS-rendered pages (headless Chromium), scanned images (`tesseract`), and audio (`whisper`). Each external tool is optional and only needed for its own adapter.
|
|
60
|
-
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify.
|
|
60
|
+
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify. An answer that cites an excerpt without quoting it is sent back for a rewrite, up to twice.
|
|
61
61
|
- **Filter ledgers.** `--scope TERMS --ledger DIR` writes one row per dropped item with its reason code, plus the input, so `gather ledger verify` can recompute every count.
|
|
62
62
|
- **Channel and playlist intake.** `gather channel <url> --store DIR` lists a channel's videos, shorts, and streams tabs (or one playlist) and gathers every entry with bounded concurrency, resumable from a per-pass ledger. Metadata and comments run as one pass and captions as another (`--no-captions`, `--captions-only`), with polite pacing, bounded backoff on HTTP 429 and session rate limits, a stop at the first bot check (never retried or answered), one caption track per video, and a run summary that counts every outcome by reason.
|
|
63
63
|
- **Scholarly-graph federation.** `gather scholar` queries OpenAlex, Semantic Scholar, and Crossref in one call, dedupes results by normalized DOI (never a fuzzy title match), and can capture citation edges as first-class records with `--edges`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gather-engine"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "Accountable research intake for difficult sources, with provenance receipts and witnessed digests."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -28,7 +28,7 @@ from gather.scope import filter_scope, in_scope
|
|
|
28
28
|
from gather.source import Catalog, Source
|
|
29
29
|
from gather.store import Corpus
|
|
30
30
|
|
|
31
|
-
__version__ = "2.
|
|
31
|
+
__version__ = "2.3.0"
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
"Catalog", "Corpus", "CorpusRootDescriptor", "CorpusRootIdentity", "Digest", "Item", "NullProvenanceProvider", "NullSynthesizer",
|
|
@@ -6,6 +6,12 @@ the answer. The report becomes one Item with ``method="synthesized"``, ``derived
|
|
|
6
6
|
the excerpts' content hashes, and the full check result in ``meta["citation_check"]``. An
|
|
7
7
|
unverifiable citation is marked in that result, never removed from the text.
|
|
8
8
|
|
|
9
|
+
Every citation must quote its excerpt. When an answer carries a bracketed number with no quote
|
|
10
|
+
before it (status ``unchecked``), ``write_report`` asks again, naming the sentences at fault, up to
|
|
11
|
+
``quote_retries`` times (default ``QUOTE_RETRIES``). It keeps the first answer with no unquoted
|
|
12
|
+
citation, or else the last answer unchanged, with ``meta["quote_requirement"]`` set to ``"unmet"``.
|
|
13
|
+
Each model call is recorded in ``meta["attempts"]``.
|
|
14
|
+
|
|
9
15
|
The model is a ``LocalModel``: loopback endpoints only, so only a model the user runs on their own
|
|
10
16
|
machine writes the report. The method follows the one-pass local report writers described for
|
|
11
17
|
AstaBrief; Gather ships no AstaBrief weights, data or outputs.
|
|
@@ -17,22 +23,25 @@ import json
|
|
|
17
23
|
import time
|
|
18
24
|
from collections.abc import Callable, Sequence
|
|
19
25
|
|
|
20
|
-
from gather.citecheck import check_report
|
|
26
|
+
from gather.citecheck import check_report, sentences
|
|
21
27
|
from gather.item import Item, content_hash, make_item
|
|
22
28
|
from gather.local_model import LocalModel
|
|
23
29
|
|
|
24
30
|
REPORT_SCHEMA = "gather.report/1"
|
|
25
31
|
MAX_EXCERPT_CHARS = 4000
|
|
32
|
+
QUOTE_RETRIES = 2
|
|
26
33
|
|
|
27
34
|
SYSTEM = (
|
|
28
35
|
"You write short research answers from numbered excerpts and nothing else. Rules: "
|
|
29
36
|
"1. Every sentence that states a fact must contain an exact quotation copied word for word from "
|
|
30
37
|
"one excerpt, in double quotes, followed by that excerpt's number in square brackets, like this: "
|
|
31
38
|
'"exact words from the excerpt" [2]. '
|
|
32
|
-
"2.
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
39
|
+
"2. Every bracketed excerpt number must come directly after the double-quoted words it supports. "
|
|
40
|
+
"A bracketed number with no quotation right before it is not allowed. "
|
|
41
|
+
"3. Quote at least four consecutive words. Do not change, shorten or join the quoted words. "
|
|
42
|
+
"4. Cite only the numbered excerpts. Use no outside knowledge. "
|
|
43
|
+
"5. If the excerpts do not answer the question, write: The excerpts do not answer this question. "
|
|
44
|
+
"6. Write at most five sentences, in plain prose, with no headings."
|
|
36
45
|
)
|
|
37
46
|
|
|
38
47
|
|
|
@@ -41,19 +50,56 @@ def excerpts_block(excerpts: Sequence[Item], max_chars: int = MAX_EXCERPT_CHARS)
|
|
|
41
50
|
for n, it in enumerate(excerpts, 1))
|
|
42
51
|
|
|
43
52
|
|
|
53
|
+
def unquoted_sentences(text: str, check: dict) -> list[str]:
|
|
54
|
+
"""The sentences of ``text`` holding a citation with no quote (status ``unchecked``)."""
|
|
55
|
+
found = sorted({c["sentence"] for c in check["citations"] if c["status"] == "unchecked"})
|
|
56
|
+
split = sentences(text)
|
|
57
|
+
return [split[n] for n in found]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def retry_prompt(user: str, previous: str, faulty: Sequence[str]) -> str:
|
|
61
|
+
"""The user message for a regeneration: the original request, the previous answer, and the
|
|
62
|
+
sentences whose citations had no quote."""
|
|
63
|
+
listed = "\n".join(f"- {s}" for s in faulty)
|
|
64
|
+
return (f"{user}\n\nYour previous answer:\n{previous}\n\n"
|
|
65
|
+
f"These sentences cite an excerpt number with no quotation before it:\n{listed}\n\n"
|
|
66
|
+
"Rewrite the whole answer. Every citation must quote its excerpt: put the exact words, copied "
|
|
67
|
+
'word for word, in double quotes directly before the excerpt number, like "exact words" [2].')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ask_until_quoted(model: LocalModel, user: str, texts: list[str], retries: int):
|
|
71
|
+
"""Call the model until an answer has no unquoted citation or the retries run out."""
|
|
72
|
+
attempts: list[dict] = []
|
|
73
|
+
prompt = user
|
|
74
|
+
for _ in range(retries + 1):
|
|
75
|
+
reply = model.chat(SYSTEM, prompt)
|
|
76
|
+
check = check_report(reply.text, texts)
|
|
77
|
+
faulty = unquoted_sentences(reply.text, check)
|
|
78
|
+
attempts.append({"elapsed_s": round(reply.elapsed_s, 3), "unchecked": check["counts"]["unchecked"]})
|
|
79
|
+
if not faulty:
|
|
80
|
+
break
|
|
81
|
+
prompt = retry_prompt(user, reply.text, faulty)
|
|
82
|
+
return reply, check, attempts
|
|
83
|
+
|
|
84
|
+
|
|
44
85
|
def write_report(question: str, excerpts: Sequence[Item], model: LocalModel, *,
|
|
45
|
-
wall: Callable[[], float] = time.time) -> Item:
|
|
46
|
-
"""Ask the local model,
|
|
86
|
+
quote_retries: int = QUOTE_RETRIES, wall: Callable[[], float] = time.time) -> Item:
|
|
87
|
+
"""Ask the local model, regenerate answers with unquoted citations, check every citation, and
|
|
88
|
+
return the report Item."""
|
|
47
89
|
if not question.strip():
|
|
48
90
|
raise ValueError("a report needs a question")
|
|
49
91
|
if not excerpts:
|
|
50
92
|
raise ValueError("a report needs at least one excerpt")
|
|
93
|
+
if quote_retries < 0:
|
|
94
|
+
raise ValueError("quote retries must be 0 or more")
|
|
51
95
|
user = f"Question: {question.strip()}\n\nExcerpts:\n\n{excerpts_block(excerpts)}"
|
|
52
|
-
reply = model.chat(SYSTEM, user)
|
|
53
96
|
texts = [it.text.strip()[:MAX_EXCERPT_CHARS] for it in excerpts]
|
|
54
|
-
check =
|
|
97
|
+
reply, check, attempts = _ask_until_quoted(model, user, texts, quote_retries)
|
|
55
98
|
meta = {"schema": REPORT_SCHEMA, "question": question.strip(), "model": reply.model,
|
|
56
|
-
"endpoint": "loopback", "elapsed_s": round(
|
|
99
|
+
"endpoint": "loopback", "elapsed_s": round(sum(a["elapsed_s"] for a in attempts), 3),
|
|
100
|
+
"quote_retries": quote_retries, "attempts": attempts,
|
|
101
|
+
"quote_requirement": "unmet" if check["counts"]["unchecked"] else "met",
|
|
102
|
+
"citation_check": check}
|
|
57
103
|
return make_item(kind="report", id=f"report-{content_hash(question.strip())[:12]}", title=question.strip()[:120],
|
|
58
104
|
text=reply.text, source="report", ref=question.strip()[:200], method="synthesized",
|
|
59
105
|
fetched_at=float(wall()),
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
(the citation check alone, on any report text).
|
|
3
3
|
|
|
4
4
|
Both exit 0 when every citation is verified and 1 when any is not, so either can gate a pipeline.
|
|
5
|
+
``gather report`` asks the model again when a citation has no quote, up to ``--quote-retries``.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
10
|
+
import argparse
|
|
9
11
|
import json
|
|
10
12
|
import sys
|
|
11
13
|
from collections.abc import Callable
|
|
@@ -22,6 +24,8 @@ def add_report_parsers(sub, add_common: Callable) -> None:
|
|
|
22
24
|
rep.add_argument("--endpoint", default=DEFAULT_ENDPOINT,
|
|
23
25
|
help=f"OpenAI-compatible endpoint on this machine (default {DEFAULT_ENDPOINT})")
|
|
24
26
|
rep.add_argument("--timeout", type=float, default=600.0, metavar="S", help="seconds for the model's answer")
|
|
27
|
+
rep.add_argument("--quote-retries", type=_retries, default=2, metavar="N",
|
|
28
|
+
help="new answers to ask for when a citation has no quote (default 2; 0 turns this off)")
|
|
25
29
|
add_common(rep)
|
|
26
30
|
rep.set_defaults(func=cmd_report)
|
|
27
31
|
|
|
@@ -32,6 +36,13 @@ def add_report_parsers(sub, add_common: Callable) -> None:
|
|
|
32
36
|
chk.set_defaults(func=cmd_cite_check)
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
def _retries(value: str) -> int:
|
|
40
|
+
n = int(value)
|
|
41
|
+
if n < 0:
|
|
42
|
+
raise argparse.ArgumentTypeError("must be 0 or more")
|
|
43
|
+
return n
|
|
44
|
+
|
|
45
|
+
|
|
35
46
|
def _summary(check: dict) -> str:
|
|
36
47
|
counts = ", ".join(f"{k} {v}" for k, v in check["counts"].items() if v)
|
|
37
48
|
return (f"citations: {check['total']} ({counts or 'none'}); precision {check['precision']}; "
|
|
@@ -49,7 +60,8 @@ def cmd_report(args) -> int:
|
|
|
49
60
|
|
|
50
61
|
try:
|
|
51
62
|
model = LocalModel(args.model, endpoint=args.endpoint, timeout=args.timeout)
|
|
52
|
-
item = write_report(args.question, load_excerpts(args.excerpts), model
|
|
63
|
+
item = write_report(args.question, load_excerpts(args.excerpts), model,
|
|
64
|
+
quote_retries=args.quote_retries)
|
|
53
65
|
except (ValueError, OSError, RuntimeError) as exc:
|
|
54
66
|
print(f"report failed: {exc}", file=sys.stderr)
|
|
55
67
|
return 2
|
|
@@ -60,6 +72,7 @@ def cmd_report(args) -> int:
|
|
|
60
72
|
else:
|
|
61
73
|
print(item.text)
|
|
62
74
|
print(f"\n{_summary(check)}")
|
|
75
|
+
print(f"quote requirement: {item.meta['quote_requirement']} after {len(item.meta['attempts'])} model call(s)")
|
|
63
76
|
if args.store:
|
|
64
77
|
_emit([item], _scope(args), False, store=args.store)
|
|
65
78
|
return 0 if _all_verified(check) else 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gather-engine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Accountable research intake for difficult sources, with provenance receipts and witnessed digests.
|
|
5
5
|
Author: Zain Dana Harper
|
|
6
6
|
License-Expression: LicenseRef-FSL-1.1-MIT
|
|
@@ -34,7 +34,7 @@ Dynamic: license-file
|
|
|
34
34
|
[](https://pypi.org/project/gather-engine/)
|
|
35
35
|

|
|
36
36
|

|
|
37
|
-

|
|
38
38
|
|
|
39
39
|
gather pulls research out of the places most tools break on: arXiv papers, authenticated JSON APIs, JavaScript-rendered pages via a real headless browser, scanned images through OCR, and audio through transcription, alongside video, web, feeds, and local docs. The core runs with zero third-party runtime dependencies, and the same engine is reachable from the CLI, MCP tools, and plain Python. Every run writes a receipt you can re-check.
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ gather pulls research out of the places most tools break on: arXiv papers, authe
|
|
|
42
42
|
|
|
43
43
|
## Current status
|
|
44
44
|
|
|
45
|
-
`gather-engine 2.
|
|
45
|
+
`gather-engine 2.3.0` is the current source version. Core intake,
|
|
46
46
|
content-addressed corpus storage, exact UTF-8 source-byte receipts, readable
|
|
47
47
|
context selection, descriptor handoff, pilot monitoring, CLI, Python API, and MCP
|
|
48
48
|
surfaces are present in this checkout. Browser rendering, OCR, audio, and fast
|
|
@@ -83,7 +83,7 @@ fabricated field cannot reach the corpus by being plausible.
|
|
|
83
83
|
- **Structured extraction with a hallucination check.** `gather.schema_extract` binds schema fields to source nodes, and `verify_record` rejects any LLM-proposed field value not grounded in the fetched content.
|
|
84
84
|
- **Streaming extraction.** `gather.stream` parses an HTML stream chunk by chunk and emits partial-update commits as blocks complete, each folded into a hash chain, so a streamed extraction is replayable.
|
|
85
85
|
- **Hard-source adapters behind one shape.** Video with captions and comments (`yt-dlp` first, with the official YouTube Data API as a fallback when you supply your own key; each item records which path served it and how fast), Reddit through its official Data API with your own app credentials, static web, RSS/Atom feeds, local docs, arXiv, PDFs (`pdftotext`), authenticated JSON APIs (token from env, never logged), JS-rendered pages (headless Chromium), scanned images (`tesseract`), and audio (`whisper`). Each external tool is optional and only needed for its own adapter.
|
|
86
|
-
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify.
|
|
86
|
+
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify. An answer that cites an excerpt without quoting it is sent back for a rewrite, up to twice.
|
|
87
87
|
- **Filter ledgers.** `--scope TERMS --ledger DIR` writes one row per dropped item with its reason code, plus the input, so `gather ledger verify` can recompute every count.
|
|
88
88
|
- **Channel and playlist intake.** `gather channel <url> --store DIR` lists a channel's videos, shorts, and streams tabs (or one playlist) and gathers every entry with bounded concurrency, resumable from a per-pass ledger. Metadata and comments run as one pass and captions as another (`--no-captions`, `--captions-only`), with polite pacing, bounded backoff on HTTP 429 and session rate limits, a stop at the first bot check (never retried or answered), one caption track per video, and a run summary that counts every outcome by reason.
|
|
89
89
|
- **Scholarly-graph federation.** `gather scholar` queries OpenAlex, Semantic Scholar, and Crossref in one call, dedupes results by normalized DOI (never a fuzzy title match), and can capture citation edges as first-class records with `--edges`.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Every citation must quote its excerpt: answers with an unquoted citation are asked again."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from gather.item import make_item
|
|
7
|
+
from gather.local_model import LocalModel
|
|
8
|
+
from gather.report import QUOTE_RETRIES, SYSTEM, write_report
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def excerpt(n, text):
|
|
12
|
+
return make_item(kind="excerpt", id=str(n), title=f"E{n}", text=text, source="docs", ref="x",
|
|
13
|
+
method="file-read", fetched_at=0.0)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
EXCERPTS = [excerpt(1, "Every run writes a receipt you can re-check later."),
|
|
17
|
+
excerpt(2, "Gather stops at bot checks and records the reason.")]
|
|
18
|
+
QUOTED = 'Gather says "every run writes a receipt" [1].'
|
|
19
|
+
BARE = "Gather keeps receipts [1]. It also stops at bot checks [2]."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Tick:
|
|
23
|
+
t = 0.0
|
|
24
|
+
|
|
25
|
+
def __call__(self):
|
|
26
|
+
self.t += 2.0
|
|
27
|
+
return self.t
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def scripted(*answers):
|
|
31
|
+
"""A model that gives the answers in order and records every request."""
|
|
32
|
+
calls = []
|
|
33
|
+
|
|
34
|
+
def post(url, body, timeout):
|
|
35
|
+
calls.append(json.loads(body))
|
|
36
|
+
text = answers[min(len(calls), len(answers)) - 1]
|
|
37
|
+
return json.dumps({"model": "m", "choices": [{"message": {"content": text}}]}).encode()
|
|
38
|
+
return LocalModel("m", post=post, clock=Tick()), calls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_the_prompt_forbids_a_bracketed_number_without_a_quote():
|
|
42
|
+
assert "A bracketed number with no quotation right before it is not allowed." in SYSTEM
|
|
43
|
+
assert QUOTE_RETRIES == 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_an_unquoted_answer_is_asked_again_and_the_quoted_answer_is_kept():
|
|
47
|
+
model, calls = scripted(BARE, QUOTED)
|
|
48
|
+
item = write_report("What does Gather record?", EXCERPTS, model)
|
|
49
|
+
assert len(calls) == 2 and item.text == QUOTED
|
|
50
|
+
assert item.meta["quote_requirement"] == "met"
|
|
51
|
+
assert item.meta["attempts"] == [{"elapsed_s": 2.0, "unchecked": 2}, {"elapsed_s": 2.0, "unchecked": 0}]
|
|
52
|
+
assert item.meta["elapsed_s"] == 4.0
|
|
53
|
+
assert item.meta["citation_check"]["counts"]["verified"] == 1
|
|
54
|
+
retry = calls[1]["messages"]
|
|
55
|
+
assert retry[0]["content"] == SYSTEM
|
|
56
|
+
assert retry[1]["content"].startswith(calls[0]["messages"][1]["content"])
|
|
57
|
+
assert f"Your previous answer:\n{BARE}" in retry[1]["content"]
|
|
58
|
+
assert "- Gather keeps receipts [1].\n- It also stops at bot checks [2]." in retry[1]["content"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_retries_stop_at_the_cap_and_the_last_answer_is_kept_unchanged():
|
|
62
|
+
last = 'It writes "every run writes a receipt" [1] and more [2].'
|
|
63
|
+
model, calls = scripted(BARE, BARE, last, QUOTED)
|
|
64
|
+
item = write_report("q", EXCERPTS, model)
|
|
65
|
+
assert len(calls) == 1 + QUOTE_RETRIES
|
|
66
|
+
assert item.text == last
|
|
67
|
+
assert item.meta["quote_requirement"] == "unmet"
|
|
68
|
+
check = item.meta["citation_check"]
|
|
69
|
+
assert check["counts"]["unchecked"] == 1 and check["counts"]["verified"] == 1
|
|
70
|
+
assert check["total"] == 2 and check["precision"] == 0.5
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize("answer", [
|
|
74
|
+
QUOTED,
|
|
75
|
+
'Gather "invents facts out of nothing at all" [1].',
|
|
76
|
+
'Gather "keeps receipts" [1].',
|
|
77
|
+
"The excerpts do not answer this question.",
|
|
78
|
+
])
|
|
79
|
+
def test_only_an_unquoted_citation_triggers_a_new_answer(answer):
|
|
80
|
+
model, calls = scripted(answer, QUOTED)
|
|
81
|
+
item = write_report("q", EXCERPTS, model)
|
|
82
|
+
assert len(calls) == 1 and item.text == answer
|
|
83
|
+
assert item.meta["quote_requirement"] == "met"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_zero_retries_makes_one_call_and_marks_the_requirement_unmet():
|
|
87
|
+
model, calls = scripted(BARE, QUOTED)
|
|
88
|
+
item = write_report("q", EXCERPTS, model, quote_retries=0)
|
|
89
|
+
assert len(calls) == 1 and item.text == BARE
|
|
90
|
+
assert item.meta["quote_requirement"] == "unmet" and item.meta["quote_retries"] == 0
|
|
91
|
+
with pytest.raises(ValueError):
|
|
92
|
+
write_report("q", EXCERPTS, model, quote_retries=-1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cli(monkeypatch, tmp_path, *answers):
|
|
96
|
+
import gather.local_model as lm
|
|
97
|
+
model, calls = scripted(*answers)
|
|
98
|
+
monkeypatch.setattr(lm, "LocalModel", lambda *a, **k: model)
|
|
99
|
+
ex = tmp_path / "ex.json"
|
|
100
|
+
ex.write_text(json.dumps([{"text": e.text} for e in EXCERPTS]), encoding="utf-8")
|
|
101
|
+
return str(ex), calls
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_gather_report_regenerates_by_default(monkeypatch, tmp_path, capsys):
|
|
105
|
+
from gather.cli import main
|
|
106
|
+
ex, calls = _cli(monkeypatch, tmp_path, BARE, QUOTED)
|
|
107
|
+
assert main(["report", "q", "--excerpts", ex, "--model", "m"]) == 0
|
|
108
|
+
assert len(calls) == 2
|
|
109
|
+
assert "quote requirement: met after 2 model call(s)" in capsys.readouterr().out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_gather_report_quote_retries_flag(monkeypatch, tmp_path, capsys):
|
|
113
|
+
from gather.cli import main
|
|
114
|
+
ex, calls = _cli(monkeypatch, tmp_path, BARE, QUOTED)
|
|
115
|
+
assert main(["report", "q", "--excerpts", ex, "--model", "m", "--quote-retries", "0"]) == 1
|
|
116
|
+
assert len(calls) == 1
|
|
117
|
+
with pytest.raises(SystemExit) as exc:
|
|
118
|
+
main(["report", "q", "--excerpts", ex, "--model", "m", "--quote-retries", "-1"])
|
|
119
|
+
assert exc.value.code == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|