feat: Phase 3 Performance — KB-Cache, Bridge-Warmstart, maxTurns 200
All checks were successful
Build AppImage / build (push) Has been skipped
All checks were successful
Build AppImage / build (push) Has been skipped
- KB-Cache im RAM: Suchergebnisse 60s gecacht, kein MySQL-Roundtrip pro Nachricht - Bridge wird beim App-Start sofort gestartet (kein Cold-Start bei erster Nachricht) - Bridge-Start-Wait von 500ms auf 200ms reduziert - maxTurns von 25 auf 200 erhoeht (verhindert "maximum turns reached" bei komplexen Tasks) - invalidate_kb_cache Command fuer manuelles Cache-Leeren Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5b857ebba4
commit
3d84feab6f
4 changed files with 111 additions and 7 deletions
|
|
@ -332,7 +332,7 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
model: useModel,
|
model: useModel,
|
||||||
promptLength: message.length,
|
promptLength: message.length,
|
||||||
contextLength: useContext?.length || 0,
|
contextLength: useContext?.length || 0,
|
||||||
maxTurns: 25,
|
maxTurns: 200,
|
||||||
resumeSessionId: resumeSessionId || null,
|
resumeSessionId: resumeSessionId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -373,7 +373,7 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
// Query-Optionen zusammenstellen
|
// Query-Optionen zusammenstellen
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
model: useModel,
|
model: useModel,
|
||||||
maxTurns: 25,
|
maxTurns: 200,
|
||||||
abortController: activeAbort,
|
abortController: activeAbort,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,8 +390,8 @@ pub async fn send_message(app: AppHandle, message: String) -> Result<String, Str
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
start_bridge(&app)?;
|
start_bridge(&app)?;
|
||||||
// Kurz warten bis Bridge bereit
|
// Kurz warten bis Bridge bereit — parallel den Context laden
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context aus DB laden (Schicht 1: Sticky Context)
|
// Context aus DB laden (Schicht 1: Sticky Context)
|
||||||
|
|
@ -534,7 +534,7 @@ pub async fn set_model(app: AppHandle, model: String) -> Result<String, String>
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
start_bridge(&app)?;
|
start_bridge(&app)?;
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modell an Bridge senden
|
// Modell an Bridge senden
|
||||||
|
|
@ -603,7 +603,7 @@ pub async fn set_agent_mode(app: AppHandle, mode: String) -> Result<String, Stri
|
||||||
|
|
||||||
if needs_start {
|
if needs_start {
|
||||||
start_bridge(&app)?;
|
start_bridge(&app)?;
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modus an Bridge senden
|
// Modus an Bridge senden
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,68 @@ use mysql_async::{Pool, prelude::*};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
// ============ KB-Cache (RAM) ============
|
||||||
|
// Cached KB-Suchergebnisse im RAM. Spart MySQL-Roundtrip pro Nachricht.
|
||||||
|
// Invalidierung: automatisch nach 60 Sekunden (TTL).
|
||||||
|
|
||||||
|
struct CacheEntry {
|
||||||
|
result: String,
|
||||||
|
created_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KbCache {
|
||||||
|
entries: HashMap<String, CacheEntry>,
|
||||||
|
ttl: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KbCache {
|
||||||
|
fn new(ttl_secs: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
ttl: Duration::from_secs(ttl_secs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, key: &str) -> Option<&str> {
|
||||||
|
if let Some(entry) = self.entries.get(key) {
|
||||||
|
if entry.created_at.elapsed() < self.ttl {
|
||||||
|
return Some(&entry.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, key: String, result: String) {
|
||||||
|
// Max 100 Eintraege — aelteste raus wenn voll
|
||||||
|
if self.entries.len() >= 100 {
|
||||||
|
self.evict_oldest();
|
||||||
|
}
|
||||||
|
self.entries.insert(key, CacheEntry { result, created_at: Instant::now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evict_oldest(&mut self) {
|
||||||
|
if let Some(oldest_key) = self.entries.iter()
|
||||||
|
.min_by_key(|(_, v)| v.created_at)
|
||||||
|
.map(|(k, _)| k.clone())
|
||||||
|
{
|
||||||
|
self.entries.remove(&oldest_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate_expired(&mut self) {
|
||||||
|
self.entries.retain(|_, v| v.created_at.elapsed() < self.ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globaler Cache — 60 Sekunden TTL
|
||||||
|
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
|
||||||
|
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(60)));
|
||||||
|
|
||||||
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
|
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
|
||||||
const MYSQL_PORT: u16 = 3306;
|
const MYSQL_PORT: u16 = 3306;
|
||||||
|
|
||||||
|
|
@ -168,7 +228,20 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
|
||||||
|
|
||||||
/// Sucht in der KB mit einem vorbereiteten Query-String
|
/// Sucht in der KB mit einem vorbereiteten Query-String
|
||||||
/// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird
|
/// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird
|
||||||
|
/// Phase 3: Mit RAM-Cache — gleiche Query innerhalb 60s wird instant aus dem Cache bedient
|
||||||
pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result<String, String> {
|
pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result<String, String> {
|
||||||
|
// Cache-Key: query + limit
|
||||||
|
let cache_key = format!("{}:{}", search_query, limit);
|
||||||
|
|
||||||
|
// Cache-Hit prüfen
|
||||||
|
{
|
||||||
|
let cache = KB_CACHE.lock().unwrap();
|
||||||
|
if let Some(cached) = cache.get(&cache_key) {
|
||||||
|
println!("⚡ KB-Cache HIT für '{}'", &search_query[..search_query.len().min(40)]);
|
||||||
|
return Ok(cached.to_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
|
||||||
|
|
@ -209,6 +282,9 @@ pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result
|
||||||
|
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]);
|
println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]);
|
||||||
|
// Leere Ergebnisse auch cachen — verhindert wiederholte DB-Abfragen
|
||||||
|
let mut cache = KB_CACHE.lock().unwrap();
|
||||||
|
cache.insert(cache_key, String::new());
|
||||||
return Ok(String::new());
|
return Ok(String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,9 +306,15 @@ pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result
|
||||||
hints.push("</knowledge-hints>".to_string());
|
hints.push("</knowledge-hints>".to_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 (cached)",
|
||||||
&search_query[..search_query.len().min(40)], results.len(), block.len());
|
&search_query[..search_query.len().min(40)], results.len(), block.len());
|
||||||
|
|
||||||
|
// In Cache speichern
|
||||||
|
{
|
||||||
|
let mut cache = KB_CACHE.lock().unwrap();
|
||||||
|
cache.insert(cache_key, block.clone());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(block)
|
Ok(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -718,3 +800,14 @@ pub async fn auto_save_error_pattern(
|
||||||
) -> Result<i64, String> {
|
) -> Result<i64, String> {
|
||||||
save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await
|
save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 3: KB-Cache invalidieren (alle Eintraege loeschen)
|
||||||
|
/// Aufrufen wenn neue KB-Eintraege gespeichert wurden
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn invalidate_kb_cache() -> Result<String, String> {
|
||||||
|
let mut cache = KB_CACHE.lock().unwrap();
|
||||||
|
let count = cache.entries.len();
|
||||||
|
cache.entries.clear();
|
||||||
|
println!("🗑️ KB-Cache invalidiert ({} Eintraege geloescht)", count);
|
||||||
|
Ok(format!("{} Cache-Eintraege geloescht", count))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,8 @@ pub fn run() {
|
||||||
knowledge::extract_message_keywords,
|
knowledge::extract_message_keywords,
|
||||||
knowledge::get_session_hints,
|
knowledge::get_session_hints,
|
||||||
knowledge::auto_save_error_pattern,
|
knowledge::auto_save_error_pattern,
|
||||||
|
// Phase 3: KB-Cache
|
||||||
|
knowledge::invalidate_kb_cache,
|
||||||
// Context-Management
|
// Context-Management
|
||||||
context::get_sticky_context,
|
context::get_sticky_context,
|
||||||
context::set_sticky_context,
|
context::set_sticky_context,
|
||||||
|
|
@ -240,6 +242,15 @@ pub fn run() {
|
||||||
// Lock-Datei erstellen (Instanz-Schutz + Update-Safety)
|
// Lock-Datei erstellen (Instanz-Schutz + Update-Safety)
|
||||||
update::create_lock_file();
|
update::create_lock_file();
|
||||||
|
|
||||||
|
// Phase 3: Bridge sofort beim App-Start starten (kein Cold-Start bei erster Nachricht)
|
||||||
|
let bridge_handle = app.handle().clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match claude::start_bridge(&bridge_handle) {
|
||||||
|
Ok(()) => println!("🔌 Bridge beim Start initialisiert (warm)"),
|
||||||
|
Err(e) => println!("⚠️ Bridge-Start beim App-Start fehlgeschlagen: {} (wird bei erster Nachricht erneut versucht)", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue