| """Tool: search the knowledge base (RAG).""" |
|
|
| from __future__ import annotations |
|
|
| import os |
| from typing import TYPE_CHECKING, Any |
|
|
| from sqlalchemy import select |
|
|
| from app.models import KnowledgeSource |
| from app.rag import index |
|
|
| |
| |
| |
| |
| _RAG_MIN_SCORE = float(os.getenv("RAG_MIN_SCORE", "0.25")) |
| _RAG_KEEP = 8 |
| _RAG_OVERFETCH = 30 |
|
|
| if TYPE_CHECKING: |
| from app.tools.registry import ToolContext |
|
|
|
|
| def _primary_url(source: KnowledgeSource) -> str | None: |
| """The best real URL to cite for a source (its location if public, else the |
| first discovered page). Never invented — these come from indexing.""" |
| if (source.location or "").startswith(("http://", "https://")): |
| return source.location |
| for u in (source.urls or []): |
| if u.startswith(("http://", "https://")): |
| return u |
| return None |
|
|
|
|
| async def run(args: dict[str, Any], ctx: ToolContext) -> dict[str, Any]: |
| query = (args.get("query") or "").strip() |
| if not query: |
| return {"context": "", "sources": [], "note": "empty query"} |
| results = await index.search(ctx.db, query, k=_RAG_OVERFETCH, tenant_id=ctx.tenant_id) |
| |
| |
| kept = [(c, s) for c, s in results if s >= _RAG_MIN_SCORE][:_RAG_KEEP] |
| |
| |
| |
| |
| |
| if not kept and results: |
| best_c, best_s = max(results, key=lambda cs: cs[1]) |
| if best_s >= _RAG_MIN_SCORE * 0.6: |
| kept = [(best_c, best_s)] |
| if not kept: |
| |
| |
| from app import analytics |
|
|
| analytics.record( |
| ctx.db, ctx.tenant_id, "unresolved", |
| session_id=ctx.session.id, meta={"q": query[:300]}, |
| ) |
| return { |
| "context": "", |
| "sources": [], |
| "note": "no relevant information found", |
| "hint": "No tienes información sobre esto. NO la inventes: dile al " |
| "cliente que no estás seguro y ofrece pasarlo con el equipo " |
| "(escalate_to_human) para confirmárselo.", |
| } |
| blocks = [] |
| sources: list[str] = [] |
| source_ids = [] |
| for chunk, _score in kept: |
| name = (chunk.meta or {}).get("source_name", "fuente") |
| blocks.append(f"[{name}] {chunk.text}") |
| if name not in sources: |
| sources.append(name) |
| if chunk.source_id not in source_ids: |
| source_ids.append(chunk.source_id) |
|
|
| |
| |
| links: list[dict[str, str]] = [] |
| if source_ids: |
| rows = ( |
| await ctx.db.execute( |
| select(KnowledgeSource).where( |
| KnowledgeSource.id.in_(source_ids), |
| KnowledgeSource.tenant_id == ctx.tenant_id, |
| ) |
| ) |
| ).scalars().all() |
| for s in rows: |
| url = _primary_url(s) |
| if url: |
| links.append({"name": s.name, "url": url}) |
|
|
| out: dict[str, Any] = {"context": "\n\n".join(blocks), "sources": sources} |
| if links: |
| out["links"] = links |
| |
| |
| |
| |
| out["hint"] = ( |
| "Cuando cites una política o un dato concreto y verificable " |
| "(devoluciones, envíos, garantía, precios, plazos), añade el enlace " |
| "real al final en formato [nombre](url) para que el cliente pueda " |
| "comprobarlo — genera confianza. Usa EXACTAMENTE una de estas URLs, " |
| "nunca inventes ni modifiques rutas." |
| ) |
| return out |
|
|