[NOTICKET][AI] feat(chat): wire Orchestrator -> slow path at the endpoint (env-gated)
Browse filesThe slow path (Planner -> TaskRunner -> Assembler) was reachable in ChatHandler but
never enabled from the live endpoint. Wire it so the team can test end-to-end from
/chat/stream, behind an env flag so it stays opt-in until the lead's real
BusinessContext lands (it's still a stub).
- settings.enable_slow_path (env ENABLE_SLOW_PATH, default False).
- api/v1/chat.py passes it to the shared ChatHandler.
- ENABLE_SLOW_PATH=true → structured intents route through the slow path and stream
status progress + the assembled answer; default keeps the single-query QueryService
path. Fast/unstructured paths unchanged.
Verified: toggle flips handler._enable_slow_path; wiring tests pass; live run through
ChatHandler.handle confirmed earlier this session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PROGRESS.md +8 -0
- src/api/v1/chat.py +6 -1
- src/config/settings.py +8 -0
|
@@ -56,6 +56,14 @@ Verified against code before logging. Severity: **critical** / important / nice-
|
|
| 56 |
| T1 | **`input_schema` is presence-only, not type-checked** — `ToolSpec.input_schema` comment said "validates ToolCall.args", but `TaskRunner._validate_args` only enforces `required` presence; the `properties` types are documentation, never validated at runtime. Clarified the contract in `tools/contracts.py` so nobody assumes type-safety (a wrong-typed arg passes validation, surfaces only inside the compute fn). Doc-only, no behavior change (90e80f9). | nice-to-have | TAB | `[x]` |
|
| 57 |
| T2 | **Dead Python embed path?** — `document_pipeline.process()` → `knowledge_processor` → `vector_store.aadd_documents()` still writes PDF/DOCX/TXT embeddings to `langchain_pg_embedding`, contradicting CLAUDE.md's "Go is sole writer, Python reads only". Verified the Go service (`Orchestrator-Agent-Service/internal/documents`) IS a complete ingestion writer to the same tables for all 5 file types (OCR + chunk + embed) → the Python embed branch is very likely redundant. **Blocked on one operational fact:** does the frontend still upload to `/document/process` (Python) or to Go? Park until confirmed — deleting a live ingestion path would break unstructured RAG. The csv/xlsx parquet branch stays regardless (feeds the catalog/tabular path). | nice-to-have | TAB | `[blocked]` |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
**Architecture verdict:** fundamentally sound (catalog-driven IR + deterministic compiler
|
| 60 |
+ static plan is the right call). Debt is transitional duplication (two planners/registries/
|
| 61 |
contract modules — documented, owned) and `ChatHandler` drifting toward a god object
|
|
|
|
| 56 |
| T1 | **`input_schema` is presence-only, not type-checked** — `ToolSpec.input_schema` comment said "validates ToolCall.args", but `TaskRunner._validate_args` only enforces `required` presence; the `properties` types are documentation, never validated at runtime. Clarified the contract in `tools/contracts.py` so nobody assumes type-safety (a wrong-typed arg passes validation, surfaces only inside the compute fn). Doc-only, no behavior change (90e80f9). | nice-to-have | TAB | `[x]` |
|
| 57 |
| T2 | **Dead Python embed path?** — `document_pipeline.process()` → `knowledge_processor` → `vector_store.aadd_documents()` still writes PDF/DOCX/TXT embeddings to `langchain_pg_embedding`, contradicting CLAUDE.md's "Go is sole writer, Python reads only". Verified the Go service (`Orchestrator-Agent-Service/internal/documents`) IS a complete ingestion writer to the same tables for all 5 file types (OCR + chunk + embed) → the Python embed branch is very likely redundant. **Blocked on one operational fact:** does the frontend still upload to `/document/process` (Python) or to Go? Park until confirmed — deleting a live ingestion path would break unstructured RAG. The csv/xlsx parquet branch stays regardless (feeds the catalog/tabular path). | nice-to-have | TAB | `[blocked]` |
|
| 58 |
|
| 59 |
+
**Slow-path endpoint wiring (2026-06-10):** the Orchestrator→slow-path is now wired
|
| 60 |
+
into the live endpoint behind an **env flag**. `settings.enable_slow_path` (env
|
| 61 |
+
`ENABLE_SLOW_PATH`, default **off**) is passed to the shared `ChatHandler` in
|
| 62 |
+
`api/v1/chat.py`. Flip `ENABLE_SLOW_PATH=true` to route `structured` intents through
|
| 63 |
+
Planner→TaskRunner→Assembler and test end-to-end from `/chat/stream` (status progress
|
| 64 |
+
events + answer stream). Stays opt-in because `BusinessContext` is still the stub;
|
| 65 |
+
fast/unstructured paths unchanged. Verified live via `ChatHandler.handle`.
|
| 66 |
+
|
| 67 |
**Architecture verdict:** fundamentally sound (catalog-driven IR + deterministic compiler
|
| 68 |
+ static plan is the right call). Debt is transitional duplication (two planners/registries/
|
| 69 |
contract modules — documented, owned) and `ChatHandler` drifting toward a god object
|
|
@@ -26,7 +26,12 @@ router = APIRouter(prefix="/api/v1", tags=["Chat"])
|
|
| 26 |
# is passed into handle()), and lazily builds + caches the Orchestrator/Chatbot
|
| 27 |
# chains — so reusing it keeps the Azure OpenAI clients (and their httpx/TLS pools)
|
| 28 |
# warm across requests instead of re-handshaking on the first call of every request.
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
_GREETINGS = frozenset(["hi", "hello", "hey", "halo", "hai", "hei"])
|
| 32 |
_GOODBYES = frozenset(["bye", "goodbye", "thanks", "thank you", "terima kasih", "sampai jumpa"])
|
|
|
|
| 26 |
# is passed into handle()), and lazily builds + caches the Orchestrator/Chatbot
|
| 27 |
# chains — so reusing it keeps the Azure OpenAI clients (and their httpx/TLS pools)
|
| 28 |
# warm across requests instead of re-handshaking on the first call of every request.
|
| 29 |
+
# enable_slow_path is env-gated (ENABLE_SLOW_PATH): when on, structured intents route
|
| 30 |
+
# Orchestrator -> Planner -> TaskRunner -> Assembler so the team can test e2e here.
|
| 31 |
+
_chat_handler = ChatHandler(
|
| 32 |
+
enable_tracing=True,
|
| 33 |
+
enable_slow_path=settings.enable_slow_path,
|
| 34 |
+
)
|
| 35 |
|
| 36 |
_GREETINGS = frozenset(["hi", "hello", "hey", "halo", "hai", "hei"])
|
| 37 |
_GOODBYES = frozenset(["bye", "goodbye", "thanks", "thank you", "terima kasih", "sampai jumpa"])
|
|
@@ -16,6 +16,14 @@ class Settings(BaseSettings):
|
|
| 16 |
case_sensitive=False,
|
| 17 |
)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Database
|
| 20 |
postgres_connstring: str
|
| 21 |
|
|
|
|
| 16 |
case_sensitive=False,
|
| 17 |
)
|
| 18 |
|
| 19 |
+
# Feature flags
|
| 20 |
+
# Route `structured` chat intents through the analytical SLOW PATH
|
| 21 |
+
# (Planner -> TaskRunner -> Assembler) instead of the single-query QueryService.
|
| 22 |
+
# Off by default; the team flips ENABLE_SLOW_PATH=true to test end-to-end from
|
| 23 |
+
# the /chat/stream endpoint. BusinessContext is still a stub until the lead's
|
| 24 |
+
# real source lands, so this stays opt-in.
|
| 25 |
+
enable_slow_path: bool = Field(alias="enable_slow_path", default=False)
|
| 26 |
+
|
| 27 |
# Database
|
| 28 |
postgres_connstring: str
|
| 29 |
|