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

@@ -1470,6 +1470,8 @@ mod tests {
valid_from: None,
valid_until: None,
superseded_by: None,
created_by_model: None,
created_by_backend: None,
},
)
.unwrap();
@@ -1691,6 +1693,8 @@ mod tests {
valid_from: None,
valid_until: None,
superseded_by: None,
created_by_model: None,
created_by_backend: None,
},
);
assert!(
@@ -1923,6 +1927,8 @@ mod tests {
valid_from: None,
valid_until: None,
superseded_by: None,
created_by_model: None,
created_by_backend: None,
},
)
.unwrap();

View File

@@ -262,6 +262,12 @@ pub struct InsertEntityFact {
/// the supersede endpoint; status flips to 'superseded' in the
/// same transaction. See migration 2026-05-10-000200.
pub superseded_by: Option<i32>,
/// Provenance for model audit — see migration 2026-05-10-000300.
/// `created_by_model` is the LLM identifier (e.g. "qwen2.5:7b",
/// "anthropic/claude-sonnet-4") or NULL for legacy / manual rows.
/// `created_by_backend` is "local" / "hybrid" / "manual" / NULL.
pub created_by_model: Option<String>,
pub created_by_backend: Option<String>,
}
#[derive(Serialize, Queryable, Clone, Debug)]
@@ -281,6 +287,8 @@ pub struct EntityFact {
pub valid_from: Option<i64>,
pub valid_until: Option<i64>,
pub superseded_by: Option<i32>,
pub created_by_model: Option<String>,
pub created_by_backend: Option<String>,
}
#[derive(Insertable)]

View File

@@ -62,6 +62,8 @@ diesel::table! {
valid_from -> Nullable<BigInt>,
valid_until -> Nullable<BigInt>,
superseded_by -> Nullable<Integer>,
created_by_model -> Nullable<Text>,
created_by_backend -> Nullable<Text>,
}
}