feat: create llm_call_log table + DB module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>master
parent
88c16c5d67
commit
b2b0b286c0
@ -0,0 +1,14 @@
|
||||
CREATE TABLE llm_call_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
job_id UUID NOT NULL,
|
||||
call_type TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
system_prompt TEXT NOT NULL DEFAULT '',
|
||||
user_prompt TEXT NOT NULL DEFAULT '',
|
||||
response_body TEXT NOT NULL DEFAULT '',
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_llm_call_log_job_id ON llm_call_log(job_id);
|
||||
CREATE INDEX idx_llm_call_log_user_id ON llm_call_log(user_id, created_at);
|
||||
@ -0,0 +1,97 @@
|
||||
//! LLM call logging: tracks every LLM interaction during synthesis generation.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::AppError;
|
||||
|
||||
/// Row returned from llm_call_log queries.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
pub struct LlmCallLogRow {
|
||||
pub id: Uuid,
|
||||
pub call_type: String,
|
||||
pub model: String,
|
||||
pub system_prompt: String,
|
||||
pub user_prompt: String,
|
||||
pub response_body: String,
|
||||
pub duration_ms: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Insert a single LLM call log entry.
|
||||
pub async fn insert(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
job_id: Uuid,
|
||||
call_type: &str,
|
||||
model: &str,
|
||||
system_prompt: &str,
|
||||
user_prompt: &str,
|
||||
response_body: &str,
|
||||
duration_ms: i32,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO llm_call_log (user_id, job_id, call_type, model, system_prompt, user_prompt, response_body, duration_ms)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(job_id)
|
||||
.bind(call_type)
|
||||
.bind(model)
|
||||
.bind(system_prompt)
|
||||
.bind(user_prompt)
|
||||
.bind(response_body)
|
||||
.bind(duration_ms)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all LLM call log entries for a generation job.
|
||||
pub async fn list_by_job_id(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
job_id: Uuid,
|
||||
) -> Result<Vec<LlmCallLogRow>, AppError> {
|
||||
let rows = sqlx::query_as::<_, LlmCallLogRow>(
|
||||
r#"
|
||||
SELECT id, call_type, model, system_prompt, user_prompt, response_body, duration_ms, created_at
|
||||
FROM llm_call_log
|
||||
WHERE user_id = $1 AND job_id = $2
|
||||
ORDER BY created_at ASC
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(job_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Truncate old LLM call log entries: replace prompt/response with first 500 chars.
|
||||
pub async fn truncate_old(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
days: i32,
|
||||
) -> Result<u64, AppError> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE llm_call_log
|
||||
SET system_prompt = LEFT(system_prompt, 500) || E'\n[truncated]',
|
||||
user_prompt = LEFT(user_prompt, 500) || E'\n[truncated]',
|
||||
response_body = LEFT(response_body, 500) || E'\n[truncated]'
|
||||
WHERE user_id = $1
|
||||
AND created_at < now() - make_interval(days => $2)
|
||||
AND LENGTH(system_prompt) > 510
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(days)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
Loading…
Reference in New Issue