Skip to content

feat(research): findings->KB facts + grounded /research synthesis (#12622) - #13010

Merged
mrveiss merged 6 commits into
Dev_new_guifrom
issue-12622
Jul 30, 2026
Merged

mrveiss merged 6 commits into
Dev_new_guifrom
issue-12622

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 30, 2026

Copy link
Copy Markdown
Owner

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.
  • KB storage interface is canonical and singular: knowledge_base_factory.get_knowledge_base() returns the composed KnowledgeBase (Facts/Documents/Search/Collections mixins). add_document() internally calls store_fact() — "documents" and "facts" are the same Redis+Chroma record, differentiated only by metadata (source_type), matching the design's KB-native model claim.
  • The real /research call path is a standalone FastAPI router, not chat_workflow._dispatch_tool_call or the (inert) AgentLoop — this matches the design's own owner decision ("first consumer: dedicated /research API endpoint"; chat/deep-research wiring is explicitly future work). Mounted exactly like services/autoresearch/routes.py via initialization/router_registry/feature_routers.py.
  • Quarantine claim ("facts in research are NOT visible to general chat/RAG") was not yet true: the live chat-RAG injection path (async_chat_workflow.py::_execute_kb_search) calls kb.search() with no metadata filter, so a newly-landed collection="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 internal ExtractedClaim/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 → WebFetcher fetch → KB.add_document (source of record) → claim extraction → KB.store_fact (quarantined research collection, unique_key-deduped) → grounded synthesis. Budget (max_sources, max_content_chars, fetch_timeout_seconds) is clamped to new autobot_shared.ssot_config fields (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 /research behind Depends(get_current_user), mounted at /api/research via feature_routers.py (same data-driven pattern as /api/autoresearch).

Quarantine enforcement: async_chat_workflow.py::_execute_kb_search now passes filters={"collection": {"$ne": "research"}} to kb.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 in models.py rather 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.
  • New tests, including failure paths: 24 in services/research/ (extractor: 7, synthesizer: 6, orchestrator: 6, routes: 5) + 1 new chat-workflow quarantine test — all pass.
  • cp-swapped baseline diff (not git stash, per worktree rules): swapped in the pre-PR versions of async_chat_workflow.py / ssot_config.py / feature_routers.py and 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.
  • Reachability proof (the key evidence for this issue): 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/research after app_factory's /api prefix, same as every other feature router). This is the real, live call path — not chat_workflow._dispatch_tool_call and not the inert AgentLoop.

Model Used

Claude Opus 4.8 (1M context)

@github-actions

Copy link
Copy Markdown
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

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant