Merge W9B — kei-chat-store cost Real reinstated
This commit is contained in:
commit
9fe780b7ac
4 changed files with 42 additions and 16 deletions
|
|
@ -1,22 +1,26 @@
|
|||
//! kei-chat-store EntitySchema — declarative spec consumed by
|
||||
//! `kei_entity_store::Store` and its verb templates.
|
||||
//!
|
||||
//! Shape (Layer-A convergence, 2026-04-23):
|
||||
//! Shape (Layer-A convergence, 2026-04-23; cost-column re-migration
|
||||
//! 2026-04-23 wave 8):
|
||||
//!
|
||||
//! - Primary entity = `chat_messages` (INTEGER PK; required by engine).
|
||||
//! Engine owns: create / get / list / search verbs + FTS reindex.
|
||||
//! - Bespoke: `chat_sessions` has a TEXT UUID primary key that the
|
||||
//! engine's `FieldKind::IntegerPk` cannot represent, so its DDL rides
|
||||
//! the engine's `custom_migrations` slot and its CRUD stays in
|
||||
//! `sessions.rs` (analogous to kei-task's milestones / deps / graph).
|
||||
//! - Archive: sessions use a TEXT `status` enum ('active' | 'archived')
|
||||
//! rather than an INTEGER flag, so the engine `archive` verb is NOT
|
||||
//! enabled. Session archival stays bespoke.
|
||||
//! - Per-message `cost` (REAL in legacy) is dropped from `chat_messages`:
|
||||
//! the engine has no REAL FieldKind and the only consumer is the
|
||||
//! session-level aggregate `chat_sessions.total_cost`, updated
|
||||
//! bespoke in `save_message`. No caller reads per-message cost in
|
||||
//! current tests or the CLI surface.
|
||||
//! - Bespoke: `chat_sessions` has a TEXT UUID primary key. The engine
|
||||
//! gained `FieldKind::TextPk` in wave 8 but Store::open currently
|
||||
//! takes a SINGLE EntitySchema, so a second managed schema for
|
||||
//! sessions would require engine multi-schema support. Until that
|
||||
//! lands, the session DDL rides `custom_migrations` and its CRUD
|
||||
//! stays in `sessions.rs` (analogous to kei-task's milestones / deps
|
||||
//! / graph). **Known open:** promote chat_sessions to a second
|
||||
//! EntitySchema (`TextPk` + `TextArchiveEnum`) once engine gains
|
||||
//! multi-schema support.
|
||||
//! - Archive: sessions use a TEXT `status` enum ('active' | 'archived').
|
||||
//! Session archival stays bespoke (same reason as above).
|
||||
//! - Per-message `cost` (REAL) is restored via
|
||||
//! `FieldKind::RealDefault(0.0)` — wave-8 addition. Engine-managed
|
||||
//! INSERT/SELECT, no bespoke SQL needed. Previously dropped when
|
||||
//! the engine had no REAL kind; re-instated 2026-04-23.
|
||||
//!
|
||||
//! FTS column-name change vs pre-convergence shape:
|
||||
//! legacy fts_chat(message_id, session_id UNINDEXED, content)
|
||||
|
|
@ -34,6 +38,7 @@ static FIELDS: &[FieldDef] = &[
|
|||
FieldDef::text_nn("content"),
|
||||
FieldDef::integer("tokens_in"),
|
||||
FieldDef::integer("tokens_out"),
|
||||
FieldDef::real_default("cost", 0.0),
|
||||
FieldDef::created_at(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
//! `kei_entity_store::verbs::search` using `CHAT_SCHEMA`. The engine
|
||||
//! handles FTS5 JOIN + rank ordering; this module maps the generic
|
||||
//! JSON result back to typed `ChatMessage` rows for legacy callers.
|
||||
//! Per-message `cost` is not persisted after the convergence (see
|
||||
//! `schema.rs` note); `cost` is populated as 0.0 on every hit.
|
||||
//! Per-message `cost` is persisted (engine `RealDefault` field);
|
||||
//! `row_to_message` reads it back as f64.
|
||||
|
||||
use crate::schema::CHAT_SCHEMA;
|
||||
use crate::sessions::ChatMessage;
|
||||
|
|
@ -32,7 +32,7 @@ fn row_to_message(r: &Value) -> Result<ChatMessage> {
|
|||
content: r["content"].as_str().unwrap_or("").into(),
|
||||
tokens_in: r["tokens_in"].as_i64().unwrap_or(0),
|
||||
tokens_out: r["tokens_out"].as_i64().unwrap_or(0),
|
||||
cost: 0.0,
|
||||
cost: r["cost"].as_f64().unwrap_or(0.0),
|
||||
created_at: r["created_at"].as_i64().unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ pub fn save_message(store: &Store, msg: &ChatMessage) -> Result<i64> {
|
|||
"content": msg.content,
|
||||
"tokens_in": msg.tokens_in,
|
||||
"tokens_out": msg.tokens_out,
|
||||
"cost": msg.cost,
|
||||
"created_at": msg.created_at,
|
||||
});
|
||||
let v = v_create::run(store.conn(), &CHAT_SCHEMA, payload)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,26 @@ fn engine_migration_parity_smoke() {
|
|||
assert_eq!(sess.message_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_roundtrips_via_search() {
|
||||
// Wave-8 re-migration: cost is re-instated as engine-managed
|
||||
// RealDefault column. The value written via save_message must be
|
||||
// visible on the ChatMessage returned from search (no longer 0.0).
|
||||
let s = mk();
|
||||
let sid = start_session(&s, "demo", "", "").unwrap();
|
||||
save_message(&s, &ChatMessage {
|
||||
session_id: sid, role: "user".into(),
|
||||
content: "rust async tokio bench cost-marker".into(),
|
||||
tokens_in: 1, tokens_out: 1, cost: 0.00777,
|
||||
..Default::default()
|
||||
}).unwrap();
|
||||
let hits = search(&s, "cost-marker", 10).unwrap();
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert!((hits[0].cost - 0.00777).abs() < 1e-9,
|
||||
"cost should round-trip via engine RealDefault column, got {}",
|
||||
hits[0].cost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_aggregates() {
|
||||
let s = mk();
|
||||
|
|
|
|||
Loading…
Reference in a new issue