trust: proactive real-source citation in bot replies
Browse files- app/tools/knowledge_tool.py +9 -2
- tests/tools/test_tools.py +35 -0
app/tools/knowledge_tool.py
CHANGED
|
@@ -98,8 +98,15 @@ async def run(args: dict[str, Any], ctx: ToolContext) -> dict[str, Any]:
|
|
| 98 |
out: dict[str, Any] = {"context": "\n\n".join(blocks), "sources": sources}
|
| 99 |
if links:
|
| 100 |
out["links"] = links
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
out["hint"] = (
|
| 102 |
-
"
|
| 103 |
-
"
|
|
|
|
|
|
|
|
|
|
| 104 |
)
|
| 105 |
return out
|
|
|
|
| 98 |
out: dict[str, Any] = {"context": "\n\n".join(blocks), "sources": sources}
|
| 99 |
if links:
|
| 100 |
out["links"] = links
|
| 101 |
+
# Proactive citation builds real trust (the customer can verify a policy
|
| 102 |
+
# claim themselves) instead of just asking the shopper to take the bot's
|
| 103 |
+
# word for it -- especially for concrete/checkable facts (returns,
|
| 104 |
+
# shipping windows, warranty, prices). Not every trivial reply needs one.
|
| 105 |
out["hint"] = (
|
| 106 |
+
"Cuando cites una política o un dato concreto y verificable "
|
| 107 |
+
"(devoluciones, envíos, garantía, precios, plazos), añade el enlace "
|
| 108 |
+
"real al final en formato [nombre](url) para que el cliente pueda "
|
| 109 |
+
"comprobarlo — genera confianza. Usa EXACTAMENTE una de estas URLs, "
|
| 110 |
+
"nunca inventes ni modifiques rutas."
|
| 111 |
)
|
| 112 |
return out
|
tests/tools/test_tools.py
CHANGED
|
@@ -133,6 +133,41 @@ async def test_dispatch_knowledge_returns_context(db_session, monkeypatch):
|
|
| 133 |
assert out["sources"] == ["FAQ"]
|
| 134 |
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
async def test_dispatch_products_surfaces_cards_and_add_url(db_session):
|
| 137 |
payload = {
|
| 138 |
"products": {
|
|
|
|
| 133 |
assert out["sources"] == ["FAQ"]
|
| 134 |
|
| 135 |
|
| 136 |
+
async def test_knowledge_tool_nudges_proactive_citation_with_real_link(db_session, monkeypatch):
|
| 137 |
+
"""Trust: when the KB match has a real, verified URL, the model is told to
|
| 138 |
+
proactively CITE it for concrete/checkable claims (returns, shipping, price)
|
| 139 |
+
-- not just when the shopper explicitly asks for a link. The widget already
|
| 140 |
+
renders [name](url) as a real clickable link."""
|
| 141 |
+
from app import embeddings as emb
|
| 142 |
+
|
| 143 |
+
async def fake_query(q):
|
| 144 |
+
return [1.0, 0.0]
|
| 145 |
+
|
| 146 |
+
async def fake_texts(texts, *, kind="passage"):
|
| 147 |
+
return [[1.0, 0.0] for _ in texts]
|
| 148 |
+
|
| 149 |
+
monkeypatch.setattr(emb, "embed_query", fake_query)
|
| 150 |
+
monkeypatch.setattr(emb, "embed_texts", fake_texts)
|
| 151 |
+
|
| 152 |
+
src = KnowledgeSource(kind="url", name="Política de envíos", location="https://tienda.example/envios")
|
| 153 |
+
db_session.add(src)
|
| 154 |
+
await db_session.flush()
|
| 155 |
+
db_session.add(
|
| 156 |
+
KnowledgeChunk(
|
| 157 |
+
source_id=src.id, ordinal=0, text="Envíos gratis desde 199€", embedding=[1.0, 0.0],
|
| 158 |
+
meta={"source_name": "Política de envíos"},
|
| 159 |
+
)
|
| 160 |
+
)
|
| 161 |
+
await db_session.flush()
|
| 162 |
+
|
| 163 |
+
ctx = ToolContext(db=db_session, session=await _session(db_session))
|
| 164 |
+
out = await registry.dispatch("search_knowledge", {"query": "envíos"}, ctx)
|
| 165 |
+
assert out["links"] == [{"name": "Política de envíos", "url": "https://tienda.example/envios"}]
|
| 166 |
+
assert "[nombre](url)" in out["hint"] # tells the model the exact citable format
|
| 167 |
+
assert "verificable" in out["hint"] # nudges WHEN to cite (concrete/checkable claims)
|
| 168 |
+
assert "EXACTAMENTE una de estas URLs" in out["hint"] # still forbids inventing routes
|
| 169 |
+
|
| 170 |
+
|
| 171 |
async def test_dispatch_products_surfaces_cards_and_add_url(db_session):
|
| 172 |
payload = {
|
| 173 |
"products": {
|