v0.17.3: bug-fix maintenance pass — ingest origin exemption, FK safety, auto-search reset, deterministic ingest, WAL, config centralization, dead-code removal

- /api/ingest exempt from origin check for CLI/Bearer clients
- recreate deleted conversations on bogus conversation_id (chat + search)
- augmented auto-search emits reset:true; frontend clears first-pass text
- image uploads stored as placeholders, never text-ingested
- check_fact_conflicts requires shared subject keywords
- deterministic ingest point ids (md5 chunk hash)
- get_load() no longer crashes when rocm-smi yields no VRAM lines
- SQLite WAL + busy_timeout; timing-safe API key compares
- supervise fire-and-forget auto-ingest tasks; log missing logprobs
- centralize EMBED_URL/EMBED_MODEL/QDRANT_URL/NODE_NAME in config
- remove dead triage.py, select_node tests, is_state_changing, stray artifacts
- add regression tests + autouse global-reset conftest
This commit is contained in:
gramps
2026-08-07 14:42:35 -07:00
parent df405a156e
commit 44387919a8
24 changed files with 478 additions and 7171 deletions
+26 -5
View File
@@ -23,6 +23,22 @@ from config import MAX_CHAT_MESSAGE_CHARS, MODEL_CONTEXT_LENGTH
log = logging.getLogger("caic")
router = APIRouter()
# References to background auto-ingest tasks so they are never garbage-collected.
_ingest_tasks: set = set()
async def _safe_ingest(coro):
try:
await coro
except Exception as e:
log.warning("auto-ingest task failed: %s", e)
def _spawn_ingest(coro):
task = asyncio.create_task(_safe_ingest(coro))
_ingest_tasks.add(task)
task.add_done_callback(_ingest_tasks.discard)
def parse_llama_stream_chunk(line: str) -> tuple:
if line.startswith("data: "):
@@ -112,6 +128,11 @@ async def chat(request: Request):
db.execute("INSERT INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(conv_id, encrypt_text(title), model, now, now))
else:
# A client-supplied id may reference a conversation that no longer exists;
# recreate the row so the message insert satisfies the FK instead of 500ing.
title = user_message[:80] + ("..." if len(user_message) > 80 else "")
db.execute("INSERT OR IGNORE INTO conversations (id, title, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(conv_id, encrypt_text(title), model, now, now))
db.execute("UPDATE conversations SET updated_at = ? WHERE id = ?", (now, conv_id))
db.execute("INSERT INTO messages (conversation_id, role, content, created_at, perplexity) VALUES (?, ?, ?, ?, ?)",
@@ -171,6 +192,8 @@ async def chat(request: Request):
assistant_msg = "".join(full_response)
perplexity = calculate_perplexity(all_logprobs) if all_logprobs else 0.0
if not all_logprobs:
log.warning("No logprobs received from inference server — perplexity auto-search unavailable")
should_search = is_uncertain(all_logprobs) or is_refusal(assistant_msg)
if search_enabled and should_search:
@@ -210,7 +233,7 @@ async def chat(request: Request):
if is_refusal(cleaned_response) or len(cleaned_response) < 20:
cleaned_response = format_direct_answer(user_message, search_results)
yield f"data: {json.dumps({'token': cleaned_response, 'conversation_id': conv_id, 'augmented': True})}\n\n"
yield f"data: {json.dumps({'token': cleaned_response, 'conversation_id': conv_id, 'augmented': True, 'reset': True})}\n\n"
if not private_chat:
saved_msg = cleaned_response + "\n\n---\n*🔍 Enhanced with web search results*"
@@ -229,8 +252,7 @@ async def chat(request: Request):
if conflicts:
rag_update = {"conflicts": conflicts}
else:
# Fire-and-forget: persist facts silently, don't block the response
asyncio.create_task(ingest_auto_fact(facts, user_message, cleaned_response))
_spawn_ingest(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
@@ -252,8 +274,7 @@ async def chat(request: Request):
if conflicts:
rag_update = {"conflicts": conflicts}
else:
# Fire-and-forget: persist facts silently, don't block the response
asyncio.create_task(ingest_auto_fact(facts, user_message, assistant_msg))
_spawn_ingest(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"