Lyra Agent hinzugefügt, Multi-Agent Routing, BaseAgent refactoring
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
// nazarick-core/src/agent.rs
|
||||
// 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
|
||||
@@ -16,26 +19,39 @@ pub struct BaseAgent {
|
||||
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.
|
||||
/// Wird von jedem Agenten in seinem new() aufgerufen.
|
||||
/// `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(soul_core_path, soul_personality_path),
|
||||
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.
|
||||
/// Konversationsverlauf wird automatisch mitgeführt.
|
||||
/// 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()?;
|
||||
|
||||
@@ -52,9 +68,13 @@ impl BaseAgent {
|
||||
|
||||
let response = self.llm.complete(request).await?;
|
||||
|
||||
self.history.push(Message::assistant(&response.content));
|
||||
// Skill-Calls parsen und ausführen — sauberen Text zurückbekommen
|
||||
let clean_response = self.skill_executor.process(&response.content);
|
||||
|
||||
Ok(response.content)
|
||||
// Sauberen Text zum Verlauf hinzufügen
|
||||
self.history.push(Message::assistant(&clean_response));
|
||||
|
||||
Ok(clean_response)
|
||||
}
|
||||
|
||||
/// Löscht den Konversationsverlauf.
|
||||
@@ -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<()>;
|
||||
}
|
||||
@@ -2,15 +2,17 @@ use crate::types::Result;
|
||||
use crate::error::NazarickError;
|
||||
|
||||
/// Verantwortlich für das Zusammensetzen des System-Prompts.
|
||||
/// Liest soul_core.md und soul_personality.md und kombiniert
|
||||
/// sie in der richtigen Reihenfolge.
|
||||
///
|
||||
/// Reihenfolge ist bewusst gewählt:
|
||||
/// 1. soul_core.md IMMER zuerst — Kernregeln haben höchste Priorität
|
||||
/// 2. soul_personality.md danach — Ton und Stil
|
||||
/// So kann soul_personality niemals soul_core überschreiben.
|
||||
/// Reihenfolge ist bewusst gewählt — höchste Priorität zuerst:
|
||||
/// 1. shared_core.md — gilt für alle Agenten, Sicherheit
|
||||
/// 2. soul_core.md — agenten-spezifische Erweiterungen
|
||||
/// 3. soul_personality.md — Identität, Charakter, entwickelbarer Stil
|
||||
///
|
||||
/// Spätere Abschnitte können frühere nie überschreiben.
|
||||
pub struct PromptBuilder {
|
||||
/// Pfad zu soul_core.md — unveränderliche Kernregeln
|
||||
/// Pfad zu shared_core.md — systemweite Regeln für alle Agenten
|
||||
shared_core_path: String,
|
||||
/// Pfad zu soul_core.md — agenten-spezifische unveränderliche Regeln
|
||||
soul_core_path: String,
|
||||
/// Pfad zu soul_personality.md — entwickelbarer Persönlichkeitsteil
|
||||
soul_personality_path: String,
|
||||
@@ -19,42 +21,50 @@ pub struct PromptBuilder {
|
||||
impl PromptBuilder {
|
||||
/// Erstellt einen neuen PromptBuilder mit den angegebenen Dateipfaden.
|
||||
pub fn new(
|
||||
shared_core_path: impl Into<String>,
|
||||
soul_core_path: impl Into<String>,
|
||||
soul_personality_path: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
shared_core_path: shared_core_path.into(),
|
||||
soul_core_path: soul_core_path.into(),
|
||||
soul_personality_path: soul_personality_path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest beide soul-Dateien und kombiniert sie zum finalen System-Prompt.
|
||||
/// Fehlt soul_personality.md wird nur soul_core.md verwendet —
|
||||
/// das System bleibt funktionsfähig auch ohne Persönlichkeitsdatei.
|
||||
/// Liest alle soul-Dateien und kombiniert sie zum finalen System-Prompt.
|
||||
/// shared_core und soul_core sind Pflicht.
|
||||
/// soul_personality ist optional — graceful fallback auf leere Persönlichkeit.
|
||||
pub fn build(&self) -> Result<String> {
|
||||
// soul_core.md ist Pflicht — ohne Kernregeln kein Start
|
||||
// shared_core ist Pflicht — systemweite Sicherheitsregeln
|
||||
let shared = std::fs::read_to_string(&self.shared_core_path)
|
||||
.map_err(|e| NazarickError::Config(
|
||||
format!("shared_core.md nicht gefunden unter '{}': {}",
|
||||
self.shared_core_path, e)
|
||||
))?;
|
||||
|
||||
// soul_core ist Pflicht — agenten-spezifische Regeln
|
||||
let core = std::fs::read_to_string(&self.soul_core_path)
|
||||
.map_err(|e| NazarickError::Config(
|
||||
format!("soul_core.md nicht gefunden unter '{}': {}",
|
||||
self.soul_core_path, e)
|
||||
))?;
|
||||
|
||||
// soul_personality.md ist optional — graceful fallback
|
||||
let personality = match std::fs::read_to_string(&self.soul_personality_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
// Kein Fehler — leere Persönlichkeit ist valid beim ersten Start
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
// soul_personality ist optional — graceful fallback
|
||||
let personality = std::fs::read_to_string(&self.soul_personality_path)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Zusammensetzen: Core immer zuerst
|
||||
let system_prompt = if personality.is_empty() {
|
||||
core
|
||||
} else {
|
||||
format!("{}\n\n---\n\n{}", core, personality)
|
||||
};
|
||||
// Zusammensetzen: shared zuerst, dann core, dann personality
|
||||
let mut parts = vec![shared];
|
||||
|
||||
Ok(system_prompt)
|
||||
if !core.trim().is_empty() {
|
||||
parts.push(core);
|
||||
}
|
||||
|
||||
if !personality.trim().is_empty() {
|
||||
parts.push(personality);
|
||||
}
|
||||
|
||||
Ok(parts.join("\n\n---\n\n"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user