knowledge: stamp model + backend on facts for audit

Adds two nullable TEXT columns to entity_facts —
`created_by_model` (LLM identifier) and `created_by_backend`
("local" / "hybrid" / "manual" / NULL) — so the curator can audit
which configurations produce good fact-keeping and which produce
noise.

photo_insights already carries model_version + backend, and
entity_facts.source_insight_id links to it, but:
  - source_insight_id is set post-loop, so chat-continuation and
    regenerated-insight facts lose the link.
  - JOINing per read is more friction than embedding provenance on
    the row itself.
  - Manual facts (POST /knowledge/facts) have no insight at all and
    need their own "manual" provenance marker.

Threading: execute_tool grows `model` + `backend` params, passed
from the three call sites (agentic insight loop, chat single-turn,
chat stream) using the loop-time `chat_backend.primary_model()` +
`effective_backend` already in scope. tool_store_fact stamps the
new fact accordingly; manual create_fact stamps backend="manual".
Legacy rows leave both NULL — pre-tracking data can't be back-
filled reliably from training_messages without burning compute.

Indexes are partial (WHERE NOT NULL) so legacy rows don't bloat
them, and "show me all facts from model X" stays fast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-10 20:05:14 -04:00
parent 85f3716379
commit f53338923d
8 changed files with 90 additions and 2 deletions

View File

@@ -497,6 +497,8 @@ impl InsightChatService {
&normalized,
req.user_id,
&active_persona,
&model_used,
&effective_backend,
&loop_cx,
)
.await;
@@ -870,6 +872,8 @@ impl InsightChatService {
&normalized,
req.user_id,
&active_persona,
&model_used,
&effective_backend,
max_iterations,
&tx,
)
@@ -1059,6 +1063,8 @@ impl InsightChatService {
&normalized,
req.user_id,
&active_persona,
&model_used,
&effective_backend,
max_iterations,
&tx,
)
@@ -1210,6 +1216,10 @@ impl InsightChatService {
normalized: &str,
user_id: i32,
active_persona: &str,
// Provenance — stamped onto any store_fact tool call made
// during this loop. Mirrors the non-streaming chat path.
model_used: &str,
effective_backend: &str,
max_iterations: usize,
tx: &tokio::sync::mpsc::Sender<ChatStreamEvent>,
) -> Result<AgenticLoopOutcome> {
@@ -1290,6 +1300,8 @@ impl InsightChatService {
normalized,
user_id,
active_persona,
model_used,
effective_backend,
&cx,
)
.await;

View File

@@ -1554,6 +1554,13 @@ Return ONLY the summary, nothing else."#,
file_path: &str,
user_id: i32,
persona_id: &str,
// Provenance — written into entity_facts.created_by_* when
// the loop calls store_fact. The caller knows the actual
// chat-runtime model and backend (which may differ from
// ollama.primary_model in hybrid mode where chat lives on
// OpenRouter while Ollama still handles vision).
model: &str,
backend: &str,
cx: &opentelemetry::Context,
) -> String {
let result = match tool_name {
@@ -1574,8 +1581,10 @@ Return ONLY the summary, nothing else."#,
}
"store_entity" => self.tool_store_entity(arguments, ollama, cx).await,
"store_fact" => {
self.tool_store_fact(arguments, file_path, user_id, persona_id, cx)
.await
self.tool_store_fact(
arguments, file_path, user_id, persona_id, model, backend, cx,
)
.await
}
"get_current_datetime" => Self::tool_get_current_datetime(),
unknown => format!("Unknown tool: {}", unknown),
@@ -2632,6 +2641,8 @@ Return ONLY the summary, nothing else."#,
file_path: &str,
user_id: i32,
persona_id: &str,
model: &str,
backend: &str,
cx: &opentelemetry::Context,
) -> String {
use crate::database::models::{InsertEntityFact, InsertEntityPhotoLink};
@@ -2700,6 +2711,8 @@ Return ONLY the summary, nothing else."#,
valid_from,
valid_until: None,
superseded_by: None,
created_by_model: Some(model.to_string()),
created_by_backend: Some(backend.to_string()),
};
let mut kdao = self
@@ -3730,6 +3743,8 @@ Return ONLY the summary, nothing else."#,
&file_path,
user_id,
&persona_id,
chat_backend.primary_model(),
&backend_label,
&loop_cx,
)
.await;