Repository navigation
feat(research): findings->KB facts + grounded /research synthesis (#12622) - #13010
Merged
Merged
Conversation
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Auto-regenerated (py3.14) to match the backend schema so the required verify-generated-types gate passes. Triggered by auto-fix-generated-types.yml.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This was referenced Jul 30, 2026
feat(research): N-source corroboration + contradiction surfacing + KB promotion gate (#12623)
#13011
Merged
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Read #12622 + umbrella #12621 and the merged design doc (
docs/architecture/RESEARCH_AGENT_PRECISION_EFFICIENCY_DESIGN.md, landed via PR #12643). Verified premise before coding:services/research/did not exist — nothing to duplicate.knowledge_base_factory.get_knowledge_base()returns the composedKnowledgeBase(Facts/Documents/Search/Collections mixins).add_document()internally callsstore_fact()— "documents" and "facts" are the same Redis+Chroma record, differentiated only by metadata (source_type), matching the design's KB-native model claim./researchcall path is a standalone FastAPI router, notchat_workflow._dispatch_tool_callor the (inert)AgentLoop— this matches the design's own owner decision ("first consumer: dedicated/researchAPI endpoint"; chat/deep-research wiring is explicitly future work). Mounted exactly likeservices/autoresearch/routes.pyviainitialization/router_registry/feature_routers.py.researchare NOT visible to general chat/RAG") was not yet true: the live chat-RAG injection path (async_chat_workflow.py::_execute_kb_search) callskb.search()with no metadata filter, so a newly-landedcollection="research"fact would otherwise leak into every chat turn. Verified empirically against the installed ChromaDB (1.5.5) that a{"collection": {"$ne": "research"}}filter correctly includes facts lacking the field entirely (pre-existing facts) while excluding quarantined ones — then wired that filter into_execute_kb_search.Design vs issue: no discrepancies found: #12622's acceptance criteria map directly onto design §9 Phase 0 scope (no Planner loop, no N-source corroboration — both later phases, #12624/#12623).
What Changed
New
services/research/package (composes existing modules only, per design §6):models.py— wire contract:ResearchRequest/ResearchResponse/Citation/ResearchFactOut, plus internalExtractedClaim/StoredFact/ResearchBudget.extractor.py— LLM atomic-claim extraction from a fetched page, with a deterministic anti-hallucination overlap guard (a claim must share ≥50% of its distinctive words with the source text or it's dropped).synthesizer.py— grounded LLM synthesis constrained to retrieved facts; every[F<n>]citation marker is post-checked against the actual fact set, invented markers are stripped, and an answer with zero surviving citations is replaced with an explicit caveat (anti-hallucinated-citation, per acceptance criteria).orchestrator.py—ResearchOrchestrator: the one new coordinator. Bounded pipeline (no Planner loop — Phase 2 per design): search-registry discovery →WebFetcherfetch →KB.add_document(source of record) → claim extraction →KB.store_fact(quarantinedresearchcollection,unique_key-deduped) → grounded synthesis. Budget (max_sources,max_content_chars,fetch_timeout_seconds) is clamped to newautobot_shared.ssot_configfields (never hardcoded):AUTOBOT_RESEARCH_MAX_SOURCES(default 5),AUTOBOT_RESEARCH_MAX_CONTENT_CHARS(8000),AUTOBOT_RESEARCH_FETCH_TIMEOUT_SECONDS(15),AUTOBOT_RESEARCH_QUARANTINE_COLLECTION(research).routes.py—POST /researchbehindDepends(get_current_user), mounted at/api/researchviafeature_routers.py(same data-driven pattern as/api/autoresearch).Quarantine enforcement:
async_chat_workflow.py::_execute_kb_searchnow passesfilters={"collection": {"$ne": "research"}}tokb.search(), so web-research facts stay invisible to the live chat-RAG path until Phase 1's promotion gate (#12623).contradictions[]is structurally present in the response but always empty in this PR — there is no N-source corroborator yet (design §9 Phase 1, #12623); documented inmodels.pyrather than silently omitted.Discovered (filed, not fixed here — different blast radius): #13009 — three other
kb.search()call sites (ai_hardware_accelerator.py:834,advanced_rag_optimizer.py:395,utils/hybrid_search.py:472) call with no metadata filter and would also leak quarantined facts if/when they're reachable from a live general-chat surface; their production-reachability wasn't independently verified in this PR.Verification
py_compile,flake8 --max-line-length=120,black --check --line-length=120(this repo's 120-col convention, not Black's 88 default): all clean on every new/touched file.services/research/(extractor: 7, synthesizer: 6, orchestrator: 6, routes: 5) + 1 new chat-workflow quarantine test — all pass.cp-swapped baseline diff (notgit stash, per worktree rules): swapped in the pre-PR versions ofasync_chat_workflow.py/ssot_config.py/feature_routers.pyand reran the pre-existing suites (tests/unit/chat_workflow/+ssot_config_test.py, 331 tests): baseline failed only the new quarantine test (expected — it asserts new behavior) with 330 passing; after restoring the PR's files, all 331 pass, 0 regressions.tests/test_startup_imports.py: 350/350 passed, unchanged before/after.initialization.router_registry.feature_routers.load_feature_routers()run standalone — baseline (pre-PR) loads 121/121 feature routers with 0 failures; with this PR it loads 122/122 with 0 failures, and the load-result registry confirms{"name": "research", "module": "services.research.routes", "loaded": True, "error": None}, mounted at prefix/research(→/api/researchafterapp_factory's/apiprefix, same as every other feature router). This is the real, live call path — notchat_workflow._dispatch_tool_calland not the inertAgentLoop.Model Used
Claude Opus 4.8 (1M context)