//! 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 article_url: Option, pub created_at: DateTime, } /// Insert a single LLM call log entry. #[allow(clippy::too_many_arguments)] 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, article_url: Option<&str>, ) -> 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, article_url) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) "#, ) .bind(user_id) .bind(job_id) .bind(call_type) .bind(model) .bind(system_prompt) .bind(user_prompt) .bind(response_body) .bind(duration_ms) .bind(article_url) .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, AppError> { let rows = sqlx::query_as::<_, LlmCallLogRow>( r#" SELECT id, call_type, model, system_prompt, user_prompt, response_body, duration_ms, article_url, 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 { 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()) }