Lyra Agent hinzugefügt, Multi-Agent Routing, BaseAgent refactoring

This commit is contained in:
Sithies
2026-03-16 23:30:42 +01:00
parent 6fc1648939
commit 750fe1f5f6
22 changed files with 454 additions and 111 deletions
+85
View File
@@ -0,0 +1,85 @@
// nazarick-core/src/agent/base.rs
//
// BaseAgent — gemeinsame Logik für alle Agenten.
// Sebas, Lyra und zukünftige Agenten sind nur noch dünne Wrapper darum.
use std::sync::Arc;
use crate::prompt::PromptBuilder;
use crate::types::{AgentId, Result};
use crate::llm::{LlmProvider, LlmRequest, Message};
use crate::agent::skill_executor::SkillExecutor;
use crate::agent::traits::PersonalityWriter;
pub struct BaseAgent {
/// Eindeutige ID dieser Agent-Instanz
pub id: AgentId,
/// Baut den System-Prompt aus soul_core + soul_personality
prompt_builder: PromptBuilder,
/// Das LLM-Backend (LmStudio, Ollama, Mistral)
llm: Box<dyn LlmProvider>,
/// Konversationsverlauf — damit der Agent den Kontext behält
history: Vec<Message>,
/// Führt Skill-Calls aus die der Agent in seiner Antwort kodiert
skill_executor: SkillExecutor,
}
impl BaseAgent {
/// Erstellt eine neue BaseAgent-Instanz.
/// `shared_core_path` → Pfad zu shared_core.md (systemweit)
/// `soul_core_path` → Pfad zu soul_core.md (agenten-spezifisch)
/// `soul_personality_path` → Pfad zu soul_personality.md (veränderlich)
/// `personality_writer` → Skill-Implementierung für Persönlichkeits-Updates
pub fn new(
shared_core_path: impl Into<String>,
soul_core_path: impl Into<String>,
soul_personality_path: impl Into<String>,
llm: Box<dyn LlmProvider>,
personality_writer: Arc<dyn PersonalityWriter>,
) -> Self {
Self {
id: AgentId::new_v4(),
prompt_builder: PromptBuilder::new(
shared_core_path,
soul_core_path,
soul_personality_path,
),
llm,
history: Vec::new(),
skill_executor: SkillExecutor::new(personality_writer),
}
}
/// Sendet eine Nachricht und gibt die Antwort zurück.
/// Parst automatisch Skill-Calls aus der Antwort und führt sie aus.
/// Gibt nur den bereinigten Text zurück — keine XML-Tags.
pub async fn chat(&mut self, user_message: &str) -> Result<String> {
let system_prompt = self.prompt_builder.build()?;
self.history.push(Message::user(user_message));
let mut messages = vec![Message::system(system_prompt)];
messages.extend(self.history.clone());
let request = LlmRequest {
messages,
max_tokens: 4096,
temperature: 0.7,
};
let response = self.llm.complete(request).await?;
// Skill-Calls parsen und ausführen — sauberen Text zurückbekommen
let clean_response = self.skill_executor.process(&response.content);
// Sauberen Text zum Verlauf hinzufügen
self.history.push(Message::assistant(&clean_response));
Ok(clean_response)
}
/// Löscht den Konversationsverlauf.
/// Nützlich wenn ein neues Gespräch beginnen soll.
pub fn clear_history(&mut self) {
self.history.clear();
}
}
+12
View File
@@ -0,0 +1,12 @@
// nazarick-core/src/agent/mod.rs
//
// Agent-Modul — BaseAgent und SkillExecutor.
// Neue Agent-Funktionalität als eigenes Submodul hinzufügen.
mod base;
mod skill_executor;
mod traits;
pub use base::BaseAgent;
pub use skill_executor::SkillExecutor;
pub use traits::PersonalityWriter;
@@ -0,0 +1,133 @@
// nazarick-core/src/agent/skill_executor.rs
//
// SkillExecutor — parst XML-Tags aus Agenten-Antworten und führt Skills aus.
// Konkrete Skill-Implementierungen werden via Trait injiziert.
//
// XML-Format für Skill-Calls:
// <skill name="update_personality">
// <field>Stil</field>
// <value>Herr bevorzugt kurze Antworten.</value>
// </skill>
use std::sync::Arc;
use tracing::{error, info};
use crate::agent::traits::PersonalityWriter;
/// Ein einzelner geparster Skill-Call aus einer Agenten-Antwort.
#[derive(Debug)]
pub struct SkillCall {
/// Name des aufgerufenen Skills
pub name: String,
/// Parameter des Skill-Calls — key/value Paare
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.
pub struct SkillExecutor {
/// Konkrete Implementierung für Persönlichkeits-Updates
personality_writer: Arc<dyn PersonalityWriter>,
}
impl SkillExecutor {
/// Erstellt einen neuen SkillExecutor.
/// `personality_writer` → konkrete Impl aus skills-Crate
pub fn new(personality_writer: Arc<dyn PersonalityWriter>) -> Self {
Self { personality_writer }
}
/// 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);
}
clean_text
}
/// Parst alle Skill-Calls aus einem Text.
fn parse(response: &str) -> (String, Vec<SkillCall>) {
let mut calls = Vec::new();
let mut clean = response.to_string();
while let Some(start) = clean.find("<skill name=\"") {
let name_start = start + "<skill name=\"".len();
if let Some(name_end) = clean[name_start..].find('"') {
let name = clean[name_start..name_start + name_end].to_string();
if let Some(end) = clean.find("</skill>") {
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 + "</skill>".len()].to_string();
clean = clean.replace(&tag, "").trim().to_string();
} else {
break;
}
} else {
break;
}
}
(clean, calls)
}
/// Extrahiert key/value Parameter aus dem Inhalt eines Skill-Tags.
fn extract_params(content: &str) -> Vec<(String, String)> {
let mut params = Vec::new();
let mut remaining = content;
while let Some(open_start) = remaining.find('<') {
let tag_start = open_start + 1;
if let Some(tag_end) = remaining[tag_start..].find('>') {
let tag_name = &remaining[tag_start..tag_start + tag_end];
if tag_name.starts_with('/') {
remaining = &remaining[tag_start + tag_end + 1..];
continue;
}
let close_tag = format!("</{}>", tag_name);
if let Some(value_end) = remaining.find(&close_tag) {
let value_start = open_start + tag_name.len() + 2;
let value = remaining[value_start..value_end].trim().to_string();
params.push((tag_name.to_string(), value));
remaining = &remaining[value_end + close_tag.len()..];
} else {
break;
}
} else {
break;
}
}
params
}
/// Führt einen einzelnen Skill-Call aus.
fn execute(&self, call: SkillCall) {
match call.name.as_str() {
"update_personality" => {
let field = call.params.iter().find(|(k, _)| k == "field").map(|(_, v)| v.as_str());
let value = call.params.iter().find(|(k, _)| k == "value").map(|(_, v)| v.as_str());
match (field, value) {
(Some(f), Some(v)) => {
if let Err(e) = self.personality_writer.update(f, v) {
error!(error = %e, "Persönlichkeits-Update fehlgeschlagen");
} else {
info!(field = %f, "Persönlichkeit aktualisiert");
}
}
_ => error!("update_personality: field oder value fehlt"),
}
}
unknown => error!(skill = %unknown, "Unbekannter Skill"),
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// nazarick-core/src/agent/traits.rs
//
// Traits für Agent-Skills — Dependency Injection.
// nazarick-core definiert nur die Schnittstelle,
// skills-Crate liefert die konkrete Implementierung.
/// Schreibt Persönlichkeits-Updates in soul_personality.md.
/// Wird von SkillExecutor genutzt — konkrete Impl in skills-Crate.
pub trait PersonalityWriter: Send + Sync {
/// Aktualisiert ein Feld in soul_personality.md.
/// `field` → Abschnittsname (z.B. "Stil")
/// `value` → Neuer Inhalt des Abschnitts
fn update(&self, field: &str, value: &str) -> anyhow::Result<()>;
}