[appimage] Phase 2.0: Proaktive Intelligenz
All checks were successful
Build AppImage / build (push) Successful in 8m19s
All checks were successful
Build AppImage / build (push) Successful in 8m19s
- MySQL Pool als Managed State (MysqlPoolState in lib.rs) - Keyword-Extraktion aus User-Nachrichten (Stoppwort-Filter DE/EN) - Proaktive KB-Abfrage bei SessionStart (proactive_session_hints) - Auto-Fehler-Pattern: error_tracker Tabelle, bei 3+ Occurrences automatisch KB-Eintrag in Kategorie 'fehler' erstellen - 6 neue Tauri-Commands für KB-Hints und Error-Tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d315f421ec
commit
2de88a2a22
6 changed files with 476 additions and 11 deletions
21
ROADMAP.md
21
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
|
||||
|
|
|
|||
|
|
@ -665,7 +665,38 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(ref ctx) = context {
|
||||
// Phase 2.0: Proaktive KB-Hints bei Session-Start laden
|
||||
// Projekt-Name aus Sticky Context extrahieren falls vorhanden
|
||||
let project_name = if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||||
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<StickyContextInfo, St
|
|||
// Context an Bridge senden
|
||||
let _ = send_to_bridge(&app, "set-context", ctx);
|
||||
|
||||
println!("✅ Sticky Context geladen: {} Einträge, ~{} Token", info.entries, info.estimated_tokens);
|
||||
println!("✅ Sticky Context geladen: {} Einträge, ~{} Token (inkl. proaktive Hints)", info.entries, info.estimated_tokens);
|
||||
} else {
|
||||
println!("ℹ️ Kein Sticky Context konfiguriert");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,6 +204,19 @@ impl Database {
|
|||
DELETE FROM monitor_events
|
||||
WHERE timestamp < datetime('now', '-7 days');
|
||||
END;
|
||||
|
||||
-- Phase 2.0: Fehler-Tracking für Auto-Pattern-Erkennung
|
||||
CREATE TABLE IF NOT EXISTS error_tracker (
|
||||
error_hash TEXT PRIMARY KEY,
|
||||
error_message TEXT NOT NULL,
|
||||
tool TEXT NOT NULL,
|
||||
occurrence_count INTEGER DEFAULT 1,
|
||||
first_seen TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
kb_pattern_id INTEGER,
|
||||
UNIQUE(error_hash)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_error_tracker_count ON error_tracker(occurrence_count DESC);
|
||||
",
|
||||
)?;
|
||||
Ok(())
|
||||
|
|
@ -839,6 +852,66 @@ impl Database {
|
|||
Ok(counts)
|
||||
}
|
||||
|
||||
// ============ Phase 2.0: Fehler-Tracking ============
|
||||
|
||||
/// Fehler-Occurrence zählen und zurückgeben
|
||||
/// Gibt (neuer_count, error_message, tool, kb_pattern_id) zurück
|
||||
pub fn track_error(&self, error_hash: &str, error_message: &str, tool: &str) -> SqlResult<(i32, Option<i64>)> {
|
||||
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<i64>) = 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<Vec<(String, String, String, i32, Option<i64>)>> {
|
||||
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::<SqlResult<Vec<_>>>()?;
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
// ============ Statistiken ============
|
||||
|
||||
/// DB-Statistiken
|
||||
|
|
@ -1043,3 +1116,42 @@ pub async fn get_monitor_stats(app: AppHandle) -> Result<Vec<(String, usize)>, 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<i64>), String> {
|
||||
let state = app.state::<DbState>();
|
||||
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::<DbState>();
|
||||
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<usize>,
|
||||
) -> Result<Vec<(String, String, String, i32, Option<i64>)>, String> {
|
||||
let limit = limit.unwrap_or(20);
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.get_error_stats(limit).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Option<Pool>>;
|
||||
|
||||
/// 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::<MysqlPoolState>() {
|
||||
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<String> {
|
||||
let words: Vec<String> = 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<String> = 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<String, String> {
|
||||
// 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<String, String> {
|
||||
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<String
|
|||
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
ORDER BY priority DESC, relevance DESC
|
||||
LIMIT ?"#,
|
||||
(query, query, limit),
|
||||
(search_query, search_query, limit),
|
||||
).await.map_err(|e| e.to_string())?;
|
||||
|
||||
drop(conn);
|
||||
let _ = pool.disconnect().await;
|
||||
|
||||
if results.is_empty() {
|
||||
println!("🔍 KB-Hints für '{}': keine Treffer", &query[..query.len().min(40)]);
|
||||
println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]);
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +231,102 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
|
|||
|
||||
let block = hints.join("\n");
|
||||
println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes",
|
||||
&query[..query.len().min(40)], results.len(), block.len());
|
||||
&search_query[..search_query.len().min(40)], results.len(), block.len());
|
||||
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Phase 2.0: Proaktive KB-Abfrage bei SessionStart
|
||||
/// Lädt relevante Einträge basierend auf Projekt-Kontext und letzter Aktivität
|
||||
pub async fn proactive_session_hints(project_name: Option<&str>) -> Result<String, String> {
|
||||
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<i64, String> {
|
||||
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<i64> = 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<String, String> {
|
|||
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<Vec<String>, String> {
|
||||
Ok(extract_keywords(&message))
|
||||
}
|
||||
|
||||
/// Proaktive KB-Hints bei Session-Start laden
|
||||
#[tauri::command]
|
||||
pub async fn get_session_hints(project_name: Option<String>) -> Result<String, String> {
|
||||
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<i64, String> {
|
||||
save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ pub fn run() {
|
|||
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
|
||||
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
|
||||
.manage::<ide::IdeState>(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::MysqlPoolState>(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,
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export async function initEventListeners(): Promise<void> {
|
|||
})
|
||||
);
|
||||
|
||||
// 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<void> {
|
|||
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<void> {
|
|||
// 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<void> {
|
|||
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, '<PATH>') // Pfade
|
||||
.replace(/[0-9a-f]{8}-[0-9a-f]{4}/gi, '<UUID>') // UUIDs
|
||||
.replace(/\d+/g, '<N>') // 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<number>('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<void> {
|
||||
try {
|
||||
const hints = await invoke<string>('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<string, Agent['type']> = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue