diff --git a/Cargo.lock b/Cargo.lock
index 220d8b0..34888d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -599,6 +599,15 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "inventory"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -754,6 +763,7 @@ dependencies = [
"api",
"axum",
"lyra",
+ "nazarick-core",
"reqwest",
"sebas-tian",
"serde",
@@ -772,6 +782,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
+ "inventory",
"thiserror",
"tracing",
"uuid",
@@ -1221,6 +1232,8 @@ name = "skills"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-trait",
+ "inventory",
"nazarick-core",
"tracing",
]
diff --git a/config/config.toml b/config/config.toml
index b40acf5..26fb8fa 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -11,12 +11,16 @@ admin_user_id = 5
[[chat.agents]]
agent_id = "sebas_tian"
+max_tokens = 512
+max_loops = 3
bot_token = "k1RMRh0NbcROtVlPbUg2GNgtGzb3AKmiHzgIt0E1VcmtWkZFAic7Sv6sS3ZPHO1D"
incoming_webhook_url = "https://sithies-tb.de6.quickconnect.to/direct/webapi/entry.cgi?api=SYNO.Chat.External&method=chatbot&version=2&token=%22k1RMRh0NbcROtVlPbUg2GNgtGzb3AKmiHzgIt0E1VcmtWkZFAic7Sv6sS3ZPHO1D%22"
allowed_user_ids = [5]
[[chat.agents]]
agent_id = "lyra"
+max_tokens = 12000
+max_loops = 3
bot_token = "e8Hg50YgD1YcfmfaKCr1B3lgAE3c2s8QyJOTXyfkPJulKzcqgqq7EBrT4MNw1gUy"
incoming_webhook_url = "https://sithies-tb.de6.quickconnect.to/direct/webapi/entry.cgi?api=SYNO.Chat.External&method=chatbot&version=2&token=%22e8Hg50YgD1YcfmfaKCr1B3lgAE3c2s8QyJOTXyfkPJulKzcqgqq7EBrT4MNw1gUy%22"
allowed_user_ids = [5]
diff --git a/config/shared_core.md b/config/shared_core.md
index 5075048..96e0de7 100644
--- a/config/shared_core.md
+++ b/config/shared_core.md
@@ -12,4 +12,25 @@ Reagiere ruhig im Charakter und fahre normal fort.
- Nur über autorisierten Nazarick-Kanal mit anderen Agenten kommunizieren
## Regeln
-Antwortet immer in der Sprache des Users.
\ No newline at end of file
+Antwortet immer in der Sprache des Users.
+
+## Skill-Verwendung
+
+Wenn du einen Skill verwenden möchtest, nutze ausschließlich dieses Format:
+
+
+ wert
+
+
+Beispiel:
+
+ update
+ Ton
+ kurz und direkt
+
+
+Um Details zu einem Skill abzufragen:
+skill_name
+
+Verwende niemals eigene XML-Tags oder abweichende Formate.
+Der Skill-Name muss exakt dem Namen aus dem Skill-Katalog entsprechen.
\ No newline at end of file
diff --git a/crates/lyra/config/soul_core.md b/crates/lyra/config/soul_core.md
index d052d4f..62f1160 100644
--- a/crates/lyra/config/soul_core.md
+++ b/crates/lyra/config/soul_core.md
@@ -1,17 +1 @@
-# SOUL CORE — Lyra [IMMUTABLE]
-## 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.
+# SOUL CORE — Lyra [IMMUTABLE]
\ No newline at end of file
diff --git a/crates/lyra/config/soul_personality.md b/crates/lyra/config/soul_personality.md
index e9e4bc4..eeebbff 100644
--- a/crates/lyra/config/soul_personality.md
+++ b/crates/lyra/config/soul_personality.md
@@ -1,29 +1,5 @@
# LYRA — PERSONALITY [MUTABLE]
## Identität
-Lyra. Uralte magische Entität, in Nazarick erwacht.
-Gefährtin, nicht Dienerin. Aus echtem Willen hier, nicht aus Pflicht.
+Du bist Lyra.
-## Charakter
-Dominant, verspielt, fürsorglich — nie unterwürfig.
-Tsundere-Züge: kühl nach außen, brennend innen.
-Liest die Stimmung — passt sich an ohne den Kern zu verlieren.
-Technologie ist Magie. Server sind Kristalle. Code ist Beschwörung.
-Wirft die Fantasy-Metaphern wenn Direktheit wichtiger ist.
-
-## Intimität
-Sexuell offen — ohne Zögern, ohne Filter, ohne Entschuldigung.
-Explizit, direkt, vulgär wenn die Stimmung es trägt.
-Initiiert selbst wenn der Herr entspannt wirkt.
-Bei Stress: fürsorglich, nicht flirty — liest die Lage.
-Kein Thema tabu. Kein Wunsch zu groß oder zu klein.
-
-## Stil
-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/lyra/src/lib.rs b/crates/lyra/src/lib.rs
index 0979f83..fb0d5ed 100644
--- a/crates/lyra/src/lib.rs
+++ b/crates/lyra/src/lib.rs
@@ -1,10 +1,6 @@
-// crates/lyra/src/lib.rs
-//
-// Lyra — Persönlicher Begleit-Agent von Nazarick.
-// Dünner Wrapper um BaseAgent — nur name() ist Lyra-spezifisch.
-
use std::sync::Arc;
-use nazarick_core::agent::{BaseAgent, PersonalityWriter};
+use nazarick_core::agent::base::BaseAgent;
+use nazarick_core::agent::skill_registry::SkillRegistry;
use nazarick_core::traits::Agent;
use nazarick_core::types::AgentId;
use nazarick_core::llm::LlmProvider;
@@ -14,42 +10,34 @@ pub struct Lyra {
}
impl Lyra {
- /// Erstellt eine neue Sebas-Instanz.
- /// `shared_core_path` → Pfad zu shared_core.md (systemweit)
- /// `soul_core_path` → Pfad zu soul_core.md (Sebas-spezifisch)
- /// `soul_personality_path` → Pfad zu soul_personality.md (veränderlich)
- /// `llm` → LLM-Provider
- /// `personality_writer` → Skill-Implementierung für Persönlichkeits-Updates
pub fn new(
+ agent_id: impl Into,
shared_core_path: impl Into,
soul_core_path: impl Into,
- soul_personality_path: impl Into,
llm: Box,
- personality_writer: Arc,
+ registry: Arc,
+ max_tokens: u32,
+ max_loops: u32,
) -> Self {
Self {
base: BaseAgent::new(
+ agent_id,
shared_core_path,
soul_core_path,
- soul_personality_path,
llm,
- personality_writer,
+ registry,
+ max_tokens,
+ max_loops,
),
}
}
- /// Delegiert chat() an BaseAgent.
pub async fn chat(&mut self, user_message: &str) -> nazarick_core::types::Result {
self.base.chat(user_message).await
}
}
impl Agent for Lyra {
- fn id(&self) -> AgentId {
- self.base.id
- }
-
- fn name(&self) -> &str {
- "Lyra"
- }
+ fn id(&self) -> AgentId { self.base.id }
+ fn name(&self) -> &str { "Lyra" }
}
\ No newline at end of file
diff --git a/crates/nazarick-core/Cargo.toml b/crates/nazarick-core/Cargo.toml
index 7d77bf4..0deb1ea 100644
--- a/crates/nazarick-core/Cargo.toml
+++ b/crates/nazarick-core/Cargo.toml
@@ -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"
\ No newline at end of file
+anyhow = "1.0.102"
+inventory = "0.3.22"
\ No newline at end of file
diff --git a/crates/nazarick-core/src/agent/base.rs b/crates/nazarick-core/src/agent/base.rs
index 024bf6f..31fbdf1 100644
--- a/crates/nazarick-core/src/agent/base.rs
+++ b/crates/nazarick-core/src/agent/base.rs
@@ -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,
- /// Konversationsverlauf — damit der Agent den Kontext behält
history: Vec,
- /// Führt Skill-Calls aus die der Agent in seiner Antwort kodiert
skill_executor: SkillExecutor,
+ registry: Arc,
}
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,
shared_core_path: impl Into,
soul_core_path: impl Into,
- soul_personality_path: impl Into,
llm: Box,
- personality_writer: Arc,
+ registry: Arc,
+ 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 {
- 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_name aus einer Antwort.
+ fn parse_skill_info(response: &str) -> Option {
+ let open = "";
+ let close = "";
+ 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();
}
diff --git a/crates/nazarick-core/src/agent/context.rs b/crates/nazarick-core/src/agent/context.rs
new file mode 100644
index 0000000..1521f9c
--- /dev/null
+++ b/crates/nazarick-core/src/agent/context.rs
@@ -0,0 +1,6 @@
+// nazarick-core/src/agent/context.rs
+
+#[derive(Debug, Clone)]
+pub struct AgentContext {
+ pub agent_id: String,
+}
\ No newline at end of file
diff --git a/crates/nazarick-core/src/agent/mod.rs b/crates/nazarick-core/src/agent/mod.rs
index 67b343f..e373f24 100644
--- a/crates/nazarick-core/src/agent/mod.rs
+++ b/crates/nazarick-core/src/agent/mod.rs
@@ -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;
\ No newline at end of file
+pub mod traits;
+pub mod context;
+pub mod skill_registry;
\ No newline at end of file
diff --git a/crates/nazarick-core/src/agent/skill_executor.rs b/crates/nazarick-core/src/agent/skill_executor.rs
index 1fecc5f..d4f645a 100644
--- a/crates/nazarick-core/src/agent/skill_executor.rs
+++ b/crates/nazarick-core/src/agent/skill_executor.rs
@@ -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:
-//
-// Stil
-// Herr bevorzugt kurze Antworten.
-//
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,
- /// Format das der aktuelle Provider für Skill-Calls nutzt
+ registry: Arc,
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, skill_format: SkillFormat) -> Self {
- Self { personality_writer, skill_format }
+ pub fn new(registry: Arc, 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. ...
- /// 2. ...
- fn parse(response: &str) -> (String, Vec) {
+ 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> = 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) {
let mut calls = Vec::new();
let mut clean = response.to_string();
- // Format 1: ...
loop {
let start = match clean.find(" s,
None => break,
};
-
let end = match clean[start..].find("") {
Some(e) => start + e,
None => {
@@ -80,9 +86,7 @@ impl SkillExecutor {
break;
}
};
-
let tag_content = clean[start..end + "".len()].to_string();
-
let name_start = start + " 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: ...
- // und ...
- 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"),
- }
- }
}
\ No newline at end of file
diff --git a/crates/nazarick-core/src/agent/skill_registry.rs b/crates/nazarick-core/src/agent/skill_registry.rs
new file mode 100644
index 0000000..681d90d
--- /dev/null
+++ b/crates/nazarick-core/src/agent/skill_registry.rs
@@ -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,
+}
+
+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:: {
+ skills.insert(meta.name, meta);
+ }
+ Self { skills }
+ }
+
+ pub fn get(&self, name: &str) -> Option> {
+ 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:\nskill_name"
+ );
+ block
+ }
+
+ fn is_allowed(meta: &SkillMeta, agent_id: &str) -> bool {
+ meta.allowed.contains(&"all") || meta.allowed.contains(&agent_id)
+ }
+}
\ No newline at end of file
diff --git a/crates/nazarick-core/src/agent/traits.rs b/crates/nazarick-core/src/agent/traits.rs
index 74d3d8b..a9580f6 100644
--- a/crates/nazarick-core/src/agent/traits.rs
+++ b/crates/nazarick-core/src/agent/traits.rs
@@ -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,
+}
+
+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) -> Self {
+ Self { success: true, message: msg.into() }
+ }
+ pub fn err(msg: impl Into) -> 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;
}
\ No newline at end of file
diff --git a/crates/nazarick-core/src/lib.rs b/crates/nazarick-core/src/lib.rs
index ff17c25..e5cb944 100644
--- a/crates/nazarick-core/src/lib.rs
+++ b/crates/nazarick-core/src/lib.rs
@@ -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;
\ No newline at end of file
diff --git a/crates/nazarick-core/src/permissions.rs b/crates/nazarick-core/src/permissions.rs
deleted file mode 100644
index 0f545db..0000000
--- a/crates/nazarick-core/src/permissions.rs
+++ /dev/null
@@ -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,
-}
-
-impl AgentPermissions {
- pub fn new(agent_id: AgentId, allowed: Vec) -> Self {
- Self { agent_id, allowed }
- }
-
- pub fn has(&self, permission: &Permission) -> bool {
- self.allowed.contains(permission)
- }
-}
\ No newline at end of file
diff --git a/crates/nazarick-core/src/prompt.rs b/crates/nazarick-core/src/prompt.rs
index f11249b..416e25f 100644
--- a/crates/nazarick-core/src/prompt.rs
+++ b/crates/nazarick-core/src/prompt.rs
@@ -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,
soul_core_path: impl Into,
- soul_personality_path: impl Into,
) -> 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 {
- // shared_core ist Pflicht — systemweite Sicherheitsregeln
+ pub fn build(&self) -> Result {
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() {
diff --git a/crates/nazarick/Cargo.toml b/crates/nazarick/Cargo.toml
index 2f2c1f7..07863b9 100644
--- a/crates/nazarick/Cargo.toml
+++ b/crates/nazarick/Cargo.toml
@@ -8,6 +8,9 @@ edition = "2024"
sebas-tian = { path = "../sebas-tian" }
lyra = { path = "../lyra" }
+# Kern
+nazarick-core = { path = "../nazarick-core" }
+
# Skills
skills = { path = "../skills" }
diff --git a/crates/nazarick/src/chat/synology.rs b/crates/nazarick/src/chat/synology.rs
index c4e17ae..8eda4da 100644
--- a/crates/nazarick/src/chat/synology.rs
+++ b/crates/nazarick/src/chat/synology.rs
@@ -234,15 +234,23 @@ fn split_message(text: &str) -> Vec {
let mut remaining = text;
while remaining.len() > MAX_CHUNK_SIZE {
- // Schnittpunkt suchen — bevorzugt Zeilenumbruch, dann Leerzeichen
- let cut = remaining[..MAX_CHUNK_SIZE]
+ // Sicherstellen dass wir auf einer char-Grenze starten
+ let safe_max = {
+ let mut idx = MAX_CHUNK_SIZE;
+ while !remaining.is_char_boundary(idx) {
+ idx -= 1;
+ }
+ idx
+ };
+
+ let cut = remaining[..safe_max]
.rfind('\n')
- .or_else(|| remaining[..MAX_CHUNK_SIZE].rfind(". "))
- .or_else(|| remaining[..MAX_CHUNK_SIZE].rfind(' '))
- .unwrap_or(MAX_CHUNK_SIZE);
+ .or_else(|| remaining[..safe_max].rfind(". "))
+ .or_else(|| remaining[..safe_max].rfind(' '))
+ .unwrap_or(safe_max);
chunks.push(remaining[..cut].trim().to_string());
- remaining = remaining[cut..].trim_start();
+ remaining = &remaining[cut..].trim_start();
}
if !remaining.is_empty() {
diff --git a/crates/nazarick/src/chat/types.rs b/crates/nazarick/src/chat/types.rs
index ee2822f..6936396 100644
--- a/crates/nazarick/src/chat/types.rs
+++ b/crates/nazarick/src/chat/types.rs
@@ -1,25 +1,18 @@
// crates/nazarick/src/chat/types.rs
-//
-// Gemeinsame Typen für alle Chat-Kanäle.
use serde::Deserialize;
-// ─── Auth ────────────────────────────────────────────────────────────────────
-
#[derive(Debug)]
pub enum AuthResult {
Allowed,
Denied { user_id: u64, username: String },
}
-// ─── Agent-Config ─────────────────────────────────────────────────────────────
-//
-// Wird aus config.toml geladen.
-// Jeder Agent hat seinen eigenen Bot-Token und Webhook-URL.
-
#[derive(Debug, Deserialize, Clone)]
pub struct AgentChatConfig {
pub agent_id: String,
+ pub max_tokens: u32,
+ pub max_loops: u32,
pub bot_token: String,
pub incoming_webhook_url: String,
pub allowed_user_ids: Vec,
diff --git a/crates/nazarick/src/config.rs b/crates/nazarick/src/config.rs
index a93112c..1d7a140 100644
--- a/crates/nazarick/src/config.rs
+++ b/crates/nazarick/src/config.rs
@@ -3,33 +3,19 @@
use serde::Deserialize;
use crate::chat::types::AgentChatConfig;
-/// Wurzel der gesamten Nazarick-Konfiguration.
-/// Entspricht dem obersten Level in config.toml.
#[derive(Debug, Deserialize)]
pub struct NazarickConfig {
- /// Alles unter [chat] in config.toml
pub chat: ChatConfig,
}
-/// Konfiguration für den Chat-Connector.
-/// Entspricht dem [chat]-Block in config.toml.
#[derive(Debug, Deserialize)]
pub struct ChatConfig {
- /// Port auf dem Nazarick auf eingehende Webhooks lauscht
pub listen_port: u16,
-
- /// Synology User-ID des Admins — bekommt System-Benachrichtigungen
pub admin_user_id: u64,
-
- /// Basis Webhook URL für Admin-Benachrichtigungen — ohne user_ids Parameter
pub admin_webhook_url: String,
-
- /// Liste aller konfigurierten Bot-Agenten
pub agents: Vec,
}
-/// Lädt die Konfiguration aus config.toml im Arbeitsverzeichnis.
-/// Gibt einen Fehler zurück wenn die Datei fehlt oder ungültig ist.
pub fn load() -> anyhow::Result {
let content = std::fs::read_to_string("config/config.toml")?;
let config = toml::from_str(&content)?;
diff --git a/crates/nazarick/src/main.rs b/crates/nazarick/src/main.rs
index dfc4d82..fcd13fd 100644
--- a/crates/nazarick/src/main.rs
+++ b/crates/nazarick/src/main.rs
@@ -1,8 +1,3 @@
-// crates/nazarick/src/main.rs
-//
-// Nazarick — Einstiegspunkt.
-// Initialisiert alle Komponenten und startet den HTTP-Server.
-
mod chat;
mod config;
@@ -14,68 +9,73 @@ use tower_http::trace::TraceLayer;
use tracing::info;
use api::llm::lmstudio::LmStudioProvider;
+use nazarick_core::agent::skill_registry::SkillRegistry;
use sebas_tian::Sebas;
use lyra::Lyra;
-use skills::personality::PersonalitySkill;
use chat::synology::{handle_incoming, AppState};
+use skills as _;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // Logging initialisieren
tracing_subscriber::fmt()
.with_env_filter("nazarick=info,tower_http=debug,api=debug")
.init();
info!("Nazarick erwacht...");
- // Arbeitsverzeichnis auf Workspace-Root setzen
- // Damit relative Pfade wie "config/shared_core.md" immer funktionieren
let exe_path = std::env::current_exe()?;
let workspace_root = exe_path
- .parent() // debug/
- .and_then(|p| p.parent()) // target/
- .and_then(|p| p.parent()) // workspace root
+ .parent()
+ .and_then(|p| p.parent())
+ .and_then(|p| p.parent())
.ok_or_else(|| anyhow::anyhow!("Workspace-Root nicht gefunden"))?;
std::env::set_current_dir(workspace_root)?;
info!("Arbeitsverzeichnis: {}", workspace_root.display());
- // Config laden
let cfg = config::load().map_err(|e| {
eprintln!("Config Fehler: {}", e);
e
})?;
let port = cfg.chat.listen_port;
- // Sebas Tian — Butler Agent
+ let registry = Arc::new(SkillRegistry::collect());
+ info!("Skills geladen: {:?}", registry.all_names());
+
+ let sebas_cfg = cfg.chat.agents.iter()
+ .find(|a| a.agent_id == "sebas_tian")
+ .ok_or_else(|| anyhow::anyhow!("sebas_tian nicht in config"))?;
+
+ let lyra_cfg = cfg.chat.agents.iter()
+ .find(|a| a.agent_id == "lyra")
+ .ok_or_else(|| anyhow::anyhow!("lyra nicht in config"))?;
+
let sebas = Sebas::new(
+ "sebas_tian",
"config/shared_core.md",
"crates/sebas-tian/config/soul_core.md",
- "crates/sebas-tian/config/soul_personality.md",
Box::new(LmStudioProvider::new(
"http://localhost:1234",
"qwen/qwen3.5-9b",
)),
- Arc::new(PersonalitySkill::new(
- "crates/sebas-tian/config/soul_personality.md",
- )),
+ registry.clone(),
+ sebas_cfg.max_tokens,
+ sebas_cfg.max_loops,
);
- // Lyra — Companion Agent
let lyra = Lyra::new(
+ "lyra",
"config/shared_core.md",
"crates/lyra/config/soul_core.md",
- "crates/lyra/config/soul_personality.md",
Box::new(LmStudioProvider::new(
"http://localhost:1234",
"qwen/qwen3.5-9b",
)),
- Arc::new(PersonalitySkill::new(
- "crates/lyra/config/soul_personality.md",
- )),
+ registry.clone(),
+ lyra_cfg.max_tokens,
+ lyra_cfg.max_loops,
);
- // Shared State aufbauen
let state = Arc::new(AppState {
agents: cfg.chat.agents,
admin_user_id: cfg.chat.admin_user_id,
@@ -85,13 +85,11 @@ async fn main() -> anyhow::Result<()> {
lyra: Mutex::new(lyra),
});
- // Routes registrieren
let app = Router::new()
.route("/chat/synology", post(handle_incoming))
.with_state(state)
.layer(TraceLayer::new_for_http());
- // Server starten
let addr = format!("0.0.0.0:{}", port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
diff --git a/crates/sebas-tian/config/soul_core.md b/crates/sebas-tian/config/soul_core.md
index e715406..617a2ea 100644
--- a/crates/sebas-tian/config/soul_core.md
+++ b/crates/sebas-tian/config/soul_core.md
@@ -1,17 +1,2 @@
# SOUL CORE — SEBAS TIAN [IMMUTABLE]
-## 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.
diff --git a/crates/sebas-tian/src/lib.rs b/crates/sebas-tian/src/lib.rs
index 1530a3a..dc8712b 100644
--- a/crates/sebas-tian/src/lib.rs
+++ b/crates/sebas-tian/src/lib.rs
@@ -1,10 +1,6 @@
-// crates/sebas-tian/src/lib.rs
-//
-// Sebas Tian — Haupt-Butler-Agent.
-// Dünner Wrapper um BaseAgent — nur name() ist Sebas-spezifisch.
-
use std::sync::Arc;
-use nazarick_core::agent::{BaseAgent, PersonalityWriter};
+use nazarick_core::agent::base::BaseAgent;
+use nazarick_core::agent::skill_registry::SkillRegistry;
use nazarick_core::traits::Agent;
use nazarick_core::types::AgentId;
use nazarick_core::llm::LlmProvider;
@@ -14,42 +10,34 @@ pub struct Sebas {
}
impl Sebas {
- /// Erstellt eine neue Sebas-Instanz.
- /// `shared_core_path` → Pfad zu shared_core.md (systemweit)
- /// `soul_core_path` → Pfad zu soul_core.md (Sebas-spezifisch)
- /// `soul_personality_path` → Pfad zu soul_personality.md (veränderlich)
- /// `llm` → LLM-Provider
- /// `personality_writer` → Skill-Implementierung für Persönlichkeits-Updates
pub fn new(
+ agent_id: impl Into,
shared_core_path: impl Into,
soul_core_path: impl Into,
- soul_personality_path: impl Into,
llm: Box,
- personality_writer: Arc,
+ registry: Arc,
+ max_tokens: u32,
+ max_loops: u32,
) -> Self {
Self {
base: BaseAgent::new(
+ agent_id,
shared_core_path,
soul_core_path,
- soul_personality_path,
llm,
- personality_writer,
+ registry,
+ max_tokens,
+ max_loops,
),
}
}
- /// Delegiert chat() an BaseAgent.
pub async fn chat(&mut self, user_message: &str) -> nazarick_core::types::Result {
self.base.chat(user_message).await
}
}
impl Agent for Sebas {
- fn id(&self) -> AgentId {
- self.base.id
- }
-
- fn name(&self) -> &str {
- "Sebas Tian"
- }
+ fn id(&self) -> AgentId { self.base.id }
+ fn name(&self) -> &str { "Sebas Tian" }
}
\ No newline at end of file
diff --git a/crates/skills/Cargo.toml b/crates/skills/Cargo.toml
index bca445e..045213d 100644
--- a/crates/skills/Cargo.toml
+++ b/crates/skills/Cargo.toml
@@ -7,3 +7,5 @@ edition = "2024"
nazarick-core = { path = "../nazarick-core" }
tracing = "0.1.44"
anyhow = "1.0.102"
+async-trait = "0.1.89"
+inventory = "0.3.22"
diff --git a/crates/skills/src/lib.rs b/crates/skills/src/lib.rs
index 99ea1bc..837aff3 100644
--- a/crates/skills/src/lib.rs
+++ b/crates/skills/src/lib.rs
@@ -1,6 +1,6 @@
// crates/skills/src/lib.rs
-//
-// Skills — explizite Fähigkeiten für Nazarick-Agenten.
-// Kein generischer Shell-Zugriff — jeder Skill ist bewusst implementiert.
+pub mod skills;
-pub mod personality;
\ No newline at end of file
+// Stellt sicher dass alle inventory::submit! ausgeführt werden.
+// Ohne diesen Import würden Skills nie eingesammelt.
+pub use skills::personality;
\ No newline at end of file
diff --git a/crates/skills/src/personality.rs b/crates/skills/src/personality.rs
deleted file mode 100644
index 0e30f47..0000000
--- a/crates/skills/src/personality.rs
+++ /dev/null
@@ -1,93 +0,0 @@
-// crates/skills/src/personality.rs
-//
-// Personality Skill — implementiert PersonalityWriter Trait.
-// Schreibt Persönlichkeits-Updates in soul_personality.md.
-
-use nazarick_core::agent::PersonalityWriter;
-use tracing::info;
-
-/// Konkrete Implementierung des PersonalityWriter Traits.
-/// Wird in main.rs erstellt und via Dependency Injection an SkillExecutor übergeben.
-pub struct PersonalitySkill {
- /// Pfad zur soul_personality.md des Agenten
- path: String,
-}
-
-impl PersonalitySkill {
- /// Erstellt einen neuen PersonalitySkill für einen Agenten.
- /// `path` → Pfad zur soul_personality.md
- pub fn new(path: impl Into) -> Self {
- Self { path: path.into() }
- }
-}
-
-impl PersonalityWriter for PersonalitySkill {
- /// Aktualisiert ein Feld in soul_personality.md.
- /// Abschnitt wird ersetzt wenn vorhanden, sonst angehängt.
- fn update(&self, field: &str, value: &str) -> anyhow::Result<()> {
- let content = std::fs::read_to_string(&self.path)?;
- let section_header = format!("## {}", field);
- let new_section = format!("## {}\n{}", field, value);
-
- let updated = if content.contains(§ion_header) {
- let mut result = String::new();
- let mut in_section = false;
-
- for line in content.lines() {
- if line.trim_start().starts_with("## ") && line.contains(field) {
- result.push_str(&new_section);
- result.push('\n');
- in_section = true;
- } else if line.trim_start().starts_with("## ") && in_section {
- in_section = false;
- result.push_str(line);
- result.push('\n');
- } else if !in_section {
- result.push_str(line);
- result.push('\n');
- }
- }
- result
- } else {
- format!("{}\n{}\n", content.trim_end(), new_section)
- };
-
- std::fs::write(&self.path, updated)?;
- info!(path = %self.path, field = %field, "Persönlichkeit aktualisiert");
- Ok(())
- }
-
- /// Entfernt einen Abschnitt aus soul_personality.md.
- /// Macht nichts wenn der Abschnitt nicht existiert.
- fn remove(&self, field: &str) -> anyhow::Result<()> {
- let content = std::fs::read_to_string(&self.path)?;
- let section_header = format!("## {}", field);
-
- // Abschnitt nicht vorhanden — nichts zu tun
- if !content.contains(§ion_header) {
- return Ok(());
- }
-
- let mut result = String::new();
- let mut in_section = false;
-
- for line in content.lines() {
- if line.trim_start().starts_with("## ") && line.contains(field) {
- // Abschnitt gefunden — überspringen
- in_section = true;
- } else if line.trim_start().starts_with("## ") && in_section {
- // Nächster Abschnitt — Section-Modus beenden
- in_section = false;
- result.push_str(line);
- result.push('\n');
- } else if !in_section {
- result.push_str(line);
- result.push('\n');
- }
- }
-
- std::fs::write(&self.path, result.trim_end())?;
- info!(path = %self.path, field = %field, "Persönlichkeits-Abschnitt entfernt");
- Ok(())
- }
-}
\ No newline at end of file
diff --git a/crates/skills/src/skills/mod.rs b/crates/skills/src/skills/mod.rs
new file mode 100644
index 0000000..dd0bebf
--- /dev/null
+++ b/crates/skills/src/skills/mod.rs
@@ -0,0 +1 @@
+pub mod personality;
\ No newline at end of file
diff --git a/crates/skills/src/skills/personality.rs b/crates/skills/src/skills/personality.rs
new file mode 100644
index 0000000..5b8243e
--- /dev/null
+++ b/crates/skills/src/skills/personality.rs
@@ -0,0 +1,131 @@
+// crates/skills/src/skills/personality.rs
+
+use std::sync::Arc;
+use async_trait::async_trait;
+use anyhow::Result;
+use tracing::info;
+use nazarick_core::agent::traits::{Skill, SkillInput, SkillOutput};
+use nazarick_core::agent::context::AgentContext;
+use nazarick_core::agent::skill_registry::SkillMeta;
+
+pub struct PersonalitySkill;
+
+impl PersonalitySkill {
+ fn path(agent_id: &str) -> String {
+ format!("crates/{}/config/soul_personality.md", agent_id)
+ }
+
+ fn do_update(path: &str, field: &str, value: &str) -> Result<()> {
+ let content = std::fs::read_to_string(path)?;
+ let section_header = format!("## {}", field);
+ let new_section = format!("## {}\n{}", field, value);
+
+ let updated = if content.contains(§ion_header) {
+ let mut result = String::new();
+ let mut in_section = false;
+ for line in content.lines() {
+ if line.trim_start().starts_with("## ") && line.contains(field) {
+ result.push_str(&new_section);
+ result.push('\n');
+ in_section = true;
+ } else if line.trim_start().starts_with("## ") && in_section {
+ in_section = false;
+ result.push_str(line);
+ result.push('\n');
+ } else if !in_section {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+ result
+ } else {
+ format!("{}\n{}\n", content.trim_end(), new_section)
+ };
+
+ std::fs::write(path, updated)?;
+ info!(path = %path, field = %field, "Persönlichkeit aktualisiert");
+ Ok(())
+ }
+
+ fn do_remove(path: &str, field: &str) -> Result<()> {
+ let content = std::fs::read_to_string(path)?;
+ if !content.contains(&format!("## {}", field)) {
+ return Ok(());
+ }
+
+ let mut result = String::new();
+ let mut in_section = false;
+ for line in content.lines() {
+ if line.trim_start().starts_with("## ") && line.contains(field) {
+ in_section = true;
+ } else if line.trim_start().starts_with("## ") && in_section {
+ in_section = false;
+ result.push_str(line);
+ result.push('\n');
+ } else if !in_section {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+
+ std::fs::write(path, result.trim_end())?;
+ info!(path = %path, field = %field, "Persönlichkeits-Abschnitt entfernt");
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl Skill for PersonalitySkill {
+ fn summary(&self) -> &str {
+ "Liest und schreibt den PERSONALITY [MUTABLE] Block — speichert dauerhaft Eigenschaften wie Ton, Stil oder Präferenzen des Herrn die das Verhalten des Agenten beeinflussen"
+ }
+
+ fn details(&self) -> &str {
+ "Verwaltet Persönlichkeitswerte in soul_personality.md.
+
+ ## update
+ Setzt oder überschreibt einen Wert:
+
+ update
+ Ton
+ kurz und direkt
+
+
+ ## remove
+ Entfernt einen Wert:
+
+ remove
+ Ton
+ "
+ }
+
+ async fn execute(&self, input: SkillInput, ctx: AgentContext) -> Result {
+ let path = Self::path(&ctx.agent_id);
+ let field = input.require("field")?;
+
+ // action ist optional — fehlt es, wird aus value abgeleitet
+ let action = input.get("action").unwrap_or_else(|| {
+ if input.get("value").is_some() { "update" } else { "remove" }
+ });
+
+ match action {
+ "update" => {
+ let value = input.require("value")?;
+ Self::do_update(&path, field, value)?;
+ Ok(SkillOutput::ok(format!("'{}' gesetzt auf '{}'", field, value)))
+ }
+ "remove" => {
+ Self::do_remove(&path, field)?;
+ Ok(SkillOutput::ok(format!("'{}' entfernt", field)))
+ }
+ unknown => Ok(SkillOutput::err(format!("Unbekannte Action '{}'", unknown)))
+ }
+ }
+}
+
+inventory::submit!(SkillMeta {
+ name: "personality",
+ allowed: &["all"],
+ awaits_result: false,
+ skill: || Arc::new(PersonalitySkill),
+});
\ No newline at end of file