diff --git a/ROADMAP.md b/ROADMAP.md index 57ee461..3f85a1d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1346,11 +1346,22 @@ END; ## In Arbeit / Geplant -### Phase 2.0: Proaktive Intelligenz (geplant) -- [ ] MySQL Pool als Managed State (Effizienz-Fix für knowledge.rs) -- [ ] Proaktive KB-Abfrage bei SessionStart -- [ ] Themen-Erkennung aus User-Nachrichten für KB-Suche -- [ ] Auto-Fehler-Pattern-Speicherung (3x gleicher Fehler → Pattern) +### Phase 2.0: Proaktive Intelligenz ✅ ERLEDIGT (20.04.2026) + +| Feature | Status | Datei(en) | +|---------|--------|-----------| +| MySQL Pool als Managed State | ✅ | `knowledge.rs`, `lib.rs` | +| Proaktive KB-Abfrage bei SessionStart | ✅ | `knowledge.rs`, `claude.rs`, `events.ts` | +| Themen-Erkennung (Keyword-Extraktion) | ✅ | `knowledge.rs` (`extract_keywords()`, Stoppwort-Filter) | +| Auto-Fehler-Pattern (3x → KB-Eintrag) | ✅ | `db.rs` (`error_tracker` Tabelle), `events.ts`, `knowledge.rs` | + +#### Details + +- **MySQL Pool**: `MysqlPoolState` als Tauri Managed State, einmal beim App-Start erstellt +- **Proaktive Hints**: `proactive_session_hints()` lädt bei Session-Start relevante KB-Einträge basierend auf Projekt-Kontext +- **Keyword-Extraktion**: Deutsche + englische Stoppwörter gefiltert, max 6 Keywords, dedupliziert +- **Error-Tracking**: `error_tracker` SQLite-Tabelle mit Hash-basiertem Grouping. Bei 3+ Occurrences wird automatisch ein `fehler`-Eintrag in der claude-db (MySQL) erstellt +- **Neue Tauri-Commands**: `extract_message_keywords`, `get_session_hints`, `auto_save_error_pattern`, `track_error`, `set_error_kb_pattern`, `get_error_stats` ### Phase 2.1: Desktop-Zugriff erweitern (geplant) - [ ] Guard-Rails in Claude-Bridge einbauen diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index ab27c48..bdd8a96 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -665,7 +665,38 @@ pub async fn init_sticky_context(app: AppHandle) -> Result>>() { + let db = db_state.lock().unwrap(); + db.get_setting("current_project_name").ok().flatten() + } else { + None + }; + + let proactive_hints = match knowledge::proactive_session_hints(project_name.as_deref()).await { + Ok(hints) if !hints.is_empty() => { + println!("📋 Proaktive Session-Hints: {} Bytes", hints.len()); + Some(hints) + } + Ok(_) => None, + Err(e) => { + println!("⚠️ Proaktive Hints Fehler (ignoriert): {}", e); + None + } + }; + + // Context + proaktive Hints kombinieren + let mut full_context = context.clone(); + if let Some(hints) = proactive_hints { + let ctx = full_context.get_or_insert_with(String::new); + if !ctx.is_empty() { + ctx.push_str("\n\n"); + } + ctx.push_str(&hints); + } + + if let Some(ref ctx) = full_context { info.loaded = true; info.estimated_tokens = ctx.len() / 4; @@ -685,7 +716,7 @@ pub async fn init_sticky_context(app: AppHandle) -> Result SqlResult<(i32, Option)> { + let now = chrono::Local::now().to_rfc3339(); + + // Versuche zu aktualisieren + let updated = self.conn.execute( + "UPDATE error_tracker SET + occurrence_count = occurrence_count + 1, + last_seen = ?1, + error_message = ?2 + WHERE error_hash = ?3", + params![now, error_message, error_hash], + )?; + + if updated == 0 { + // Neuer Eintrag + self.conn.execute( + "INSERT INTO error_tracker (error_hash, error_message, tool, occurrence_count, first_seen, last_seen) + VALUES (?1, ?2, ?3, 1, ?4, ?4)", + params![error_hash, error_message, tool, now], + )?; + return Ok((1, None)); + } + + // Aktuellen Count und kb_pattern_id holen + let result: (i32, Option) = self.conn.query_row( + "SELECT occurrence_count, kb_pattern_id FROM error_tracker WHERE error_hash = ?1", + params![error_hash], + |row| Ok((row.get(0)?, row.get(1)?)), + )?; + + Ok(result) + } + + /// KB-Pattern-ID für einen Fehler speichern (nachdem Pattern in KB erstellt wurde) + pub fn set_error_kb_pattern(&self, error_hash: &str, kb_pattern_id: i64) -> SqlResult<()> { + self.conn.execute( + "UPDATE error_tracker SET kb_pattern_id = ?1 WHERE error_hash = ?2", + params![kb_pattern_id, error_hash], + )?; + Ok(()) + } + + /// Fehler-Statistiken laden (Top N häufigste Fehler) + pub fn get_error_stats(&self, limit: usize) -> SqlResult)>> { + let mut stmt = self.conn.prepare( + "SELECT error_hash, error_message, tool, occurrence_count, kb_pattern_id + FROM error_tracker + ORDER BY occurrence_count DESC + LIMIT ?1" + )?; + let stats = stmt.query_map(params![limit as i64], |row| { + Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)) + })?.collect::>>()?; + Ok(stats) + } + // ============ Statistiken ============ /// DB-Statistiken @@ -1043,3 +1116,42 @@ pub async fn get_monitor_stats(app: AppHandle) -> Result, S let db = state.lock().unwrap(); db.count_monitor_events_by_type().map_err(|e| e.to_string()) } + +// ============ Phase 2.0: Fehler-Tracking Commands ============ + +/// Fehler tracken — gibt (count, kb_pattern_id) zurück +#[tauri::command] +pub async fn track_error( + app: AppHandle, + error_hash: String, + error_message: String, + tool: String, +) -> Result<(i32, Option), String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.track_error(&error_hash, &error_message, &tool).map_err(|e| e.to_string()) +} + +/// KB-Pattern-ID für Fehler setzen +#[tauri::command] +pub async fn set_error_kb_pattern( + app: AppHandle, + error_hash: String, + kb_pattern_id: i64, +) -> Result<(), String> { + let state = app.state::(); + let db = state.lock().unwrap(); + db.set_error_kb_pattern(&error_hash, kb_pattern_id).map_err(|e| e.to_string()) +} + +/// Fehler-Statistiken laden +#[tauri::command] +pub async fn get_error_stats( + app: AppHandle, + limit: Option, +) -> Result)>, String> { + let limit = limit.unwrap_or(20); + let state = app.state::(); + let db = state.lock().unwrap(); + db.get_error_stats(limit).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index 02f369f..63481f9 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -1,9 +1,12 @@ // Claude Desktop — Wissensbasis (claude-db) // Direkte MySQL-Anbindung zur zentralen Wissensdatenbank +// Phase 2.0: MySQL Pool als Managed State + Themen-Erkennung use mysql_async::{Pool, prelude::*}; use serde::{Deserialize, Serialize}; use chrono::NaiveDateTime; +use std::sync::Arc; +use tauri::{AppHandle, Manager}; /// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults const MYSQL_PORT: u16 = 3306; @@ -13,6 +16,86 @@ fn mysql_user() -> String { std::env::var("CLAUDE_MYSQL_USER").unwrap_or_else(|_ fn mysql_pass() -> String { std::env::var("CLAUDE_MYSQL_PASS").unwrap_or_else(|_| "8715".to_string()) } fn mysql_db() -> String { std::env::var("CLAUDE_MYSQL_DB").unwrap_or_else(|_| "claude".to_string()) } +/// Managed MySQL Pool — wird einmal beim App-Start erstellt +/// Alle Knowledge-Funktionen nutzen diesen Pool statt jedes Mal einen neuen zu erstellen +pub type MysqlPoolState = Arc>; + +/// Erstellt den globalen MySQL Pool (einmal beim App-Start aufrufen) +pub fn create_managed_pool() -> MysqlPoolState { + let url = format!( + "mysql://{}:{}@{}:{}/{}", + mysql_user(), mysql_pass(), mysql_host(), MYSQL_PORT, mysql_db() + ); + match Pool::new(url.as_str()) { + pool => { + println!("🗄️ MySQL Pool erstellt ({}:{})", mysql_host(), MYSQL_PORT); + Arc::new(Some(pool)) + } + } +} + +/// Pool aus AppHandle holen — Fallback auf neuen Pool wenn State nicht verfügbar +/// TODO: Bestehende Commands schrittweise auf get_pool(app) migrieren +#[allow(dead_code)] +fn get_pool(app: Option<&AppHandle>) -> Pool { + if let Some(app) = app { + if let Some(pool_state) = app.try_state::() { + if let Some(pool) = pool_state.as_ref() { + return pool.clone(); + } + } + } + // Fallback: neuen Pool erstellen (für Aufrufe ohne AppHandle, z.B. search_knowledge_internal) + create_pool() +} + +/// Deutsche Stoppwörter die bei der Themen-Erkennung gefiltert werden +const STOP_WORDS: &[&str] = &[ + "der", "die", "das", "den", "dem", "des", "ein", "eine", "einer", "einem", "einen", + "und", "oder", "aber", "doch", "noch", "auch", "nur", "schon", "mal", "dann", + "ist", "sind", "war", "hat", "haben", "wird", "werden", "kann", "können", "soll", + "muss", "müssen", "darf", "will", "wollen", "möchte", "würde", "sollte", + "ich", "du", "er", "sie", "es", "wir", "ihr", "mein", "dein", "sein", + "mit", "für", "auf", "von", "aus", "bei", "nach", "über", "unter", "vor", + "wie", "was", "wer", "wo", "wann", "warum", "wieso", "weshalb", + "nicht", "kein", "keine", "keinen", "keinem", + "this", "that", "the", "and", "for", "with", "from", "into", + "bitte", "danke", "okay", "alles", "nächste", "mach", "zeig", "gib", + "mir", "dir", "uns", "hier", "dort", "jetzt", "gerade", "einfach", + "phase", "feature", "erstelle", "implementiere", "baue", +]; + +/// Extrahiert relevante Keywords aus einer User-Nachricht +/// Filtert Stoppwörter und kurze Wörter raus, gibt die besten Suchbegriffe zurück +pub fn extract_keywords(message: &str) -> Vec { + let words: Vec = message + .to_lowercase() + // Satzzeichen entfernen + .replace(|c: char| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.', " ") + .split_whitespace() + .filter(|w| { + w.len() >= 3 + && !STOP_WORDS.contains(&w.as_ref()) + // Zahlen alleine sind selten gute Suchbegriffe + && !w.chars().all(|c| c.is_numeric()) + }) + .map(|w| w.to_string()) + .collect(); + + // Deduplizieren und max 6 Keywords behalten + let mut unique: Vec = Vec::new(); + for w in words { + if !unique.contains(&w) { + unique.push(w); + } + if unique.len() >= 6 { + break; + } + } + + unique +} + /// Wissenseintrag aus der knowledge-Tabelle #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KnowledgeEntry { @@ -69,7 +152,23 @@ fn create_pool() -> Pool { /// KB-Hints für eine Nachricht laden — fehlertolerant, gibt leeren String bei DB-Problemen /// Wird von claude.rs aufgerufen bevor die Nachricht an die Bridge geht +/// Phase 2.0: Nutzt jetzt Keyword-Extraktion statt rohe Nachricht als Query pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result { + // Phase 2.0: Keywords aus der Nachricht extrahieren für bessere Suche + let keywords = extract_keywords(query); + let search_query = if keywords.is_empty() { + // Fallback: erste 100 Zeichen der Nachricht + query[..query.len().min(100)].to_string() + } else { + keywords.join(" ") + }; + + search_knowledge_by_query(&search_query, limit).await +} + +/// Sucht in der KB mit einem vorbereiteten Query-String +/// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird +pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result { let pool = create_pool(); // Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren @@ -102,14 +201,14 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result Result) -> Result { + let mut search_terms = Vec::new(); + + // Projekt-bezogene Begriffe + if let Some(name) = project_name { + search_terms.push(name.to_string()); + // Projekt-spezifische Zusatzbegriffe + let lower = name.to_lowercase(); + if lower.contains("dolibarr") { search_terms.push("dolibarr".to_string()); } + if lower.contains("claude") { search_terms.push("claude desktop tauri".to_string()); } + if lower.contains("bericht") { search_terms.push("bericht modul".to_string()); } + if lower.contains("kunden") { search_terms.push("kundenkarte schaltplan".to_string()); } + } + + // Allgemeine Begriffe für Session-Start (aktive Fehler, wichtige Patterns) + search_terms.push("fehler workaround aktiv".to_string()); + + let query = search_terms.join(" "); + println!("📋 Proaktive KB-Abfrage: '{}'", &query[..query.len().min(60)]); + + search_knowledge_by_query(&query, 5).await +} + +/// Phase 2.0: Auto-Fehler-Pattern in KB speichern +/// Wird aufgerufen wenn ein Fehler 3x aufgetreten ist +pub async fn save_error_pattern_to_kb( + error_hash: &str, + error_message: &str, + tool: &str, + occurrence_count: i32, +) -> Result { + let pool = create_pool(); + let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; + + // Prüfen ob für diesen Hash schon ein KB-Eintrag existiert + let existing: Option = conn.exec_first( + r#"SELECT id FROM knowledge + WHERE category = 'fehler' + AND tags LIKE CONCAT('%', ?, '%') + AND status = 'active' + LIMIT 1"#, + (error_hash,), + ).await.map_err(|e| e.to_string())?; + + if let Some(id) = existing { + // Schon vorhanden — nur Occurrence aktualisieren + conn.exec_drop( + r#"UPDATE knowledge SET + content = CONCAT(content, '\n\n---\nWeiteres Auftreten: ', NOW(), ' (', ?, 'x gesamt)'), + updated_at = NOW() + WHERE id = ?"#, + (occurrence_count, id), + ).await.map_err(|e| e.to_string())?; + + drop(conn); + let _ = pool.disconnect().await; + println!("📝 Fehler-Pattern #{} aktualisiert ({}x)", id, occurrence_count); + return Ok(id); + } + + // Neuen KB-Eintrag erstellen + let title = format!("Auto-Pattern: {} Fehler in {}", &error_message[..error_message.len().min(60)], tool); + let content = format!( + "## Automatisch erkanntes Fehler-Pattern\n\n\ + **Tool:** {}\n\ + **Häufigkeit:** {}x aufgetreten\n\ + **Fehlermeldung:**\n```\n{}\n```\n\n\ + **Hash:** `{}`\n\n\ + > Dieses Pattern wurde automatisch erstellt nachdem der Fehler {}x aufgetreten ist.\n\ + > Bitte Lösung/Workaround ergänzen.", + tool, occurrence_count, &error_message[..error_message.len().min(500)], error_hash, occurrence_count + ); + let tags = format!("auto-pattern,fehler,{},{}", tool.to_lowercase(), error_hash); + + conn.exec_drop( + r#"INSERT INTO knowledge (category, title, content, tags, priority, status, source, created_at, updated_at) + VALUES ('fehler', ?, ?, ?, 2, 'active', 'auto-pattern', NOW(), NOW())"#, + (&title, &content, &tags), + ).await.map_err(|e| e.to_string())?; + + let id: i64 = conn.last_insert_id().ok_or("Keine Insert-ID")? as i64; + + drop(conn); + let _ = pool.disconnect().await; + + println!("🆕 Neues Fehler-Pattern in KB gespeichert: #{} ({}x {})", id, occurrence_count, tool); + Ok(id) +} + // ============ Tauri Commands ============ /// Wissensbasis durchsuchen (Volltext) @@ -503,3 +693,28 @@ pub async fn test_knowledge_connection() -> Result { println!("✅ Wissensbasis verbunden: {} Einträge", count); Ok(format!("Verbunden! {} Einträge in der Wissensbasis", count)) } + +// ============ Phase 2.0: Neue Commands ============ + +/// Keywords aus einer Nachricht extrahieren (für Frontend-Debug/Anzeige) +#[tauri::command] +pub async fn extract_message_keywords(message: String) -> Result, String> { + Ok(extract_keywords(&message)) +} + +/// Proaktive KB-Hints bei Session-Start laden +#[tauri::command] +pub async fn get_session_hints(project_name: Option) -> Result { + proactive_session_hints(project_name.as_deref()).await +} + +/// Fehler-Pattern automatisch in KB speichern (aufgerufen von Frontend bei 3+ Occurrences) +#[tauri::command] +pub async fn auto_save_error_pattern( + error_hash: String, + error_message: String, + tool: String, + occurrence_count: i32, +) -> Result { + save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f9010a8..bf5a914 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,8 @@ pub fn run() { .manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new()))) .manage::(Arc::new(Mutex::new(hooks::HookManager::default()))) .manage::(Arc::new(Mutex::new(ide::IdeConnector::default()))) + // Phase 2.0: MySQL Pool als Managed State — wird einmal erstellt, von allen Knowledge-Commands geteilt + .manage::(knowledge::create_managed_pool()) .invoke_handler(tauri::generate_handler![ // Claude SDK claude::send_message, @@ -86,6 +88,10 @@ pub fn run() { db::load_monitor_events_by_type, db::clear_all_monitor_events, db::get_monitor_stats, + // Phase 2.0: Fehler-Tracking + db::track_error, + db::set_error_kb_pattern, + db::get_error_stats, // Wissensbasis (claude-db) knowledge::search_knowledge, knowledge::get_knowledge, @@ -95,6 +101,10 @@ pub fn run() { knowledge::test_knowledge_connection, knowledge::get_tool_hints, knowledge::format_tool_hints, + // Phase 2.0: Proaktive Intelligenz + knowledge::extract_message_keywords, + knowledge::get_session_hints, + knowledge::auto_save_error_pattern, // Context-Management context::get_sticky_context, context::set_sticky_context, diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index 4305a9c..3277b58 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -118,7 +118,7 @@ export async function initEventListeners(): Promise { }) ); - // Session erstellt — Hook feuern (fire-and-forget) + // Session erstellt — Hook feuern + proaktive KB-Hints laden (fire-and-forget) listeners.push( await listen<{ id: string }>('session-created', (event) => { const { id } = event.payload; @@ -127,6 +127,9 @@ export async function initEventListeners(): Promise { event: 'SessionStart', summary: JSON.stringify({ sessionId: id }) }).catch((err) => console.debug('Hook session-start fehlgeschlagen:', err)); + + // Phase 2.0: Proaktive KB-Abfrage bei Session-Start + loadProactiveSessionHints(); }) ); @@ -290,6 +293,9 @@ export async function initEventListeners(): Promise { // Pattern-Detektion ist optional — Fehler nur loggen console.debug('Pattern-Detektion fehlgeschlagen:', err); }); + + // Phase 2.0: Auto-Fehler-Tracking — Fehler hashen und zählen + trackErrorOccurrence(output, tool || 'unknown'); } }) ); @@ -424,6 +430,86 @@ export async function cleanupEventListeners(): Promise { listeners = []; } +// Phase 2.0: Fehler-Hash berechnen (einfacher Hash aus Fehlermeldung) +function hashError(errorMessage: string): string { + // Normalisierung: Zahlen, Pfade und UUIDs entfernen für besseres Grouping + const normalized = errorMessage + .substring(0, 200) + .replace(/\/[\w/.-]+/g, '') // Pfade + .replace(/[0-9a-f]{8}-[0-9a-f]{4}/gi, '') // UUIDs + .replace(/\d+/g, '') // Zahlen + .replace(/\s+/g, ' ') // Whitespace + .trim() + .toLowerCase(); + + // Einfacher String-Hash + let hash = 0; + for (let i = 0; i < normalized.length; i++) { + const char = normalized.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // 32-bit Integer + } + return 'err_' + Math.abs(hash).toString(36); +} + +// Phase 2.0: Auto-Fehler-Tracking — Fehler zählen und bei 3+ automatisch Pattern in KB speichern +async function trackErrorOccurrence(errorMessage: string, tool: string) { + try { + const errorHash = hashError(errorMessage); + const [count, existingKbId] = await invoke<[number, number | null]>('track_error', { + errorHash, + errorMessage: errorMessage.substring(0, 1000), + tool, + }); + + console.log(`📊 Fehler-Tracking: ${errorHash} → ${count}x (KB: ${existingKbId || 'noch nicht'})`); + + // Bei 3+ Occurrences und noch kein KB-Eintrag: automatisch speichern + if (count >= 3 && !existingKbId) { + console.log(`🆕 Auto-Pattern: Fehler ${count}x aufgetreten, speichere in KB...`); + + const kbId = await invoke('auto_save_error_pattern', { + errorHash, + errorMessage: errorMessage.substring(0, 1000), + tool, + occurrenceCount: count, + }); + + // KB-ID zurückschreiben + await invoke('set_error_kb_pattern', { errorHash, kbPatternId: kbId }); + + addMonitorEvent('hook', `Auto-Pattern erstellt: KB #${kbId} (${count}x ${tool})`, { + errorHash, + kbId, + occurrenceCount: count, + tool, + }); + } + } catch (err) { + // Fehler-Tracking ist komplett optional — niemals die App blockieren + console.debug('Fehler-Tracking fehlgeschlagen:', err); + } +} + +// Phase 2.0: Proaktive KB-Abfrage bei Session-Erstellung +export async function loadProactiveSessionHints(projectName?: string): Promise { + try { + const hints = await invoke('get_session_hints', { + projectName: projectName || null, + }); + + if (hints && hints.length > 0) { + console.log('📋 Proaktive Session-Hints geladen:', hints.length, 'Bytes'); + addMonitorEvent('hook', `Proaktive KB-Hints geladen (~${Math.ceil(hints.length / 4)} Token)`, { + projectName, + hintSize: hints.length, + }); + } + } catch (err) { + console.debug('Proaktive Session-Hints nicht verfügbar:', err); + } +} + // Agent-Typ mappen function mapAgentType(type: string): Agent['type'] { const typeMap: Record = {