diff --git a/apps/api/.env.example b/apps/api/.env.example index 4a01c6d8d..7f51ce287 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -94,6 +94,10 @@ RETRIEVAL_WALLET_PER_RETRIEVE_STEP_BUDGET=40000 RETRIEVAL_WALLET_PER_SYNTHESIZE_STEP_BUDGET=6000 RETRIEVAL_WORKFLOW_PARALLEL_MAX=3 +# Agentic retrieval (LLM-driven hierarchical navigation). +# Set to false to fall back to legacy 3-channel RRF mode. +RETRIEVAL_AGENTIC_ENABLED=true + # File handling defaults SUPPORTED_EXTENSIONS=.doc,.docx,.pdf,.txt,.xls,.xlsx,.csv,.pptx,.jpg,.jpeg,.png,.md MAX_FILE_SIZE=104857600 @@ -128,4 +132,4 @@ ILOVEAPI_TIMEOUT=120 # Legacy parser compatibility fields. ALL_DF_COLS=content,path,type,length,keywords,summary,know_id,tokens,connectto,addtime,page_nums -SPLIT_CHAR=--> +SPLIT_CHAR=/ diff --git a/apps/worker/.env.example b/apps/worker/.env.example index 6c46d9df7..a57b458fc 100644 --- a/apps/worker/.env.example +++ b/apps/worker/.env.example @@ -94,6 +94,10 @@ RETRIEVAL_WALLET_PER_RETRIEVE_STEP_BUDGET=40000 RETRIEVAL_WALLET_PER_SYNTHESIZE_STEP_BUDGET=6000 RETRIEVAL_WORKFLOW_PARALLEL_MAX=3 +# Agentic retrieval (LLM-driven hierarchical navigation). +# Set to false to fall back to legacy 3-channel RRF mode. +RETRIEVAL_AGENTIC_ENABLED=true + # Required for specific features: billing and analytics BILLING_ENABLED=false STRIPE_SECRET_KEY= @@ -118,5 +122,5 @@ MAX_FILE_SIZE=104857600 # Legacy parser compatibility fields. ALL_DF_COLS=content,path,type,length,keywords,summary,know_id,tokens,connectto,addtime,page_nums -SPLIT_CHAR=--> +SPLIT_CHAR=/ diff --git a/docs/agentic-rag-audit-20260513.md b/docs/agentic-rag-audit-20260513.md new file mode 100644 index 000000000..fe09703ce --- /dev/null +++ b/docs/agentic-rag-audit-20260513.md @@ -0,0 +1,353 @@ +# Agentic RAG 流程审计更新 + +日期:2026-05-13 + +范围:基于 `AGENTS.md`、`.agent/skills/agentic_debug_patterns/SKILL.md`、既有 trace 目录 `/Users/wuchengke/Desktop/agentic_e2e_traces/20260513_183340`,以及额外运行的典型用例结果。 + +额外 trace 输出目录: + +- `/Users/wuchengke/Desktop/agentic_e2e_traces/20260513_190749_extra` +- `/Users/wuchengke/Desktop/agentic_e2e_traces/20260513_210646_extra_batch` + +## 结论摘要 + +这套 agentic RAG 的总体方向是成立的:外层 workflow 可以把复杂问题拆成多个 retrieve/synthesize step 并并发执行;内层 retrieval agent 能基于 KG 选文档、树导航、发现补充路径,并把 connected image/table 内嵌回证据树。尤其是全局图片/图表类问题,已有路径可以通过 `connect_to` 找回资源所属文本 section。 + +但从 harness 工程师和真实用户视角看,目前仍有几个核心逻辑风险: + +1. 图表资源的“证据渲染归属”和“返回引用归属”不一致。渲染树里通常能用 `connect_to` 找到底层 owner section,但 `referenced_chunks`/citation 仍可能显示物理路径 `Root`,这会直接破坏用户理解图表出处。 +2. discovery merge 过于积极。即使 BFS 已经 `STOP`,后置 discovery 仍会把深层或邻近年份路径并入证据,导致 outline 类问题和窄 section 问题出现噪声。 +3. 预算和状态分类混淆。无证据问题会被包装成 `budget_stop`,掩盖真实原因;同时 bootstrap/revision 小预算耗尽时,整体 wallet 仍可能很充足,用户看到的失败原因不准确。 +4. 多文件/多 step 并发在外层有效,但单 step 内的多 doc 导航仍偏串行;更重要的是 `discovery_auto` 会把弱相关文档强行并入,容易在跨年份、跨主题问题上污染预算和证据。 +5. 回答 JSON 解析不够稳健。`attempt_answer` 返回含换行的 JSON-like 文本时会解析失败,单步用户可能看到 JSON wrapper。 +6. trace DB schema 与 ORM 不一致,导致 agentic trace 入库失败,削弱 harness 可观测性。 + +## 本次补跑用例 + +### T1_Outline_Extra + +Query: + +> 民生证券这份利率专题研报的整体结构是什么?包含哪些主要章节? + +结果: + +- Router:`workflow_single_step` +- LLM calls:4 +- refs:13 +- elapsed:约 13s +- action:`kg_document_select -> navigate -> discovery_select -> attempt_answer` + +观察: + +- `navigate` 在 root 层正确选择 `STOP`,这对“整体结构/主要章节”类问题是合理的。 +- 但后续 `discovery_select` 又选入了深层路径,如 `2 阶段性调整.../2.1.1 基本面企稳` 和 `5、2024:“资产荒”的极致演绎`。 +- 最终 evidence 约 7395 chars,answer 只有约 149 chars,说明证据明显过量。 +- `referenced_chunks` 里部分 image/table 的 section 显示为 `Root`,但 evidence tree 实际把它们挂在更具体 leaf section 下。 + +判断: + +这是 discovery merge 策略的问题。对 root outline 查询,BFS 已经完成任务后,不应默认再并入深层 discovery 结果。否则用户问“目录结构”,结果引用中会混入某些深层图表,影响可信度。 + +### T2_Deep_Section_Extra + +Query: + +> 2016年债市走牛的几个阶段中,机构行为是如何推动行情演绎的?有哪些相关图表说明? + +结果: + +- Router:`workflow_single_step` +- LLM calls:4 +- refs:51 +- elapsed:约 29s +- evidence:约 22960 chars +- wallet context:`TIGHT` + +观察: + +- `navigate` 选择了 `NAVIGATE`,并带 `FIND_IMAGES`、`FIND_TABLES`,方向正确。 +- 但 root scope 的 asset tool 拉入了过多全局资源;同时 discovery 又选中父级 `1、2016:机构行为助推行情演绎`,导致 hydration 范围扩大。 +- evidence 里实际有图2、图3、图4、图5等图题和图片描述,但模型回答中仍说“未提供图表具体标题/编号”。 +- refs 达到 51,包含不少 2018、2019、2023、2024 等非目标年份资源。 +- 2016 相关图片在引用元数据中仍有 `section=Root` 的情况,虽然它们通过 `connect_to` 在 evidence 中被放回了具体 section。 + +判断: + +这是窄 section + 图表问题的典型失败形态:导航方向正确,但工具作用域过宽、discovery 过宽、证据渲染噪声大,导致模型虽然拿到了图表,却没有稳定提取图题和归属。 + +### T3_Compare_Extra + +Query: + +> 对比2024年和2025年AI安全市场规模,并结合证据给出变化原因。 + +结果: + +- Router:`workflow_decomposed` +- LLM calls:27 +- refs:1 +- elapsed:约 38s +- plan:s1 查 2024 市场规模,s2 查 2025 市场规模,s3 查变化原因,s4 synthesize + +观察: + +- 外层 workflow 确认可以并发执行多个 retrieve step,三个 retrieve step 的 KG select 和 navigate 调用是交错发生的。 +- 三个 retrieve step 最终都进入 revision,然后以 `budget_stop` 结束。 +- 总体 wallet 仍有大量剩余,但 bootstrap/revision 局部预算先被耗尽,最终对用户呈现为“预算停止”。 +- 实际语义更接近:KB 中缺少可支撑 2024/2025 AI 安全市场规模对比的证据。 +- `discovery_auto` 因年份词匹配,把债券研报等弱相关文档带入候选,造成预算消耗和路径污染。 + +判断: + +这是预算状态和无证据状态混淆。对用户来说,“知识库没有足够证据”和“预算不够”是两类完全不同的反馈;当前状态分类会误导用户,也会误导 harness 判断。 + +## 核心问题清单 + +### 1. 图表资源归属在 citation 层丢失 + +涉及核心设计: + +- 每个独立图表/图片/表格都应通过 `connect_to` 找到底层 section 归属。 +- 物理资源 chunk 的 `path` 可能是 `images/...` 或 `tables/...`,甚至 DB section 可能挂在 `Root`。 +- 逻辑归属应以 text chunk 的 `metadata.connect_to[].target` 为准,`target` 指向 image/table chunk_id。 + +当前表现: + +- evidence tree 渲染阶段多数情况下能用 owner path 把资源挂回 leaf section。 +- 但最终 `referenced_chunks`/citation 仍可能使用资源 chunk 自身的 `section_path`,因此显示 `Root`。 + +影响: + +- 用户看到图表出处为 `Root`,无法判断它属于哪个章节。 +- 对图表比较、章节归因、报告复核非常不友好。 +- 这和“每个独立图表都有 `connect_to` 找到一个底层 section 归属”的设计要求冲突。 + +建议: + +- citation/ref 组装时优先使用 `owner_section_path`,只有不存在时才回退到物理 `section_path`。 +- 返回结构中建议同时保留: + - `owner_section_path`:逻辑归属,用于用户展示和排序。 + - `physical_section_path`:数据库/资源物理挂载位置,用于调试。 + - `connect_to_source_chunk_id`:是哪一个 text chunk 证明了该资源归属。 +- 对 image/table 引用增加断言:若存在 `connect_to` owner,则展示 section 不应为 `Root`。 + +### 2. discovery merge 对 STOP 和 outline 查询缺少门控 + +当前表现: + +- T1 root outline 查询已经由 `navigate` 正确 `STOP`。 +- 后续 `discovery_select` 仍并入深层路径和资源。 + +影响: + +- 简单结构问题证据膨胀。 +- 引用混入深层内容,用户会怀疑答案是不是依据了错误章节。 +- 预算被无谓消耗。 + +建议: + +- 对 outline/structure/catalogue 类意图设置 discovery gate: + - 若 root STOP 且问题不要求“细节/图表/数据”,跳过 discovery hydration。 + - 或只允许 discovery 返回 top-level structural sections,不 hydrate leaf content/assets。 +- `discovery_select` 的 prompt 应明确区分: + - structure query:只补结构遗漏。 + - evidence query:可补 leaf 内容。 + - asset query:可补 image/table。 + +### 3. 图表工具作用域过宽 + +当前表现: + +- T2 中 `NAVIGATE + FIND_IMAGES/FIND_TABLES` 方向正确,但 root 或父级 scope asset extraction 拉入大量非目标年份图表。 +- 后续 trimming 虽然会删一部分,但已经消耗 context 和模型注意力。 + +影响: + +- 窄问题变成大范围 evidence dump。 +- 模型可能拿到正确图题却没有稳定使用,反而回答“没有具体标题”。 +- refs 过多,前端引用列表不可读。 + +建议: + +- 当 action 为 `NAVIGATE` 且有 selected leaf paths 时,asset tools 默认只对 selected paths 或其 owner-linked assets 生效。 +- 只有 action 为 root `STOP` 且 query 明确要求“列出全部图表/图片/表格”时,才允许文档级全量 asset pull。 +- 对 `FIND_IMAGES/FIND_TABLES` 的输出增加 owner filter:资源必须能通过 `connect_to` 归属到当前 selected subtree。 + +### 4. 预算分配与状态管理需要区分技术预算和语义失败 + +当前表现: + +- T3 三个 retrieve step 最终都是 `budget_stop`。 +- 但总 wallet 明显还有剩余,真正失败原因是没有足够证据。 +- bootstrap/revision 局部预算耗尽被升级成 step 级 budget stop。 + +影响: + +- 用户会以为“系统钱/上下文不够”,而不是“知识库无证据”。 +- harness 也难以判断是预算策略问题、检索召回问题还是 KB 数据缺失。 + +建议: + +- step status 拆分: + - `not_found_no_evidence` + - `not_found_low_confidence` + - `budget_exhausted_bootstrap` + - `budget_exhausted_context` + - `budget_exhausted_total` +- synthesize 时保留每个 retrieve step 的 semantic reason,不要只看 stop_reason 字符串。 +- revision loop 中,如果第一轮和第二轮文档选择高度重复且 verdict 是“KB 缺证据”,应提前停止,避免继续烧 bootstrap。 +- `BudgetWallet` 的 reclaimed budget 如果暂不重分配,snapshot 文案应避免暗示这些预算已重新可用。 + +### 5. Planner 缺少 KB inventory,导致 plan reasoning 误报 + +当前表现: + +- T4 中 planner reasoning 出现 “knowledge base is empty”。 +- 实际 trace 中 KB 并不为空。 + +判断: + +`QueryPlanner.plan()` 支持 `kb_total_docs/kb_total_chunks` 参数,但 workflow 调用路径没有传入真实 inventory,默认值为 0。 + +影响: + +- plan reasoning 不可信。 +- 对调试和用户解释都很危险。 + +建议: + +- `_load_or_plan()` 前读取当前 namespace 的 KB inventory,并传给 planner。 +- workflow plan cache key 应包含 KB version 或文档集合 fingerprint,否则 KB 更新后可能复用旧 plan。 + +### 6. `attempt_answer` JSON 解析不稳健 + +当前表现: + +- T4 中 `attempt_answer` 返回 JSON-like 内容,但因 raw newline 或不合规转义导致 parse 失败。 +- parse 失败后逻辑把原始字符串当作 DONE answer。 + +影响: + +- 单步用户可能看到 `{"status":"DONE","answer":...}` wrapper。 +- synth step 可能能“洗掉”问题,但 single-step 场景会暴露。 + +建议: + +- 增加 tolerant JSON repair,只修复回答字段中的裸换行/控制字符。 +- 如果解析失败且文本明显以 JSON object 开头,不应直接 `DONE raw`,而应降级重试或抽取 `answer` 字段。 + +### 7. trace DB schema 与 ORM 不一致 + +当前表现: + +- `retrieval_runs.parent_run_id/workflow_step_id/workflow_plan` 在 ORM 中存在。 +- alembic migration 中未创建这些列。 +- trace create_run 报 `UndefinedColumnError`。 + +影响: + +- DB trace 不可用。 +- harness 只能依赖 Markdown trace,无法做结构化聚合和回归分析。 + +建议: + +- 补 migration。 +- 增加一个轻量 schema contract test,覆盖 `RetrievalTraceRecorder.create_run()`。 + +### 8. 多文件并发导航的现状 + +已确认: + +- 外层 workflow retrieve steps 使用 topological batch 并发执行。 +- T3 中多个 retrieve step 的 KG select/navigate 调用交错,说明并发有效。 + +风险: + +- 单个 retrieve step 内 selected docs 仍偏串行。 +- `discovery_auto` 追加的弱相关文档没有足够 domain guard,T3 因年份匹配引入了债券研报。 + +建议: + +- 对 `discovery_auto` 文档追加设置最低 domain relevance: + - 文档 title/summary/keywords 至少命中主题实体。 + - 或要求 bottom chunk 与 query 的非时间词、非通用词有足够 overlap。 +- 单 step 多 doc 可考虑并发,但要先修好 doc relevance guard,否则并发只会更快地放大噪声。 + +## 遗留与冗余代码观察 + +### Legacy retrieval 路径仍和 agentic 路径混杂 + +`run_retrieval_query()` 中同时存在 agentic workflow 和 legacy 3-channel RRF 排序/graph routing。若 agentic 已是主路径,建议把 legacy 路径隔离为明确 fallback,避免后续改动时误改两套逻辑。 + +### 旧 graph/discovery helper 有疑似未使用分支 + +`agentic/orchestrator.py` 附近存在 `_grep_discover_document_ids`、`_expand_by_edges` 等老式发现逻辑痕迹。若主流程已经切到 bottom discovery + KG select,应确认这些 helper 是否仍被调用;未调用则标记删除或迁移到测试辅助。 + +### path dedup 当前依赖“一叶一文本 chunk”隐含前提 + +当前 `_hydrate_paths_to_rows` 用 path-level `seen_paths` 是安全的,因为解析模型近似保持“一 leaf section 一个 text chunk”。但如果未来 parser 把一个 leaf section 拆成多个 text chunks,path-level dedup 会丢内容。 + +建议: + +- 在注释和测试中写明该前提。 +- 或把 dedup key 改成 `(document_id, section_path, chunk_id)`,再在 render 层控制同 section 合并。 + +## 建议优先级 + +P0: + +1. 修复 image/table citation 归属:优先展示 `connect_to` owner section,不再把有 owner 的图表显示成 `Root`。 +2. 修复 trace DB migration,恢复 harness 结构化观测。 +3. 修复 `attempt_answer` JSON parse fallback,避免把 wrapper 暴露给用户。 + +P1: + +1. 对 root STOP/outline query 增加 discovery gate。 +2. 收紧 asset tool 作用域:`NAVIGATE + selected paths` 时只找 selected subtree 的 connected assets。 +3. 拆分 `budget_stop` 与 `not_found` 状态,synthesize 阶段保留真实失败原因。 + +P2: + +1. 给 planner 传真实 KB inventory,并把 KB fingerprint 纳入 plan cache key。 +2. 给 `discovery_auto` 增加 domain relevance guard。 +3. 清理 legacy helper 和未使用 discovery/graph 分支。 +4. 为 path dedup 增加未来多 chunk leaf 的保护测试。 + +## 建议回归用例 + +1. Outline STOP 不应 hydrate 深层 leaf: + - Query:`民生证券这份利率专题研报的整体结构是什么?包含哪些主要章节?` + - 断言:refs 中不应出现大量 image/table;深层 section 不应被 discovery 自动并入。 + +2. 2016 section 图表归属: + - Query:`2016年债市走牛的几个阶段中,机构行为是如何推动行情演绎的?有哪些相关图表说明?` + - 断言:所有相关 image/table citation 的展示 section 应为 2016 底层 section,而不是 `Root`。 + +3. 全量图表查询: + - Query:`列出AI安全大模型报告中所有的图表和图片,并简要描述每张图的内容。` + - 断言:允许 root/global asset pull,但每个独立图表仍应有 owner section;确实无底层 owner 的封面/前言图要显式标记为 document-level。 + +4. KB 无证据查询: + - Query:`对比2024年和2025年AI安全市场规模,并结合证据给出变化原因。` + - 断言:返回状态应是 no evidence / insufficient evidence,而不是 generic `budget_stop`。 + +5. Planner inventory: + - 构造非空 KB。 + - 断言 planner reasoning 不得出现 “knowledge base is empty”。 + +## 代码落点索引 + +- Workflow orchestration:`packages/shared-python/shared/services/retrieval/workflow/orchestrator.py` +- Planner:`packages/shared-python/shared/services/retrieval/workflow/planner.py` +- Workflow budget wallet:`packages/shared-python/shared/services/retrieval/workflow/wallet.py` +- Inner agent orchestrator:`packages/shared-python/shared/services/retrieval/agentic/orchestrator.py` +- Inner agent tools:`packages/shared-python/shared/services/retrieval/agentic/tools.py` +- Answer policy / JSON parse:`packages/shared-python/shared/services/retrieval/agentic/policy.py` +- Navigation tree render:`packages/shared-python/shared/services/retrieval/agent_navigate.py` +- Retrieval entry / hydration:`packages/shared-python/shared/services/retrieval/app_service.py` +- Retrieval trace:`packages/shared-python/shared/services/retrieval/agentic/trace.py` +- ORM retrieval tables:`packages/shared-python/shared/models/database/document.py` +- Migration:`apps/api/alembic/versions/e5f6a7b8c9d0_add_agentic_retrieval_tables.py` +- Debug harness:`apps/worker/debug_agentic_e2e.py` + diff --git a/docs/external-services.md b/docs/external-services.md deleted file mode 100644 index 32ecb405f..000000000 --- a/docs/external-services.md +++ /dev/null @@ -1,51 +0,0 @@ -# External Service Dependencies - -Knowhere API is the backend for . Public product -documentation can also link to when deeper setup -references are helpful. - -## Required For Local Startup - -Needs these dependencies to run the backend -surface locally: - -- PostgreSQL for the application database -- Redis for Celery and short-lived state -- S3-compatible storage for uploads and result assets -- one OpenAI-compatible LLM provider key for retrieval and parsing flows - -The repo-managed `deploy/local-dev` stack provides PostgreSQL, Redis, and -LocalStack so the default `env.example` files can use a coherent local baseline. - -## Required Only For Specific Features - -- MinerU: - required only if you want MinerU-backed document parsing flows -- iLoveAPI: - required only for conversion paths such as PPTX-to-PDF -- QStash: - required only if you want queued outbound webhook delivery -- Stripe: - required only if you enable billing and checkout flows -- OAuth provider credentials: - required only if you run dashboard-linked auth flows - -## Optional Observability And Analytics - -- Logfire for distributed tracing export - -These integrations are intentionally optional. Leaving them empty should not -block a local backend bootstrap. - -## Minimum Viable Local Configuration - -The smallest supported local setup is: - -1. copy `apps/api/env.example` and `apps/worker/env.example` -2. keep the default local PostgreSQL, Redis, and LocalStack values -3. add one real LLM provider key such as `DS_KEY` -4. run `deploy/local-dev/start-dev.sh` -5. start the API and worker with `uv run` - -That path is the baseline public developer workflow. Additional providers should -only be configured when you need the matching feature set. diff --git a/docs/testing-guidance.md b/docs/testing-guidance.md deleted file mode 100644 index 8d6df6b49..000000000 --- a/docs/testing-guidance.md +++ /dev/null @@ -1,82 +0,0 @@ -# Testing Guidance - -## Goal - -Keep the main test suites focused on the project surface rather than internal implementation details. - -The important things to verify are: - -- HTTP contract: request shape, response shape, status codes, and headers -- Observable side effects: database writes, database updates, Redis state, and queued work -- Runtime guarantees: auth behavior, rate limiting, conflict handling, and validation handling -- Migration and persistence correctness: schema, constraints, and SQL-backed behavior - -The important things to avoid are: - -- Internal function boundary assertions -- Repository or service call-sequence assertions -- Mock-heavy tests that change the behavior under test in a material way - -## Test Taxonomy - -- `apps/api/tests/contract` - API endpoint and surface specifications -- `apps/api/tests/support` - app bootstrap, test environment, database reset, Redis reset, and seed helpers -- `apps/api/tests/migrations` - Alembic and schema guarantees -- `apps/worker/tests/contract` - worker entrypoint, queued-work boundary, and durable side-effect specifications -- `apps/api/tests` and `apps/worker/tests` - narrow app-level component tests only when they protect pure logic or deterministic edge-case parsing - -Do not add a standalone shared-package test tree for behavior that belongs to the API or worker surface. - -## Contract Test Rules - -- A contract test must call the project surface, not an internal helper. -- A contract test must assert an externally visible result or durable side effect. -- API contract tests should use the real FastAPI lifespan. -- Contract tests should use real PostgreSQL where SQL behavior depends on it. -- API and worker contract tests use `fakeredis` for Redis behavior while keeping the same Redis service interfaces. -- Mock only hard-to-control external boundaries such as third-party HTTP, storage providers, time, or filesystem edges. - -## Naming Rules - -- File names should follow the surface area being specified. -- Contract test functions should prefer `test_should_`. -- Test names should describe user-visible behavior, not implementation details. - -## Fixture Boundaries - -- `apps/api/tests/support` owns API bootstrap, environment setup, lifespan control, and seed data. -- API contract tests should not override core dependencies such as auth, database access, or rate limiting for the behavior under test. -- Worker contract tests own worker task entrypoints, queued-work boundaries, and durable task outcomes. - -## Coverage Expectations - -- API contract coverage should track every mounted router group in `apps/api/app/api/v1/api_v1.py`. -- Worker contract coverage should track every registered Celery task in `apps/worker/app/core/tasks`. -- When a stronger contract test replaces an old mock-heavy test, remove the weaker test or reduce it to a narrow component test. - -## Local Environment - -- API and worker contract tests require PostgreSQL server binaries and contrib extensions for `pytest-postgresql`. -- API and worker contract tests do not require a running local PostgreSQL or Redis service. -- API and worker contract tests use isolated `pytest-postgresql` processes and `fakeredis`. - -## Commands - -- `uv run python apps/api/scripts/ensure_test_environment.py --install` -- `uv run python apps/api/scripts/ensure_test_environment.py` -- `uv run pytest apps/api/tests/contract -q` -- `uv run pytest apps/api/tests/migrations -q` -- `uv run pytest apps/api/tests -q` -- `uv run pytest apps/worker/tests/contract -q` -- `uv run pytest apps/api/tests apps/worker/tests/contract -q` - -## Failure Triage - -- Re-run the smallest affected suite first. -- Re-run from a clean `pytest-postgresql` process when the failure depends on database side effects. -- Prefer fixing the harness or production behavior over adding more mocks. diff --git a/packages/shared-python/shared/services/retrieval/app_service.py b/packages/shared-python/shared/services/retrieval/app_service.py index dc14c055a..42166ea28 100644 --- a/packages/shared-python/shared/services/retrieval/app_service.py +++ b/packages/shared-python/shared/services/retrieval/app_service.py @@ -1100,7 +1100,7 @@ async def run_retrieval_query( return await _to_public_response(response) # ══ Route: agentic (unified workflow) vs legacy ══ - _agentic_enabled = os.environ.get('RETRIEVAL_AGENTIC_ENABLED', 'false') == 'true' + _agentic_enabled = os.environ.get('RETRIEVAL_AGENTIC_ENABLED', 'true') == 'true' if _agentic_enabled: # ── Unified agentic path via WorkflowOrchestrator ── # Simple queries: planner returns a single-step plan (no decomposition). diff --git a/packages/shared-python/shared/tests/test_workflow_orchestrator.py b/packages/shared-python/shared/tests/test_workflow_orchestrator.py deleted file mode 100644 index 9189558a7..000000000 --- a/packages/shared-python/shared/tests/test_workflow_orchestrator.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -import os - -os.environ.setdefault("DS_KEY", "test") -os.environ.setdefault("DS_URL", "https://example.com") -os.environ.setdefault("S3_BUCKET_NAME", "test") -os.environ.setdefault("S3_ACCESS_KEY_ID", "test") -os.environ.setdefault("S3_SECRET_ACCESS_KEY", "test") -os.environ.setdefault("S3_TEMP_PATH", "/tmp") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://root:root123@localhost:5432/Knowhere") -os.environ.setdefault("TMP_PATH", "/tmp") - -from shared.services.retrieval.workflow.synthesizer import compose_final_answer -from shared.services.retrieval.workflow.types import PlannedStep, QueryPlan, StepResult - - -def test_compose_final_answer_concat_final_parts(): - plan = QueryPlan( - original_query="q", - steps=[ - PlannedStep(id="s1", sub_query="q1", output_role="final_part"), - PlannedStep(id="s2", sub_query="q2", output_role="intermediate"), - PlannedStep(id="s3", sub_query="q3", output_role="final_part"), - ], - final_strategy="concat_final_parts", - ) - results = { - "s1": StepResult("s1", "q1", "retrieve", [], "final_part", answer_text="A1"), - "s2": StepResult("s2", "q2", "retrieve", [], "intermediate", answer_text="A2"), - "s3": StepResult("s3", "q3", "retrieve", [], "final_part", answer_text="A3"), - } - - assert compose_final_answer(plan, results) == "A1\n\nA3" - - -def test_compose_final_answer_last_synthesize(): - plan = QueryPlan( - original_query="q", - steps=[ - PlannedStep(id="s1", sub_query="q1", output_role="consumed_by_synthesis"), - PlannedStep(id="s2", sub_query="q2", step_kind="synthesize", depends_on=["s1"], output_role="final_part"), - ], - final_strategy="last_synthesize", - ) - results = { - "s1": StepResult("s1", "q1", "retrieve", [], "consumed_by_synthesis", answer_text="A1"), - "s2": StepResult("s2", "q2", "synthesize", ["s1"], "final_part", answer_text="FINAL"), - } - - assert compose_final_answer(plan, results) == "FINAL" - - -def test_query_plan_topological_batches(): - plan = QueryPlan( - original_query="q", - steps=[ - PlannedStep(id="s1", sub_query="q1"), - PlannedStep(id="s2", sub_query="q2"), - PlannedStep(id="s3", sub_query="q3", step_kind="synthesize", depends_on=["s1", "s2"]), - ], - ) - - batches = plan.topological_batches() - - assert [step.id for step in batches[0]] == ["s1", "s2"] - assert [step.id for step in batches[1]] == ["s3"] diff --git a/packages/shared-python/shared/tests/test_workflow_planner.py b/packages/shared-python/shared/tests/test_workflow_planner.py deleted file mode 100644 index 0f1aaae6e..000000000 --- a/packages/shared-python/shared/tests/test_workflow_planner.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -import os - -os.environ.setdefault("DS_KEY", "test") -os.environ.setdefault("DS_URL", "https://example.com") -os.environ.setdefault("S3_BUCKET_NAME", "test") -os.environ.setdefault("S3_ACCESS_KEY_ID", "test") -os.environ.setdefault("S3_SECRET_ACCESS_KEY", "test") -os.environ.setdefault("S3_TEMP_PATH", "/tmp") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://root:root123@localhost:5432/Knowhere") -os.environ.setdefault("TMP_PATH", "/tmp") - -import pytest - -from shared.services.retrieval.agentic.budget import BudgetLedger -from shared.services.retrieval.workflow.planner import QueryPlanner - - -@pytest.mark.asyncio -async def test_planner_parses_multi_step_plan(): - async def llm(_prompt): - return """ - { - "reasoning_summary": "compare two years then synthesize", - "steps": [ - {"id": "s1", "sub_query": "2024 market size", "step_kind": "retrieve", "depends_on": [], "output_role": "consumed_by_synthesis"}, - {"id": "s2", "sub_query": "2025 market size", "step_kind": "retrieve", "depends_on": [], "output_role": "consumed_by_synthesis"}, - {"id": "s3", "sub_query": "compare s1 and s2", "step_kind": "synthesize", "depends_on": ["s1", "s2"], "output_role": "final_part"} - ], - "final_strategy": "last_synthesize" - } - """ - - planner = QueryPlanner( - llm_fn=llm, - planner_ledger=BudgetLedger(total=4000, planning_ratio=0.0, bootstrap=4000), - max_steps=5, - total_budget=200000, - per_step_budget=40000, - ) - plan = await planner.plan(query="compare 2024 and 2025") - - assert len(plan.steps) == 3 - assert plan.steps[2].depends_on == ["s1", "s2"] - assert plan.final_strategy == "last_synthesize" - - -@pytest.mark.asyncio -async def test_planner_falls_back_to_single_step_on_bad_json(): - async def llm(_prompt): - return "not json" - - planner = QueryPlanner( - llm_fn=llm, - planner_ledger=None, - max_steps=5, - total_budget=200000, - per_step_budget=40000, - ) - plan = await planner.plan(query="hello") - - assert plan.planner_status == "fallback" - assert len(plan.steps) == 1 - assert plan.steps[0].sub_query == "hello" - - -@pytest.mark.asyncio -async def test_planner_falls_back_when_too_many_steps(): - async def llm(_prompt): - return { - "steps": [ - {"id": f"s{i}", "sub_query": f"q{i}", "step_kind": "retrieve", "depends_on": [], "output_role": "final_part"} - for i in range(1, 7) - ], - "final_strategy": "concat_final_parts", - }.__repr__().replace("'", '"') - - planner = QueryPlanner( - llm_fn=llm, - planner_ledger=None, - max_steps=3, - total_budget=200000, - per_step_budget=40000, - ) - plan = await planner.plan(query="too broad") - - assert plan.planner_status == "fallback" - assert len(plan.steps) == 1 diff --git a/packages/shared-python/shared/tests/test_workflow_wallet.py b/packages/shared-python/shared/tests/test_workflow_wallet.py deleted file mode 100644 index 5d9f2d252..000000000 --- a/packages/shared-python/shared/tests/test_workflow_wallet.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -import os - -os.environ.setdefault("DS_KEY", "test") -os.environ.setdefault("DS_URL", "https://example.com") -os.environ.setdefault("S3_BUCKET_NAME", "test") -os.environ.setdefault("S3_ACCESS_KEY_ID", "test") -os.environ.setdefault("S3_SECRET_ACCESS_KEY", "test") -os.environ.setdefault("S3_TEMP_PATH", "/tmp") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://root:root123@localhost:5432/Knowhere") -os.environ.setdefault("TMP_PATH", "/tmp") - -import pytest - -from shared.services.retrieval.workflow.types import PlannedStep, QueryPlan -from shared.services.retrieval.workflow.wallet import BudgetWallet - - -@pytest.mark.asyncio -async def test_wallet_allocates_ledgers_per_step(): - plan = QueryPlan( - original_query="q", - steps=[ - PlannedStep(id="s1", sub_query="q1", step_kind="retrieve"), - PlannedStep(id="s2", sub_query="q2", step_kind="synthesize", depends_on=["s1"]), - ], - ) - wallet = BudgetWallet( - total=50000, - per_retrieve_step_default=40000, - per_synthesize_step_default=6000, - ) - - ledgers = await wallet.allocate(plan) - - assert set(ledgers) == {"s1", "s2"} - assert wallet.snapshot()["allocated"] == 46000 - - -@pytest.mark.asyncio -async def test_wallet_scales_down_under_total_cap(): - plan = QueryPlan( - original_query="q", - steps=[ - PlannedStep(id="s1", sub_query="q1", step_kind="retrieve"), - PlannedStep(id="s2", sub_query="q2", step_kind="retrieve"), - PlannedStep(id="s3", sub_query="q3", step_kind="synthesize", depends_on=["s1", "s2"]), - ], - ) - wallet = BudgetWallet( - total=30000, - per_retrieve_step_default=40000, - per_synthesize_step_default=6000, - ) - - await wallet.allocate(plan) - snapshot = wallet.snapshot() - - assert snapshot["allocated"] <= 30000 - assert snapshot["allocations"]["s1"] >= 4000 - assert snapshot["allocations"]["s3"] >= 1500 - - -@pytest.mark.asyncio -async def test_wallet_reclaim_records_unused_capacity(): - plan = QueryPlan( - original_query="q", - steps=[PlannedStep(id="s1", sub_query="q1", step_kind="retrieve")], - ) - wallet = BudgetWallet( - total=40000, - per_retrieve_step_default=40000, - per_synthesize_step_default=6000, - ) - ledgers = await wallet.allocate(plan) - - await wallet.reclaim("s1", ledgers["s1"]) - - assert wallet.snapshot()["reclaimed"]["s1"] == 40000