Lyra Agent hinzugefügt, Multi-Agent Routing, BaseAgent refactoring
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<()>;
|
||||
}
|
||||
Reference in New Issue
Block a user