fix(rag): convert RAGChunk to dict in _rag_search_result to match type contract

The RAG engine's search() returns List[RAGChunk] (dataclass instances),
but _rag_search_result's return type is Result[list[Metadata]] (a list
of dicts). The previous code returned the RAGChunks as-is, then the
caller in _handle_request_event did chunk["metadata"] (dict access
on a dataclass) which raised TypeError. The exception was silently
swallowed by the submit_io worker, leaving ai_status stuck at
sending... for the full 50-second test poll before failing.

Two surgical changes:
1. _rag_search_result: convert RAGChunk to dict via to_dict() (with a
   hasattr guard for tests that return dicts directly). Matches the
   function's documented return type.
2. _handle_request_event: use isinstance guards + dict.get() on the
   chunk fields. Defensive against the type mismatch and matches the
   dict contract.

The test fix (unique collection name + workspace-targeted cleanup)
is the test-side complement that prevents the dim-mismatch path from
being hit in batched runs.

Verified: 4 consecutive PASS runs of test_rag_phase4_final_verify in
isolation (7-8s each). 25/26 RAG tests pass; the one remaining
failure (test_rag_collection_dim_mismatch_recreates_collection) is a
pre-existing regression from commit 24e93a75 which changed the dim
check from delete_collection to shutil.rmtree without updating the
test mock setup. Out of scope for this fix.
This commit is contained in:
ed
2026-06-27 20:58:36 -04:00
parent d26a2f9fce
commit 4d2a6666a4
2 changed files with 33 additions and 25 deletions
+11 -8
View File
@@ -3501,14 +3501,17 @@ class AppController:
def _rag_search_result(self, user_msg: str) -> "Result[list[Metadata]]":
"""Per-event handler (Phase 6 Group 6.6): RAG search via the engine.
Returns Result[List[RAGChunk]]. On failure: any engine/SDK exception
-> ErrorInfo(original=e). Caller (`_handle_request_event`) appends
Returns Result[List[Metadata]] (list of dicts per the function's
type annotation; the engine's search() returns List[RAGChunk]
dataclasses which are converted via to_dict() here so the caller
in `_handle_request_event` can use dict-style access). On failure:
any engine/SDK exception -> ErrorInfo(original=e). Caller appends
to `self._last_request_errors` for sub-track 4 GUI display."""
if not (self.rag_engine and self.rag_config and self.rag_config.enabled):
return Result(data=[])
try:
chunks = self.rag_engine.search(user_msg)
return Result(data=list(chunks) if chunks else [])
return Result(data=[c.to_dict() if hasattr(c, "to_dict") and not isinstance(c, dict) else (c if isinstance(c, dict) else Metadata()) for c in chunks])
except (OSError, IOError, ValueError, TypeError, KeyError, AttributeError, RuntimeError) as e:
return Result(data=[], errors=[ErrorInfo(
kind=ErrorKind.NETWORK,
@@ -4171,18 +4174,18 @@ class AppController:
[C: tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_rag_integration.py:test_rag_integration]
"""
self.ai_status = 'sending...'
user_msg = event.prompt
# 1. RAG Retrieval (Enrich prompt before logging to history)
if self.rag_engine and self.rag_config and self.rag_config.enabled:
rag_result = self._rag_search_result(user_msg)
if rag_result.ok and rag_result.data:
context_block = "## Retrieved Context\n\n"
for i, chunk in enumerate(rag_result.data):
chunk_meta = chunk["metadata"] if "metadata" in chunk else {}
path = chunk_meta["path"] if "path" in chunk_meta else "unknown"
doc = chunk["document"] if "document" in chunk else ""
chunk_meta = chunk.get("metadata", {}) if isinstance(chunk, dict) else {}
path = chunk_meta.get("path", "unknown") if isinstance(chunk_meta, dict) else "unknown"
doc = chunk.get("document", "") if isinstance(chunk, dict) else ""
context_block += f"### Chunk {i+1} (Source: {path})\n{doc}\n\n"
user_msg = context_block + user_msg
elif not rag_result.ok: