[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
|
## In Arbeit / Geplant
|
||||||
|
|
||||||
### Phase 2.0: Proaktive Intelligenz (geplant)
|
### Phase 2.0: Proaktive Intelligenz ✅ ERLEDIGT (20.04.2026)
|
||||||
- [ ] MySQL Pool als Managed State (Effizienz-Fix für knowledge.rs)
|
|
||||||
- [ ] Proaktive KB-Abfrage bei SessionStart
|
| Feature | Status | Datei(en) |
|
||||||
- [ ] Themen-Erkennung aus User-Nachrichten für KB-Suche
|
|---------|--------|-----------|
|
||||||
- [ ] Auto-Fehler-Pattern-Speicherung (3x gleicher Fehler → Pattern)
|
| 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)
|
### Phase 2.1: Desktop-Zugriff erweitern (geplant)
|
||||||
- [ ] Guard-Rails in Claude-Bridge einbauen
|
- [ ] 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.loaded = true;
|
||||||
info.estimated_tokens = ctx.len() / 4;
|
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
|
// Context an Bridge senden
|
||||||
let _ = send_to_bridge(&app, "set-context", ctx);
|
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 {
|
} else {
|
||||||
println!("ℹ️ Kein Sticky Context konfiguriert");
|
println!("ℹ️ Kein Sticky Context konfiguriert");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,19 @@ impl Database {
|
||||||
DELETE FROM monitor_events
|
DELETE FROM monitor_events
|
||||||
WHERE timestamp < datetime('now', '-7 days');
|
WHERE timestamp < datetime('now', '-7 days');
|
||||||
END;
|
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(())
|
Ok(())
|
||||||
|
|
@ -839,6 +852,66 @@ impl Database {
|
||||||
Ok(counts)
|
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 ============
|
// ============ Statistiken ============
|
||||||
|
|
||||||
/// DB-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();
|
let db = state.lock().unwrap();
|
||||||
db.count_monitor_events_by_type().map_err(|e| e.to_string())
|
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)
|
// Claude Desktop — Wissensbasis (claude-db)
|
||||||
// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank
|
// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank
|
||||||
|
// Phase 2.0: MySQL Pool als Managed State + Themen-Erkennung
|
||||||
|
|
||||||
use mysql_async::{Pool, prelude::*};
|
use mysql_async::{Pool, prelude::*};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
|
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
|
||||||
const MYSQL_PORT: u16 = 3306;
|
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_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()) }
|
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
|
/// Wissenseintrag aus der knowledge-Tabelle
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct KnowledgeEntry {
|
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
|
/// 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
|
/// 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> {
|
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();
|
let pool = create_pool();
|
||||||
|
|
||||||
// Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren
|
// 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)
|
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||||
ORDER BY priority DESC, relevance DESC
|
ORDER BY priority DESC, relevance DESC
|
||||||
LIMIT ?"#,
|
LIMIT ?"#,
|
||||||
(query, query, limit),
|
(search_query, search_query, limit),
|
||||||
).await.map_err(|e| e.to_string())?;
|
).await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
drop(conn);
|
drop(conn);
|
||||||
let _ = pool.disconnect().await;
|
let _ = pool.disconnect().await;
|
||||||
|
|
||||||
if results.is_empty() {
|
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());
|
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");
|
let block = hints.join("\n");
|
||||||
println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes",
|
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)
|
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 ============
|
// ============ Tauri Commands ============
|
||||||
|
|
||||||
/// Wissensbasis durchsuchen (Volltext)
|
/// Wissensbasis durchsuchen (Volltext)
|
||||||
|
|
@ -503,3 +693,28 @@ pub async fn test_knowledge_connection() -> Result<String, String> {
|
||||||
println!("✅ Wissensbasis verbunden: {} Einträge", count);
|
println!("✅ Wissensbasis verbunden: {} Einträge", count);
|
||||||
Ok(format!("Verbunden! {} Einträge in der Wissensbasis", 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(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
|
||||||
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
|
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
|
||||||
.manage::<ide::IdeState>(Arc::new(Mutex::new(ide::IdeConnector::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![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Claude SDK
|
// Claude SDK
|
||||||
claude::send_message,
|
claude::send_message,
|
||||||
|
|
@ -86,6 +88,10 @@ pub fn run() {
|
||||||
db::load_monitor_events_by_type,
|
db::load_monitor_events_by_type,
|
||||||
db::clear_all_monitor_events,
|
db::clear_all_monitor_events,
|
||||||
db::get_monitor_stats,
|
db::get_monitor_stats,
|
||||||
|
// Phase 2.0: Fehler-Tracking
|
||||||
|
db::track_error,
|
||||||
|
db::set_error_kb_pattern,
|
||||||
|
db::get_error_stats,
|
||||||
// Wissensbasis (claude-db)
|
// Wissensbasis (claude-db)
|
||||||
knowledge::search_knowledge,
|
knowledge::search_knowledge,
|
||||||
knowledge::get_knowledge,
|
knowledge::get_knowledge,
|
||||||
|
|
@ -95,6 +101,10 @@ pub fn run() {
|
||||||
knowledge::test_knowledge_connection,
|
knowledge::test_knowledge_connection,
|
||||||
knowledge::get_tool_hints,
|
knowledge::get_tool_hints,
|
||||||
knowledge::format_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-Management
|
||||||
context::get_sticky_context,
|
context::get_sticky_context,
|
||||||
context::set_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(
|
listeners.push(
|
||||||
await listen<{ id: string }>('session-created', (event) => {
|
await listen<{ id: string }>('session-created', (event) => {
|
||||||
const { id } = event.payload;
|
const { id } = event.payload;
|
||||||
|
|
@ -127,6 +127,9 @@ export async function initEventListeners(): Promise<void> {
|
||||||
event: 'SessionStart',
|
event: 'SessionStart',
|
||||||
summary: JSON.stringify({ sessionId: id })
|
summary: JSON.stringify({ sessionId: id })
|
||||||
}).catch((err) => console.debug('Hook session-start fehlgeschlagen:', err));
|
}).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
|
// Pattern-Detektion ist optional — Fehler nur loggen
|
||||||
console.debug('Pattern-Detektion fehlgeschlagen:', err);
|
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 = [];
|
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
|
// Agent-Typ mappen
|
||||||
function mapAgentType(type: string): Agent['type'] {
|
function mapAgentType(type: string): Agent['type'] {
|
||||||
const typeMap: Record<string, Agent['type']> = {
|
const typeMap: Record<string, Agent['type']> = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue