v0.17.2: auto-fact detection with conflict-alert flow

- auto_detect_facts() scans chat turns for factual content (IPs, paths,
  services, config changes, hardware refs) using pattern matching
- check_fact_conflicts() cross-references detected facts against stored
  FTS5 memories — when a contradiction exists (same topic, diff value)
  the system surfaces a rag_update_suggestion in the done SSE payload
- Frontend shows a floating notification banner comparing old vs new
  fact with Update/Dismiss buttons
- confirm_fact_update() replaces the memory + re-embeds/re-indexes
  the Qdraft entry on user confirmation
- Silent auto-ingest (memories + Qdrant) when no conflict exists
- Frontend: msg-toolbar opacity 0→0.35 for visibility
This commit is contained in:
gramps
2026-07-13 08:25:08 -07:00
parent dcb73945e0
commit cbe4a361bb
6 changed files with 268 additions and 6 deletions
+22 -4
View File
@@ -1,4 +1,5 @@
"""JarvisChat routers - /api/chat streaming endpoint."""
import asyncio
import json
import logging
import uuid
@@ -10,8 +11,8 @@ from fastapi.responses import StreamingResponse
from config import DEFAULT_MODEL, LLAMA_SERVER_BASE
from db import get_db, get_upload_context
from memory import process_remember_command
from rag import build_system_prompt
from memory import process_remember_command, auto_detect_facts, check_fact_conflicts
from rag import build_system_prompt, ingest_auto_fact
from search import (calculate_perplexity, is_uncertain, is_refusal,
clean_hedging, format_search_results, format_direct_answer,
extract_search_query, query_searxng)
@@ -127,6 +128,7 @@ async def chat(request: Request):
tokens_per_sec = 0.0
completion_tokens = 0
prompt_tokens = 0
rag_update = None
if remember_response:
yield f"data: {json.dumps({'token': remember_response + chr(10) + chr(10), 'conversation_id': conv_id})}\n\n"
@@ -204,7 +206,15 @@ async def chat(request: Request):
db2.commit()
db2.close()
yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'searched': True, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'prompt_tokens': prompt_tokens, 'completion_tokens': completion_tokens, 'context_length': MODEL_CONTEXT_LENGTH})}\n\n"
facts = auto_detect_facts(user_message, cleaned_response)
if facts:
conflicts = check_fact_conflicts(facts)
if conflicts:
rag_update = {"conflicts": conflicts}
else:
asyncio.ensure_future(ingest_auto_fact(facts, user_message, cleaned_response))
yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'searched': True, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'prompt_tokens': prompt_tokens, 'completion_tokens': completion_tokens, 'context_length': MODEL_CONTEXT_LENGTH, **(rag_update and {'rag_update_suggestion': rag_update} or {})})}\n\n"
return
saved_msg = assistant_msg
@@ -217,7 +227,15 @@ async def chat(request: Request):
db2.commit()
db2.close()
yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'prompt_tokens': prompt_tokens, 'completion_tokens': completion_tokens, 'context_length': MODEL_CONTEXT_LENGTH})}\n\n"
facts = auto_detect_facts(user_message, assistant_msg)
if facts:
conflicts = check_fact_conflicts(facts)
if conflicts:
rag_update = {"conflicts": conflicts}
else:
asyncio.ensure_future(ingest_auto_fact(facts, user_message, assistant_msg))
yield f"data: {json.dumps({'done': True, 'conversation_id': conv_id, 'perplexity': round(perplexity, 2), 'tokens_per_sec': round(tokens_per_sec, 1), 'prompt_tokens': prompt_tokens, 'completion_tokens': completion_tokens, 'context_length': MODEL_CONTEXT_LENGTH, **(rag_update and {'rag_update_suggestion': rag_update} or {})})}\n\n"
except httpx.RemoteProtocolError:
pass
+17
View File
@@ -4,6 +4,7 @@ from typing import Optional
from db import get_db
from memory import add_memory, delete_memory, update_memory, get_all_memories, search_memories
from rag import confirm_fact_update
from security import read_json_body, BODY_LIMIT_DEFAULT_BYTES
from config import MAX_MEMORY_FACT_CHARS
@@ -54,6 +55,22 @@ async def search_memories_api(q: str, limit: int = 10):
return {"results": results, "count": len(results)}
@router.post("/api/memories/confirm-update")
async def confirm_memory_update(request: Request):
body = await read_json_body(request, BODY_LIMIT_DEFAULT_BYTES)
memory_id = body.get("memory_id")
new_fact = str(body.get("new_fact", "")).strip()
old_fact = str(body.get("old_fact", "")).strip()
user_message = str(body.get("user_message", "")).strip()
assistant_message = str(body.get("assistant_message", "")).strip()
if not memory_id or not new_fact:
raise HTTPException(status_code=400, detail="memory_id and new_fact are required")
ok = await confirm_fact_update(memory_id, old_fact, new_fact, user_message, assistant_message)
if not ok:
raise HTTPException(status_code=404, detail="Memory not found")
return {"status": "ok"}
@router.get("/api/memories/stats")
async def memory_stats():
db = get_db()