Wire up query history: localStorage session persistence + sidebar UI
This commit is contained in:
@@ -20,6 +20,7 @@ class RetrievalTrace:
|
||||
citations: tuple[dict[str, Any], ...]
|
||||
correlation_id: str | None = None
|
||||
otel_trace_id: str | None = None
|
||||
conversation_id: str | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
@@ -63,6 +64,7 @@ class PostgresTraceRepository:
|
||||
citations: tuple[dict[str, Any], ...],
|
||||
correlation_id: str | None = None,
|
||||
otel_trace_id: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
) -> str:
|
||||
import psycopg
|
||||
|
||||
@@ -73,13 +75,13 @@ class PostgresTraceRepository:
|
||||
INSERT INTO rag_retrieval_trace (
|
||||
trace_id, query_text, subject_scope, query_intent,
|
||||
decision, reason, resolved_drug_id, citations,
|
||||
correlation_id, otel_trace_id
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s)
|
||||
correlation_id, otel_trace_id, conversation_id
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
trace_id, query, subject_scope, intent, decision, reason,
|
||||
resolved_drug_id, json.dumps(citations, ensure_ascii=False),
|
||||
correlation_id, otel_trace_id,
|
||||
correlation_id, otel_trace_id, conversation_id,
|
||||
),
|
||||
)
|
||||
return trace_id
|
||||
@@ -92,7 +94,7 @@ class PostgresTraceRepository:
|
||||
"""
|
||||
SELECT trace_id::text, query_text, subject_scope, query_intent,
|
||||
decision, reason, resolved_drug_id, citations,
|
||||
correlation_id, otel_trace_id, created_at
|
||||
correlation_id, otel_trace_id, conversation_id, created_at
|
||||
FROM rag_retrieval_trace WHERE trace_id = %s
|
||||
""",
|
||||
(trace_id,),
|
||||
@@ -103,9 +105,46 @@ class PostgresTraceRepository:
|
||||
trace_id=row[0], query=row[1], subject_scope=row[2], intent=row[3],
|
||||
decision=row[4], reason=row[5], resolved_drug_id=row[6],
|
||||
citations=tuple(row[7]), correlation_id=row[8], otel_trace_id=row[9],
|
||||
created_at=row[10],
|
||||
conversation_id=row[10], created_at=row[11],
|
||||
)
|
||||
|
||||
def list_by_conversation(
|
||||
self, conversation_id: str, limit: int
|
||||
) -> list[RetrievalTrace]:
|
||||
"""Past queries for one session — Feature-List #25 (lịch sử tra
|
||||
cứu), most recent first. Scoped to `conversation_id` on purpose:
|
||||
this system has no auth anywhere (`apps/api-gateway`/`auth-service`
|
||||
are unbuilt — see README), so an unscoped listing would mix every
|
||||
browser's/user's queries together. Citations/answer text are NOT
|
||||
persisted here (only decision/reason/resolved_drug_id) — a history
|
||||
entry is for re-running the same query, not replaying its old
|
||||
answer verbatim.
|
||||
"""
|
||||
import psycopg
|
||||
|
||||
with psycopg.connect(self._dsn, connect_timeout=5) as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT trace_id::text, query_text, subject_scope, query_intent,
|
||||
decision, reason, resolved_drug_id, citations,
|
||||
correlation_id, otel_trace_id, conversation_id, created_at
|
||||
FROM rag_retrieval_trace
|
||||
WHERE conversation_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(conversation_id, limit),
|
||||
).fetchall()
|
||||
return [
|
||||
RetrievalTrace(
|
||||
trace_id=row[0], query=row[1], subject_scope=row[2], intent=row[3],
|
||||
decision=row[4], reason=row[5], resolved_drug_id=row[6],
|
||||
citations=tuple(row[7]), correlation_id=row[8], otel_trace_id=row[9],
|
||||
conversation_id=row[10], created_at=row[11],
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def save_feedback(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -359,6 +359,52 @@ class QdrantRetriever:
|
||||
hits.append(SearchHit(_document(payload), 1.0))
|
||||
return hits
|
||||
|
||||
def list_sections(self, drug_id: str) -> list[tuple[str, str]]:
|
||||
"""Every section this drug has ANY content for — prose or
|
||||
quarantined — as `(section_key, section_display_name)` pairs, in
|
||||
book order (`rag.sections.SECTION_ORDER`). Feature-List #4: the UI
|
||||
needs the real per-drug checklist, not a generic 19-item list, since
|
||||
coverage genuinely varies (confirmed corpus-wide: 7 to 19 sections
|
||||
per drug).
|
||||
|
||||
Deliberately NOT `find_by_drug`'s `chunk_kind == "prose"` filter — a
|
||||
section that exists ONLY as a quarantined table (no prose chunk at
|
||||
all) is still a real section of this monograph; the caller decides
|
||||
how to present a request for it (`find_by_section` already handles
|
||||
the quarantine notice). A projection scroll: only the two payload
|
||||
fields this needs, never `text` — the checklist has no reason to
|
||||
pull every chunk's full content over the wire.
|
||||
"""
|
||||
from qdrant_client.models import FieldCondition, Filter, MatchValue
|
||||
|
||||
from rag.sections import SECTION_ORDER
|
||||
|
||||
scroll_filter = Filter(
|
||||
must=[FieldCondition(key="drug_id", match=MatchValue(value=drug_id))]
|
||||
)
|
||||
found: dict[str, str] = {}
|
||||
offset = None
|
||||
while True:
|
||||
points, offset = self._client.scroll(
|
||||
collection_name=self._collection_name,
|
||||
scroll_filter=scroll_filter,
|
||||
limit=256,
|
||||
offset=offset,
|
||||
with_payload=["section_key", "section_display_name"],
|
||||
)
|
||||
for point in points:
|
||||
payload = dict(point.payload or {})
|
||||
key = payload.get("section_key")
|
||||
if key and key not in found:
|
||||
found[key] = payload.get("section_display_name") or key
|
||||
if offset is None:
|
||||
break
|
||||
|
||||
order = {key: index for index, key in enumerate(SECTION_ORDER)}
|
||||
return sorted(
|
||||
found.items(), key=lambda item: order.get(item[0], len(order))
|
||||
)
|
||||
|
||||
def find_by_indication(self, indication_text: str, limit: int) -> list[SearchHit]:
|
||||
"""Reverse lookup: every drug whose `chi_dinh` text mentions the given
|
||||
symptom/indication, keyword-matched. Deterministic, no fabrication
|
||||
|
||||
Reference in New Issue
Block a user