upgrade Skill system auf regitry
This commit is contained in:
@@ -8,4 +8,5 @@ thiserror = "2.0.18"
|
||||
uuid = { version = "1.22.0", features = ["v4"] }
|
||||
async-trait = "0.1.89"
|
||||
tracing = "0.1.44"
|
||||
anyhow = "1.0.102"
|
||||
anyhow = "1.0.102"
|
||||
inventory = "0.3.22"
|
||||
@@ -1,87 +1,146 @@
|
||||
// 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;
|
||||
use crate::agent::context::AgentContext;
|
||||
use crate::agent::skill_registry::SkillRegistry;
|
||||
|
||||
pub struct BaseAgent {
|
||||
/// Eindeutige ID dieser Agent-Instanz
|
||||
pub id: AgentId,
|
||||
/// Baut den System-Prompt aus soul_core + soul_personality
|
||||
agent_id: String,
|
||||
max_tokens: u32,
|
||||
max_loops: u32,
|
||||
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,
|
||||
registry: Arc<SkillRegistry>,
|
||||
}
|
||||
|
||||
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(
|
||||
agent_id: impl Into<String>,
|
||||
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>,
|
||||
registry: Arc<SkillRegistry>,
|
||||
max_tokens: u32,
|
||||
max_loops: u32,
|
||||
) -> Self {
|
||||
// Skill-Format vom Provider abfragen bevor llm konsumiert wird
|
||||
let skill_format = llm.skill_format();
|
||||
let agent_id = agent_id.into();
|
||||
|
||||
Self {
|
||||
id: AgentId::new_v4(),
|
||||
agent_id: agent_id.clone(),
|
||||
max_tokens,
|
||||
max_loops,
|
||||
prompt_builder: PromptBuilder::new(
|
||||
&agent_id,
|
||||
shared_core_path,
|
||||
soul_core_path,
|
||||
soul_personality_path,
|
||||
),
|
||||
llm,
|
||||
history: Vec::new(),
|
||||
skill_executor: SkillExecutor::new(personality_writer, skill_format),
|
||||
skill_executor: SkillExecutor::new(registry.clone(), skill_format),
|
||||
registry,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
let ctx = AgentContext { agent_id: self.agent_id.clone() };
|
||||
|
||||
// System-Prompt einmal aufbauen — bleibt für alle Loop-Iterationen gleich
|
||||
let mut system_prompt = self.prompt_builder.build()?;
|
||||
let skills_block = self.registry.prompt_block(&self.agent_id);
|
||||
if !skills_block.is_empty() {
|
||||
system_prompt.push_str("\n\n");
|
||||
system_prompt.push_str(&skills_block);
|
||||
}
|
||||
|
||||
// User-Nachricht zur History hinzufügen
|
||||
self.history.push(Message::user(user_message));
|
||||
|
||||
let mut messages = vec![Message::system(system_prompt)];
|
||||
messages.extend(self.history.clone());
|
||||
let mut last_response = String::new();
|
||||
|
||||
let request = LlmRequest {
|
||||
messages,
|
||||
max_tokens: 4096,
|
||||
temperature: 0.7,
|
||||
};
|
||||
for loop_index in 1..=self.max_loops {
|
||||
let is_last_loop = loop_index == self.max_loops;
|
||||
|
||||
let response = self.llm.complete(request).await?;
|
||||
// Loop-Hinweis als System-Nachricht — Agent weiß wo er ist
|
||||
let loop_hint = if is_last_loop {
|
||||
format!(
|
||||
"[Interner Schritt — Loop {}/{} — Letzter Schritt, antworte jetzt]",
|
||||
loop_index, self.max_loops
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"[Interner Schritt — Loop {}/{}]\n\
|
||||
Wenn du keine weiteren Skills oder Informationen brauchst, antworte jetzt.\n\
|
||||
Wenn du noch einen Skill brauchst, rufe ihn auf.",
|
||||
loop_index, self.max_loops
|
||||
)
|
||||
};
|
||||
|
||||
// Skill-Calls parsen und ausführen — sauberen Text zurückbekommen
|
||||
let clean_response = self.skill_executor.process(&response.content);
|
||||
// Prompt zusammenbauen — system + loop hint + history
|
||||
let system_with_hint = format!("{}\n\n{}", system_prompt, loop_hint);
|
||||
let mut messages = vec![Message::system(system_with_hint)];
|
||||
messages.extend(self.history.clone());
|
||||
|
||||
// Sauberen Text zum Verlauf hinzufügen
|
||||
self.history.push(Message::assistant(&clean_response));
|
||||
let request = LlmRequest {
|
||||
messages,
|
||||
max_tokens: self.max_tokens,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
Ok(clean_response)
|
||||
let response = self.llm.complete(request).await?;
|
||||
let raw = response.content.clone();
|
||||
|
||||
// skill_info abfangen — Details holen und als nächste Nachricht einspeisen
|
||||
if let Some(skill_name) = Self::parse_skill_info(&raw) {
|
||||
if let Some(skill) = self.registry.get(&skill_name) {
|
||||
let details = format!(
|
||||
"[Skill-Details für '{}']\n{}",
|
||||
skill_name,
|
||||
skill.details()
|
||||
);
|
||||
// Details kommen als interne Nachricht in die History —
|
||||
// nicht an den User, nur für den nächsten LLM-Call
|
||||
self.history.push(Message::assistant(&raw));
|
||||
self.history.push(Message::user(&details));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skill-Calls ausführen — sauberen Text zurückbekommen
|
||||
let clean = self.skill_executor.process(&raw, ctx.clone()).await;
|
||||
|
||||
// Wenn keine skill_info und kein Skill-Call — Agent ist fertig
|
||||
if clean == raw.trim() {
|
||||
last_response = clean.clone();
|
||||
self.history.push(Message::assistant(&clean));
|
||||
break;
|
||||
}
|
||||
|
||||
// Skill wurde ausgeführt — nächste Iteration
|
||||
last_response = clean.clone();
|
||||
self.history.push(Message::assistant(&clean));
|
||||
}
|
||||
|
||||
Ok(last_response)
|
||||
}
|
||||
|
||||
/// Parst <skill_info>skill_name</skill_info> aus einer Antwort.
|
||||
fn parse_skill_info(response: &str) -> Option<String> {
|
||||
let open = "<skill_info>";
|
||||
let close = "</skill_info>";
|
||||
let start = response.find(open)? + open.len();
|
||||
let end = response.find(close)?;
|
||||
Some(response[start..end].trim().to_string())
|
||||
}
|
||||
|
||||
/// 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,6 @@
|
||||
// nazarick-core/src/agent/context.rs
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentContext {
|
||||
pub agent_id: String,
|
||||
}
|
||||
@@ -3,10 +3,8 @@
|
||||
// Agent-Modul — BaseAgent und SkillExecutor.
|
||||
// Neue Agent-Funktionalität als eigenes Submodul hinzufügen.
|
||||
|
||||
mod base;
|
||||
pub mod base;
|
||||
mod skill_executor;
|
||||
mod traits;
|
||||
|
||||
pub use base::BaseAgent;
|
||||
pub use skill_executor::SkillExecutor;
|
||||
pub use traits::PersonalityWriter;
|
||||
pub mod traits;
|
||||
pub mod context;
|
||||
pub mod skill_registry;
|
||||
@@ -1,78 +1,84 @@
|
||||
// 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;
|
||||
use tracing::{error, info, warn};
|
||||
use crate::agent::skill_registry::SkillRegistry;
|
||||
use crate::agent::traits::SkillInput;
|
||||
use crate::agent::context::AgentContext;
|
||||
use crate::llm::SkillFormat;
|
||||
use crate::agent::traits::Skill;
|
||||
|
||||
/// 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 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<dyn PersonalityWriter>,
|
||||
/// Format das der aktuelle Provider für Skill-Calls nutzt
|
||||
registry: Arc<SkillRegistry>,
|
||||
skill_format: SkillFormat,
|
||||
}
|
||||
|
||||
impl SkillExecutor {
|
||||
/// Erstellt einen neuen SkillExecutor.
|
||||
/// `personality_writer` → konkrete Impl aus skills-Crate
|
||||
/// `skill_format` → vom LlmProvider bestimmt
|
||||
pub fn new(personality_writer: Arc<dyn PersonalityWriter>, skill_format: SkillFormat) -> Self {
|
||||
Self { personality_writer, skill_format }
|
||||
pub fn new(registry: Arc<SkillRegistry>, skill_format: SkillFormat) -> Self {
|
||||
Self { registry, skill_format }
|
||||
}
|
||||
|
||||
/// Parst XML-Tags aus der Antwort, führt Skills aus, gibt sauberen Text zurück.
|
||||
pub fn process(&self, response: &str) -> String {
|
||||
pub async fn process(&self, response: &str, ctx: AgentContext) -> String {
|
||||
match self.skill_format {
|
||||
SkillFormat::None => response.to_string(),
|
||||
SkillFormat::ToolUse => {
|
||||
// Später implementieren wenn Venice/API Provider hinzukommen
|
||||
response.to_string()
|
||||
}
|
||||
SkillFormat::ToolUse => response.to_string(),
|
||||
SkillFormat::Xml => {
|
||||
let (clean_text, calls) = Self::parse(response);
|
||||
let (clean_text, calls) = self.parse(response);
|
||||
for call in calls {
|
||||
self.execute(call);
|
||||
self.execute(call, ctx.clone()).await;
|
||||
}
|
||||
clean_text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parst alle Skill-Calls aus einem Text.
|
||||
/// Unterstützt zwei Formate:
|
||||
/// 1. <skill name="update_personality">...</skill>
|
||||
/// 2. <update_personality>...</update_personality>
|
||||
fn parse(response: &str) -> (String, Vec<SkillCall>) {
|
||||
async fn execute(&self, call: SkillCall, ctx: AgentContext) {
|
||||
// Rechte prüfen bevor der Skill überhaupt geholt wird
|
||||
if !self.registry.verify(&ctx.agent_id, &call.name) {
|
||||
warn!(
|
||||
skill = %call.name,
|
||||
agent = %ctx.agent_id,
|
||||
"Skill-Aufruf verweigert — keine Berechtigung"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(skill): Option<Arc<dyn Skill>> = self.registry.get(&call.name) else {
|
||||
warn!(skill = %call.name, "Skill nicht gefunden");
|
||||
return;
|
||||
};
|
||||
|
||||
let params = call.params.into_iter().collect();
|
||||
let input = SkillInput { params };
|
||||
|
||||
match skill.execute(input, ctx).await {
|
||||
Ok(output) if output.success => {
|
||||
info!(skill = %call.name, "{}", output.message);
|
||||
}
|
||||
Ok(output) => {
|
||||
error!(skill = %call.name, "Fehlgeschlagen: {}", output.message);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(skill = %call.name, error = %e, "Skill abgebrochen");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(&self, response: &str) -> (String, Vec<SkillCall>) {
|
||||
let mut calls = Vec::new();
|
||||
let mut clean = response.to_string();
|
||||
|
||||
// Format 1: <skill name="...">...</skill>
|
||||
loop {
|
||||
let start = match clean.find("<skill name=\"") {
|
||||
Some(s) => s,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let end = match clean[start..].find("</skill>") {
|
||||
Some(e) => start + e,
|
||||
None => {
|
||||
@@ -80,9 +86,7 @@ impl SkillExecutor {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let tag_content = clean[start..end + "</skill>".len()].to_string();
|
||||
|
||||
let name_start = start + "<skill name=\"".len();
|
||||
let name_end = match clean[name_start..].find('"') {
|
||||
Some(e) => name_start + e,
|
||||
@@ -92,7 +96,6 @@ impl SkillExecutor {
|
||||
}
|
||||
};
|
||||
let name = clean[name_start..name_end].to_string();
|
||||
|
||||
let inner_start = match clean[start..end].find('>') {
|
||||
Some(i) => start + i + 1,
|
||||
None => {
|
||||
@@ -106,18 +109,14 @@ impl SkillExecutor {
|
||||
clean = clean.replace(&tag_content, "").trim().to_string();
|
||||
}
|
||||
|
||||
// Format 2: <update_personality>...</update_personality>
|
||||
// und <remove_personality>...</remove_personality>
|
||||
for skill_name in &["update_personality", "remove_personality"] {
|
||||
for name in self.registry.all_names() {
|
||||
loop {
|
||||
let open_tag = format!("<{}>", skill_name);
|
||||
let close_tag = format!("</{}>", skill_name);
|
||||
|
||||
let open_tag = format!("<{}>", name);
|
||||
let close_tag = format!("</{}>", 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 => {
|
||||
@@ -125,14 +124,10 @@ impl SkillExecutor {
|
||||
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,
|
||||
});
|
||||
calls.push(SkillCall { name: name.to_string(), params });
|
||||
clean = clean.replace(&tag_content, "").trim().to_string();
|
||||
}
|
||||
}
|
||||
@@ -140,21 +135,17 @@ impl SkillExecutor {
|
||||
(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;
|
||||
@@ -168,43 +159,6 @@ impl SkillExecutor {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
"remove_personality" => {
|
||||
let field = call.params.iter().find(|(k, _)| k == "field").map(|(_, v)| v.as_str());
|
||||
|
||||
match field {
|
||||
Some(f) => {
|
||||
if let Err(e) = self.personality_writer.remove(f) {
|
||||
error!(error = %e, "Persönlichkeits-Entfernung fehlgeschlagen");
|
||||
} else {
|
||||
info!(field = %f, "Persönlichkeits-Abschnitt entfernt");
|
||||
}
|
||||
}
|
||||
None => error!("remove_personality: field fehlt"),
|
||||
}
|
||||
}
|
||||
unknown => error!(skill = %unknown, "Unbekannter Skill"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
use crate::agent::traits::Skill;
|
||||
|
||||
pub struct SkillMeta {
|
||||
pub name: &'static str,
|
||||
pub allowed: &'static [&'static str],
|
||||
/// true = Agent muss auf Ergebnis warten (z.B. web_search)
|
||||
/// false = fire-and-forget, Agent kann gleichzeitig antworten (z.B. personality)
|
||||
pub awaits_result: bool,
|
||||
pub skill: fn() -> Arc<dyn Skill>,
|
||||
}
|
||||
|
||||
inventory::collect!(SkillMeta);
|
||||
|
||||
pub struct SkillRegistry {
|
||||
skills: HashMap<&'static str, &'static SkillMeta>,
|
||||
}
|
||||
|
||||
impl SkillRegistry {
|
||||
pub fn collect() -> Self {
|
||||
let mut skills = HashMap::new();
|
||||
for meta in inventory::iter::<SkillMeta> {
|
||||
skills.insert(meta.name, meta);
|
||||
}
|
||||
Self { skills }
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<Arc<dyn Skill>> {
|
||||
self.skills.get(name).map(|meta| (meta.skill)())
|
||||
}
|
||||
|
||||
pub fn verify(&self, agent_id: &str, skill_name: &str) -> bool {
|
||||
match self.skills.get(skill_name) {
|
||||
Some(meta) => Self::is_allowed(meta, agent_id),
|
||||
None => {
|
||||
warn!(skill = %skill_name, "Skill nicht registriert");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_names(&self) -> Vec<&'static str> {
|
||||
self.skills.keys().copied().collect()
|
||||
}
|
||||
|
||||
pub fn prompt_block(&self, agent_id: &str) -> String {
|
||||
let skills: Vec<_> = self.skills.values()
|
||||
.filter(|meta| Self::is_allowed(meta, agent_id))
|
||||
.collect();
|
||||
|
||||
if skills.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut block = String::from("=== Verfügbare Skills ===\n");
|
||||
for meta in &skills {
|
||||
let instance = (meta.skill)();
|
||||
let mode = if meta.awaits_result {
|
||||
"[wartet auf Ergebnis]"
|
||||
} else {
|
||||
"[fire-and-forget]"
|
||||
};
|
||||
block.push_str(&format!(
|
||||
"- {} {}: {}\n",
|
||||
meta.name, mode, instance.summary()
|
||||
));
|
||||
}
|
||||
block.push_str(
|
||||
"\nFür Details und Verwendung eines Skills:\n<skill_info>skill_name</skill_info>"
|
||||
);
|
||||
block
|
||||
}
|
||||
|
||||
fn is_allowed(meta: &SkillMeta, agent_id: &str) -> bool {
|
||||
meta.allowed.contains(&"all") || meta.allowed.contains(&agent_id)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,46 @@
|
||||
// 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 und verwaltet 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.
|
||||
/// Abschnitt wird ersetzt wenn vorhanden, sonst angehängt.
|
||||
fn update(&self, field: &str, value: &str) -> anyhow::Result<()>;
|
||||
use std::collections::HashMap;
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
use crate::agent::context::AgentContext;
|
||||
|
||||
/// Entfernt einen Abschnitt aus soul_personality.md.
|
||||
/// Macht nichts wenn der Abschnitt nicht existiert.
|
||||
fn remove(&self, field: &str) -> anyhow::Result<()>;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SkillInput {
|
||||
pub params: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl SkillInput {
|
||||
pub fn require(&self, key: &str) -> Result<&str> {
|
||||
self.params
|
||||
.get(key)
|
||||
.map(|s| s.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Parameter '{}' fehlt", key))
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&str> {
|
||||
self.params.get(key).map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SkillOutput {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl SkillOutput {
|
||||
pub fn ok(msg: impl Into<String>) -> Self {
|
||||
Self { success: true, message: msg.into() }
|
||||
}
|
||||
pub fn err(msg: impl Into<String>) -> Self {
|
||||
Self { success: false, message: msg.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Skill: Send + Sync {
|
||||
fn summary(&self) -> &str;
|
||||
fn details(&self) -> &str;
|
||||
async fn execute(&self, input: SkillInput, ctx: AgentContext) -> Result<SkillOutput>;
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
pub mod error;
|
||||
pub mod types;
|
||||
pub mod traits;
|
||||
pub mod permissions;
|
||||
pub mod usage;
|
||||
/// Implementiert den Agent-Trait aus nazarick-core und nutzt
|
||||
/// den api-Crate für LLM-Kommunikation.
|
||||
///
|
||||
/// Persönlichkeit basiert auf zwei Dateien:
|
||||
/// - soul_core.md → unveränderlicher Kern (Regeln, Sicherheit)
|
||||
/// - soul_personality.md → entwickelbarer Teil (Ton, Präferenzen)
|
||||
pub mod prompt;
|
||||
pub mod llm;
|
||||
pub mod agent;
|
||||
@@ -1,32 +0,0 @@
|
||||
use crate::types::AgentId;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Permission {
|
||||
// LLM
|
||||
LlmAccess,
|
||||
// Image
|
||||
ImageGeneration,
|
||||
// Skills
|
||||
SkillFileRead,
|
||||
SkillFileWrite,
|
||||
SkillWebSearch,
|
||||
// Channels
|
||||
ChannelSynology,
|
||||
ChannelWebUi,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentPermissions {
|
||||
pub agent_id: AgentId,
|
||||
pub allowed: Vec<Permission>,
|
||||
}
|
||||
|
||||
impl AgentPermissions {
|
||||
pub fn new(agent_id: AgentId, allowed: Vec<Permission>) -> Self {
|
||||
Self { agent_id, allowed }
|
||||
}
|
||||
|
||||
pub fn has(&self, permission: &Permission) -> bool {
|
||||
self.allowed.contains(permission)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,41 @@
|
||||
use crate::types::Result;
|
||||
// nazarick-core/src/prompt.rs
|
||||
use crate::error::NazarickError;
|
||||
|
||||
/// Verantwortlich für das Zusammensetzen des System-Prompts.
|
||||
///
|
||||
/// 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 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
|
||||
/// Wird aus agent_id abgeleitet — kein externer Parameter nötig
|
||||
soul_personality_path: String,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
/// Erstellt einen neuen PromptBuilder mit den angegebenen Dateipfaden.
|
||||
pub fn new(
|
||||
agent_id: &str,
|
||||
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(),
|
||||
soul_personality_path: format!(
|
||||
"crates/{}/config/soul_personality.md", agent_id
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// shared_core ist Pflicht — systemweite Sicherheitsregeln
|
||||
pub fn build(&self) -> Result<String, NazarickError> {
|
||||
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)
|
||||
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)
|
||||
format!("soul_core.md nicht gefunden unter '{}': {}", self.soul_core_path, e)
|
||||
))?;
|
||||
|
||||
// soul_personality ist optional — graceful fallback
|
||||
let personality = std::fs::read_to_string(&self.soul_personality_path)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Zusammensetzen: shared zuerst, dann core, dann personality
|
||||
let mut parts = vec![shared];
|
||||
|
||||
if !core.trim().is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user