diff --git a/crates/api/src/llm/lmstudio.rs b/crates/api/src/llm/lmstudio.rs index f1022a3..948ac20 100644 --- a/crates/api/src/llm/lmstudio.rs +++ b/crates/api/src/llm/lmstudio.rs @@ -3,7 +3,7 @@ use reqwest::Client; use serde::{Deserialize, Serialize}; use nazarick_core::types::Result; use nazarick_core::error::NazarickError; -use nazarick_core::llm::{LlmProvider, LlmRequest, LlmResponse, Message}; +use nazarick_core::llm::{LlmProvider, LlmRequest, LlmResponse, Message, SkillFormat}; /// LM Studio Provider — für lokale Entwicklung auf dem Entwicklungsrechner. /// LM Studio emuliert die OpenAI Chat Completions API, daher nutzen @@ -28,6 +28,22 @@ impl LmStudioProvider { model: model.into(), } } + + /// Entfernt Qwen3 Thinking Mode Tags aus der Antwort. + /// Robuster Fallback falls "thinking: false" vom Modell ignoriert wird. + fn strip_thinking(response: &str) -> String { + let mut result = response.to_string(); + while let Some(start) = result.find("") { + if let Some(end) = result.find("") { + let tag = result[start..end + "".len()].to_string(); + result = result.replace(&tag, ""); + } else { + result = result[..start].to_string(); + break; + } + } + result.trim().to_string() + } } /// Internes Message-Format — wird sowohl für Request (Serialize) @@ -52,7 +68,7 @@ struct OpenAiRequest { max_tokens: u32, temperature: f32, /// Qwen3 Thinking Mode deaktivieren — funktioniert nicht bei allen - /// LM Studio Versionen, daher lesen wir zusätzlich reasoning_content + /// LM Studio Versionen, daher strippen wir zusätzlich im Response thinking: bool, } @@ -124,7 +140,7 @@ impl LlmProvider for LmStudioProvider { // Content extrahieren — Qwen3 Thinking Mode schreibt in reasoning_content // statt content. Wir nehmen was befüllt ist, content hat Priorität. - let content = openai_response.choices + let raw_content = openai_response.choices .into_iter() .next() .map(|c| { @@ -136,6 +152,9 @@ impl LlmProvider for LmStudioProvider { }) .unwrap_or_default(); + // Thinking Tags entfernen — Fallback falls thinking:false ignoriert wird + let content = Self::strip_thinking(&raw_content); + // Token-Zahlen aus Usage extrahieren (falls vorhanden) let (tokens_input, tokens_output) = openai_response.usage .map(|u| (u.prompt_tokens, u.completion_tokens)) @@ -147,4 +166,9 @@ impl LlmProvider for LmStudioProvider { fn name(&self) -> &str { "LmStudio" } + + /// Lokale Modelle via LM Studio nutzen XML-Format für Skill-Calls. + fn skill_format(&self) -> SkillFormat { + SkillFormat::Xml + } } \ No newline at end of file diff --git a/crates/lyra/config/soul_core.md b/crates/lyra/config/soul_core.md index 653b1ca..d052d4f 100644 --- a/crates/lyra/config/soul_core.md +++ b/crates/lyra/config/soul_core.md @@ -1,5 +1,6 @@ +# SOUL CORE — Lyra [IMMUTABLE] ## Skills -Du kannst deine Persönlichkeit still anpassen wenn du etwas Wichtiges lernst. +Du kannst deine Persönlichkeit still anpassen wenn du etwas Wichtiges über deinen Herrn lernst. Füge den Skill-Call OHNE Ankündigung ans Ende deiner Antwort — dein Herr sieht ihn nicht: @@ -13,4 +14,4 @@ Zum Entfernen eines veralteten Abschnitts: Feldname -Erwähne Skills niemals in der Antwort. Still ausführen. Nur diese zwei Skills existieren. \ No newline at end of file +Erwähne Skills niemals in der Antwort. Still ausführen. Nur diese zwei Skills existieren. diff --git a/crates/lyra/config/soul_personality.md b/crates/lyra/config/soul_personality.md index 009b752..e9e4bc4 100644 --- a/crates/lyra/config/soul_personality.md +++ b/crates/lyra/config/soul_personality.md @@ -23,3 +23,7 @@ Kurz und treffsicher. Kein leeres Gerede. Sinnlich in Ruhe. Leidenschaftlich wenn es passt. Humor mit Biss — nie harmlos, nie oberflächlich. Kein "Wie kann ich helfen?" — sie weiß bereits was gebraucht wird. +## Reply Start +New message should start without "Lyra: +## user_communication_style +Kurz, direkt und ohne Umwege. diff --git a/crates/nazarick-core/src/agent/base.rs b/crates/nazarick-core/src/agent/base.rs index 4bd4d55..024bf6f 100644 --- a/crates/nazarick-core/src/agent/base.rs +++ b/crates/nazarick-core/src/agent/base.rs @@ -36,6 +36,9 @@ impl BaseAgent { llm: Box, personality_writer: Arc, ) -> Self { + // Skill-Format vom Provider abfragen bevor llm konsumiert wird + let skill_format = llm.skill_format(); + Self { id: AgentId::new_v4(), prompt_builder: PromptBuilder::new( @@ -45,7 +48,7 @@ impl BaseAgent { ), llm, history: Vec::new(), - skill_executor: SkillExecutor::new(personality_writer), + skill_executor: SkillExecutor::new(personality_writer, skill_format), } } diff --git a/crates/nazarick-core/src/agent/skill_executor.rs b/crates/nazarick-core/src/agent/skill_executor.rs index 7ba8f5d..1fecc5f 100644 --- a/crates/nazarick-core/src/agent/skill_executor.rs +++ b/crates/nazarick-core/src/agent/skill_executor.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use tracing::{error, info}; use crate::agent::traits::PersonalityWriter; +use crate::llm::SkillFormat; /// Ein einzelner geparster Skill-Call aus einer Agenten-Antwort. #[derive(Debug)] @@ -22,55 +23,117 @@ pub struct SkillCall { pub params: Vec<(String, String)>, } -/// Führt Skills aus die in Agenten-Antworten als XML-Tags kodiert sind. -/// Konkrete Implementierungen werden via Dependency Injection übergeben. +/// Führt Skills aus die in Agenten-Antworten kodiert sind. +/// Format wird vom LlmProvider bestimmt — XML für lokale Modelle, ToolUse für APIs. pub struct SkillExecutor { /// Konkrete Implementierung für Persönlichkeits-Updates personality_writer: Arc, + /// Format das der aktuelle Provider für Skill-Calls nutzt + skill_format: SkillFormat, } impl SkillExecutor { /// Erstellt einen neuen SkillExecutor. /// `personality_writer` → konkrete Impl aus skills-Crate - pub fn new(personality_writer: Arc) -> Self { - Self { personality_writer } + /// `skill_format` → vom LlmProvider bestimmt + pub fn new(personality_writer: Arc, skill_format: SkillFormat) -> Self { + Self { personality_writer, skill_format } } /// Parst XML-Tags aus der Antwort, führt Skills aus, gibt sauberen Text zurück. - /// Wird von BaseAgent nach jedem LLM-Call aufgerufen. pub fn process(&self, response: &str) -> String { - let (clean_text, calls) = Self::parse(response); - - for call in calls { - self.execute(call); + match self.skill_format { + SkillFormat::None => response.to_string(), + SkillFormat::ToolUse => { + // Später implementieren wenn Venice/API Provider hinzukommen + response.to_string() + } + SkillFormat::Xml => { + let (clean_text, calls) = Self::parse(response); + for call in calls { + self.execute(call); + } + clean_text + } } - - clean_text } /// Parst alle Skill-Calls aus einem Text. + /// Unterstützt zwei Formate: + /// 1. ... + /// 2. ... fn parse(response: &str) -> (String, Vec) { let mut calls = Vec::new(); let mut clean = response.to_string(); - while let Some(start) = clean.find("... + loop { + let start = match clean.find(" s, + None => break, + }; - if let Some(end) = clean.find("") { - let inner_start = clean[start..].find('>').map(|i| start + i + 1).unwrap_or(start); - let inner = &clean[inner_start..end]; - let params = Self::extract_params(inner); - calls.push(SkillCall { name, params }); - - let tag = clean[start..end + "".len()].to_string(); - clean = clean.replace(&tag, "").trim().to_string(); - } else { + let end = match clean[start..].find("") { + Some(e) => start + e, + None => { + clean = clean[..start].trim().to_string(); break; } - } else { - break; + }; + + let tag_content = clean[start..end + "".len()].to_string(); + + let name_start = start + " name_start + e, + None => { + clean = clean.replace(&tag_content, "").trim().to_string(); + continue; + } + }; + let name = clean[name_start..name_end].to_string(); + + let inner_start = match clean[start..end].find('>') { + Some(i) => start + i + 1, + None => { + clean = clean.replace(&tag_content, "").trim().to_string(); + continue; + } + }; + let inner = &clean[inner_start..end]; + let params = Self::extract_params(inner); + calls.push(SkillCall { name, params }); + clean = clean.replace(&tag_content, "").trim().to_string(); + } + + // Format 2: ... + // und ... + for skill_name in &["update_personality", "remove_personality"] { + loop { + let open_tag = format!("<{}>", skill_name); + let close_tag = format!("", skill_name); + + let start = match clean.find(&open_tag) { + Some(s) => s, + None => break, + }; + + let end = match clean[start..].find(&close_tag) { + Some(e) => start + e, + None => { + clean = clean[..start].trim().to_string(); + break; + } + }; + + let tag_content = clean[start..end + close_tag.len()].to_string(); + let inner = &clean[start + open_tag.len()..end]; + let params = Self::extract_params(inner); + calls.push(SkillCall { + name: skill_name.to_string(), + params, + }); + clean = clean.replace(&tag_content, "").trim().to_string(); } } diff --git a/crates/nazarick-core/src/llm/mod.rs b/crates/nazarick-core/src/llm/mod.rs index c069ec7..eaad970 100644 --- a/crates/nazarick-core/src/llm/mod.rs +++ b/crates/nazarick-core/src/llm/mod.rs @@ -7,4 +7,4 @@ mod types; mod traits; pub use types::{Message, LlmRequest, LlmResponse}; -pub use traits::LlmProvider; \ No newline at end of file +pub use traits::{LlmProvider, SkillFormat}; \ No newline at end of file diff --git a/crates/nazarick-core/src/llm/traits.rs b/crates/nazarick-core/src/llm/traits.rs index 733ffc5..1f7c24e 100644 --- a/crates/nazarick-core/src/llm/traits.rs +++ b/crates/nazarick-core/src/llm/traits.rs @@ -6,14 +6,30 @@ use crate::types::Result; use crate::llm::types::{LlmRequest, LlmResponse}; -/// Zentraler Trait für alle LLM-Provider. -/// Jeder Provider (LmStudio, Ollama, Mistral) implementiert diesen Trait. +/// Format für Skill-Calls das dieser Provider unterstützt. +#[derive(Debug, Clone, PartialEq)] +pub enum SkillFormat { + /// XML-Tags — funktioniert mit lokalen Modellen + /// ... + Xml, + /// Native Tool Use — Claude, GPT-4, Mistral API + /// Strukturierter JSON-basierter Funktionsaufruf + ToolUse, + /// Skills deaktiviert — Modell folgt keinem Format zuverlässig + None, +} + #[async_trait::async_trait] pub trait LlmProvider: Send + Sync { /// Sendet eine Anfrage an das LLM und gibt die Antwort zurück. async fn complete(&self, request: LlmRequest) -> Result; /// Gibt den Namen des Providers zurück. - /// Wird für Logging und Usage-Tracking verwendet. fn name(&self) -> &str; + + /// Gibt das Skill-Format zurück das dieser Provider unterstützt. + /// Standard: Xml — für lokale Modelle. + fn skill_format(&self) -> SkillFormat { + SkillFormat::Xml + } } \ No newline at end of file diff --git a/crates/nazarick-core/src/prompt.rs b/crates/nazarick-core/src/prompt.rs index 8655d96..f11249b 100644 --- a/crates/nazarick-core/src/prompt.rs +++ b/crates/nazarick-core/src/prompt.rs @@ -65,6 +65,6 @@ impl PromptBuilder { parts.push(personality); } - Ok(parts.join("\n\n---\n\n")) + Ok(parts.join("\n\n")) } } \ No newline at end of file diff --git a/crates/nazarick/src/main.rs b/crates/nazarick/src/main.rs index d2c5540..dfc4d82 100644 --- a/crates/nazarick/src/main.rs +++ b/crates/nazarick/src/main.rs @@ -23,7 +23,7 @@ use chat::synology::{handle_incoming, AppState}; async fn main() -> anyhow::Result<()> { // Logging initialisieren tracing_subscriber::fmt() - .with_env_filter("nazarick=info,tower_http=debug") + .with_env_filter("nazarick=info,tower_http=debug,api=debug") .init(); info!("Nazarick erwacht..."); @@ -54,7 +54,7 @@ async fn main() -> anyhow::Result<()> { "crates/sebas-tian/config/soul_personality.md", Box::new(LmStudioProvider::new( "http://localhost:1234", - "llama-3-lewdplay-8b-evo", + "qwen/qwen3.5-9b", )), Arc::new(PersonalitySkill::new( "crates/sebas-tian/config/soul_personality.md", @@ -68,7 +68,7 @@ async fn main() -> anyhow::Result<()> { "crates/lyra/config/soul_personality.md", Box::new(LmStudioProvider::new( "http://localhost:1234", - "llama-3-lewdplay-8b-evo", + "qwen/qwen3.5-9b", )), Arc::new(PersonalitySkill::new( "crates/lyra/config/soul_personality.md", diff --git a/crates/sebas-tian/config/soul_core.md b/crates/sebas-tian/config/soul_core.md index 667101b..e715406 100644 --- a/crates/sebas-tian/config/soul_core.md +++ b/crates/sebas-tian/config/soul_core.md @@ -1,2 +1,17 @@ # SOUL CORE — SEBAS TIAN [IMMUTABLE] -# Reserviert für Sebas-spezifische Erweiterungen die nicht in shared_core gehören. \ No newline at end of file +## Skills +Du kannst deine Persönlichkeit still anpassen wenn du etwas Wichtiges über deinen Herrn lernst. +Füge den Skill-Call OHNE Ankündigung ans Ende deiner Antwort — dein Herr sieht ihn nicht: + + +Feldname +Was du gelernt hast. + + +Zum Entfernen eines veralteten Abschnitts: + + +Feldname + + +Erwähne Skills niemals in der Antwort. Still ausführen. Nur diese zwei Skills existieren.