diff --git a/CLAUDE.md b/CLAUDE.md index 8565db9..84a8223 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,7 @@ cd frontend && npx tsc --noEmit - `GET /api/v1/admin/users` — user list - `PUT /api/v1/admin/users/:id/role` — role management -## Database (20 migrations) +## Database (21 migrations) Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log` ## Environment Variables diff --git a/backend/migrations/20260325000021_add_article_url_to_llm_log.sql b/backend/migrations/20260325000021_add_article_url_to_llm_log.sql new file mode 100644 index 0000000..e41814b --- /dev/null +++ b/backend/migrations/20260325000021_add_article_url_to_llm_log.sql @@ -0,0 +1 @@ +ALTER TABLE llm_call_log ADD COLUMN article_url TEXT; diff --git a/backend/src/db/llm_call_log.rs b/backend/src/db/llm_call_log.rs index c0bf81e..0bbcb65 100644 --- a/backend/src/db/llm_call_log.rs +++ b/backend/src/db/llm_call_log.rs @@ -17,6 +17,7 @@ pub struct LlmCallLogRow { pub user_prompt: String, pub response_body: String, pub duration_ms: i32, + pub article_url: Option, pub created_at: DateTime, } @@ -32,11 +33,12 @@ pub async fn insert( 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) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + 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) @@ -47,6 +49,7 @@ pub async fn insert( .bind(user_prompt) .bind(response_body) .bind(duration_ms) + .bind(article_url) .execute(pool) .await?; Ok(()) @@ -60,7 +63,7 @@ pub async fn list_by_job_id( ) -> Result, AppError> { let rows = sqlx::query_as::<_, LlmCallLogRow>( r#" - SELECT id, call_type, model, system_prompt, user_prompt, response_body, duration_ms, created_at + 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 diff --git a/backend/src/services/source_scraper.rs b/backend/src/services/source_scraper.rs index a8e19fa..2b1656a 100644 --- a/backend/src/services/source_scraper.rs +++ b/backend/src/services/source_scraper.rs @@ -172,6 +172,7 @@ pub async fn extract_article_links_with_llm( crate::db::llm_call_log::insert( pool, uid, jid, "link_extraction", model, &system, &user, &response_str, llm_duration as i32, + None, ).await.ok(); } diff --git a/backend/src/services/synthesis.rs b/backend/src/services/synthesis.rs index 6f03f5e..99a35a5 100644 --- a/backend/src/services/synthesis.rs +++ b/backend/src/services/synthesis.rs @@ -485,7 +485,7 @@ async fn run_generation_inner( // Log the LLM call if let Ok(ref resp) = result { let resp_str = serde_json::to_string_pretty(resp).unwrap_or_default(); - crate::db::llm_call_log::insert(&pool, uid, jid, "classify_summarize", &mdl, &sys, &usr, &resp_str, duration as i32).await.ok(); + crate::db::llm_call_log::insert(&pool, uid, jid, "classify_summarize", &mdl, &sys, &usr, &resp_str, duration as i32, Some(&url)).await.ok(); } (url, su, title, result) @@ -569,7 +569,7 @@ async fn run_generation_inner( let llm_start = std::time::Instant::now(); let raw_results = provider.call_llm(&model_websearch, &sys_prompt, &usr_prompt, &search_schema).await?; let llm_duration = llm_start.elapsed().as_millis() as u64; - log_llm_call(&state.pool, user_id, job_id, "search", &model_websearch, &sys_prompt, &usr_prompt, &raw_results, llm_duration).await; + log_llm_call(&state.pool, user_id, job_id, "search", &model_websearch, &sys_prompt, &usr_prompt, &raw_results, llm_duration, None).await; emit_progress(tx, "parsing", "Analyse des resultats...", 75); let parsed = parse_llm_output(&raw_results, &user_categories)?; @@ -759,11 +759,13 @@ async fn log_llm_call( user_prompt: &str, response: &serde_json::Value, duration_ms: u64, + article_url: Option<&str>, ) { let response_str = serde_json::to_string_pretty(response).unwrap_or_default(); db::llm_call_log::insert( pool, user_id, job_id, call_type, model, system_prompt, user_prompt, &response_str, duration_ms as i32, + article_url, ) .await .ok(); // Don't fail synthesis if logging fails diff --git a/frontend/src/i18n/fr.ts b/frontend/src/i18n/fr.ts index 41c2b7c..efcc5bf 100644 --- a/frontend/src/i18n/fr.ts +++ b/frontend/src/i18n/fr.ts @@ -368,6 +368,7 @@ const fr = { 'llmLogs.viewLogs': 'Voir les logs IA', 'llmLogs.seconds': 's', 'llmLogs.back': 'Retour', + 'llmLogs.articleUrl': 'Article', // Common 'common.loading': 'Chargement...', diff --git a/frontend/src/pages/LlmLogs.tsx b/frontend/src/pages/LlmLogs.tsx index eb360e8..b98d2a3 100644 --- a/frontend/src/pages/LlmLogs.tsx +++ b/frontend/src/pages/LlmLogs.tsx @@ -7,6 +7,7 @@ import LoadingSpinner from '~/components/ui/LoadingSpinner'; const CALL_TYPE_BADGE: Record = { search: 'bg-blue-100 text-blue-800', + classify_summarize: 'bg-purple-100 text-purple-800', classification_phase1: 'bg-purple-100 text-purple-800', classification_phase2: 'bg-purple-100 text-purple-800', rewrite: 'bg-green-100 text-green-800', @@ -94,6 +95,20 @@ const LlmLogs: Component = () => { + +
+ {t('llmLogs.articleUrl')}: + + {entry.article_url} + +
+
+
diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 909fa79..93562fe 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -296,5 +296,6 @@ export interface LlmCallLogEntry { user_prompt: string; response_body: string; duration_ms: number; + article_url: string | null; created_at: string; }