feat(kb): multi-topic foundation — kb_topic table, topic-scoped endpoints
Phase 1 of the multi-domain refactor. Adds a kb_topic table seeded with the existing 'ביטוח לאומי' domain (id=1) and pins all 6 existing kb_source rows to it, so search/ask can be scoped per domain without disturbing the current single-topic UX. - scripts/migrations/001_topics.sql: idempotent migration. Creates kb_topic, seeds national-insurance with the system_prompt_addendum copied verbatim from kb_public.py's hardcoded text, adds nullable kb_source.topic_id, backfills it to 1, then SET NOT NULL. GRANTs to shira_kb mirror its existing kb_source privileges. Wrapped in BEGIN /COMMIT with a sanity-check that raises if the backfill is incomplete. - api/services/kb/topics.py: list_topics / get_topic / resolve_topic_id. resolve_topic_id raises ValueError on unknown ids so the route layer can return 400 instead of silently mixing domains. - api/services/kb/search.py: search() + _retrieve_rrf accept topic_id; the SQL adds AND s.topic_id = $N when present. _expand_query is parametrized by topic name (was hardcoded "הביטוח הלאומי"). - api/routes/kb_public.py: new GET /kb/topics. /kb/search /kb/sources /kb/ask /kb/ask/stream all accept topic_id and call resolve_topic_id. _build_ask_runner_context loads system_prompt_addendum from the DB instead of the hardcoded insurance string. - mcp_server/tools/legal_kb_tools.py: tool renamed search_insurance_kb → search_legal_kb, takes topic_id at registration so each conversation is pinned to its caller-chosen domain. Tool description interpolates the topic name. - api/services/agent_runner.py: kb_topic_id + kb_topic_name flow through to register_legal_kb_tools. Hebrew progress labels updated. Refs Task Master #12 (espocrm-extensions/KnowledgeBase). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
"""Israeli National Insurance knowledge base tools (search)."""
|
||||
"""Legal knowledge base tool — topic-scoped semantic + lexical search.
|
||||
|
||||
Renamed from search_insurance_kb to search_legal_kb in Phase 1 of the
|
||||
multi-topic refactor (Task Master KnowledgeBase #12). The handler now
|
||||
takes a topic_id at registration time so each conversation is scoped to
|
||||
its caller-chosen domain.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -47,15 +53,30 @@ def _format_hits(hits: list[dict]) -> str:
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def register_legal_kb_tools(tools: dict, sources_used: list[dict] | None = None) -> None:
|
||||
"""Register the search_insurance_kb tool.
|
||||
def register_legal_kb_tools(
|
||||
tools: dict,
|
||||
sources_used: list[dict] | None = None,
|
||||
topic_id: int | None = None,
|
||||
topic_name: str | None = None,
|
||||
) -> None:
|
||||
"""Register the search_legal_kb tool.
|
||||
|
||||
topic_id pins retrieval to a single domain (kb_source.topic_id). When
|
||||
None, the underlying search falls back to the lowest active topic —
|
||||
the route layer (kb_public.py) normally resolves a concrete id before
|
||||
this is called, so None should be a transitional case only.
|
||||
|
||||
topic_name is interpolated into the tool description so the LLM sees
|
||||
a domain-specific description (e.g. "ביטוח לאומי" vs "דיני עבודה").
|
||||
|
||||
If `sources_used` is provided, each hit's (source_id, page_number,
|
||||
section_ref, title, kind, original_path) is appended — deduplicated
|
||||
by (source_id, page_number) — so the caller can build a "view PDF"
|
||||
link list alongside the agent's text answer.
|
||||
"""
|
||||
async def search_insurance_kb(
|
||||
domain_text = topic_name or "המשפט"
|
||||
|
||||
async def search_legal_kb(
|
||||
query: str,
|
||||
kind: str = "any",
|
||||
top_k: int = 8,
|
||||
@@ -64,7 +85,13 @@ def register_legal_kb_tools(tools: dict, sources_used: list[dict] | None = None)
|
||||
if kind not in ("law", "regulation", "circular", "any"):
|
||||
kind = "any"
|
||||
top_k = max(1, min(int(top_k or 8), 15))
|
||||
hits = await kb_search.search(query=query, kind=kind, top_k=top_k)
|
||||
hits = await kb_search.search(
|
||||
query=query,
|
||||
kind=kind,
|
||||
top_k=top_k,
|
||||
topic_id=topic_id,
|
||||
topic_name=topic_name,
|
||||
)
|
||||
if sources_used is not None:
|
||||
seen = {(s.get("source_id"), s.get("page_number")) for s in sources_used}
|
||||
for h in hits:
|
||||
@@ -83,16 +110,15 @@ def register_legal_kb_tools(tools: dict, sources_used: list[dict] | None = None)
|
||||
seen.add(key)
|
||||
return _format_hits(hits)
|
||||
except Exception as e:
|
||||
logger.exception("[search_insurance_kb] failed")
|
||||
logger.exception("[search_legal_kb] failed (topic_id=%s)", topic_id)
|
||||
return fail(f"שגיאה בחיפוש בבסיס הידע: {e}")
|
||||
|
||||
tools["search_insurance_kb"] = {
|
||||
tools["search_legal_kb"] = {
|
||||
"description": (
|
||||
"חיפוש בבסיס הידע של הביטוח הלאומי — חוק הביטוח הלאומי, תקנות הביטוח "
|
||||
"הלאומי וחוזרי הביטוח הלאומי. מחזיר קטעים רלוונטיים עם ציטוט מדויק "
|
||||
"(סעיף/תקנה/מס' חוזר). יש להשתמש בכלי לפני טענה משפטית מבוססת-מקור, "
|
||||
"ולצטט את ה-section_ref המדויק בתשובה. החיפוש משלב דמיון סמנטי וחיפוש "
|
||||
"מילולי (hybrid)."
|
||||
f"חיפוש בבסיס הידע המשפטי בנושא {domain_text} — חוקים, תקנות וחוזרים. "
|
||||
"מחזיר קטעים רלוונטיים עם ציטוט מדויק (סעיף/תקנה/מס' חוזר). "
|
||||
"יש להשתמש בכלי לפני טענה משפטית מבוססת-מקור, ולצטט את ה-section_ref "
|
||||
"המדויק בתשובה. החיפוש משלב דמיון סמנטי וחיפוש מילולי (hybrid)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -113,5 +139,5 @@ def register_legal_kb_tools(tools: dict, sources_used: list[dict] | None = None)
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
"handler": search_insurance_kb,
|
||||
"handler": search_legal_kb,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user