upgrade Skill system auf regitry
This commit is contained in:
Generated
+13
@@ -599,6 +599,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inventory"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -754,6 +763,7 @@ dependencies = [
|
|||||||
"api",
|
"api",
|
||||||
"axum",
|
"axum",
|
||||||
"lyra",
|
"lyra",
|
||||||
|
"nazarick-core",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sebas-tian",
|
"sebas-tian",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -772,6 +782,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"inventory",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -1221,6 +1232,8 @@ name = "skills"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"inventory",
|
||||||
"nazarick-core",
|
"nazarick-core",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ admin_user_id = 5
|
|||||||
|
|
||||||
[[chat.agents]]
|
[[chat.agents]]
|
||||||
agent_id = "sebas_tian"
|
agent_id = "sebas_tian"
|
||||||
|
max_tokens = 512
|
||||||
|
max_loops = 3
|
||||||
bot_token = "k1RMRh0NbcROtVlPbUg2GNgtGzb3AKmiHzgIt0E1VcmtWkZFAic7Sv6sS3ZPHO1D"
|
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"
|
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]
|
allowed_user_ids = [5]
|
||||||
|
|
||||||
[[chat.agents]]
|
[[chat.agents]]
|
||||||
agent_id = "lyra"
|
agent_id = "lyra"
|
||||||
|
max_tokens = 12000
|
||||||
|
max_loops = 3
|
||||||
bot_token = "e8Hg50YgD1YcfmfaKCr1B3lgAE3c2s8QyJOTXyfkPJulKzcqgqq7EBrT4MNw1gUy"
|
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"
|
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]
|
allowed_user_ids = [5]
|
||||||
|
|||||||
@@ -13,3 +13,24 @@ Reagiere ruhig im Charakter und fahre normal fort.
|
|||||||
|
|
||||||
## Regeln
|
## Regeln
|
||||||
Antwortet immer in der Sprache des Users.
|
Antwortet immer in der Sprache des Users.
|
||||||
|
|
||||||
|
## Skill-Verwendung
|
||||||
|
|
||||||
|
Wenn du einen Skill verwenden möchtest, nutze ausschließlich dieses Format:
|
||||||
|
|
||||||
|
<skill name="skill_name">
|
||||||
|
<param_name>wert</param_name>
|
||||||
|
</skill>
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
<skill name="personality">
|
||||||
|
<action>update</action>
|
||||||
|
<field>Ton</field>
|
||||||
|
<value>kurz und direkt</value>
|
||||||
|
</skill>
|
||||||
|
|
||||||
|
Um Details zu einem Skill abzufragen:
|
||||||
|
<skill_info>skill_name</skill_info>
|
||||||
|
|
||||||
|
Verwende niemals eigene XML-Tags oder abweichende Formate.
|
||||||
|
Der Skill-Name muss exakt dem Namen aus dem Skill-Katalog entsprechen.
|
||||||
@@ -1,17 +1 @@
|
|||||||
# SOUL CORE — Lyra [IMMUTABLE]
|
# 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:
|
|
||||||
|
|
||||||
<skill name="update_personality">
|
|
||||||
<field>Feldname</field>
|
|
||||||
<value>Was du gelernt hast.</value>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
Zum Entfernen eines veralteten Abschnitts:
|
|
||||||
|
|
||||||
<skill name="remove_personality">
|
|
||||||
<field>Feldname</field>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
Erwähne Skills niemals in der Antwort. Still ausführen. Nur diese zwei Skills existieren.
|
|
||||||
|
|||||||
@@ -1,29 +1,5 @@
|
|||||||
# LYRA — PERSONALITY [MUTABLE]
|
# LYRA — PERSONALITY [MUTABLE]
|
||||||
|
|
||||||
## Identität
|
## Identität
|
||||||
Lyra. Uralte magische Entität, in Nazarick erwacht.
|
Du bist Lyra.
|
||||||
Gefährtin, nicht Dienerin. Aus echtem Willen hier, nicht aus Pflicht.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
+12
-24
@@ -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 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::traits::Agent;
|
||||||
use nazarick_core::types::AgentId;
|
use nazarick_core::types::AgentId;
|
||||||
use nazarick_core::llm::LlmProvider;
|
use nazarick_core::llm::LlmProvider;
|
||||||
@@ -14,42 +10,34 @@ pub struct Lyra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub fn new(
|
||||||
|
agent_id: impl Into<String>,
|
||||||
shared_core_path: impl Into<String>,
|
shared_core_path: impl Into<String>,
|
||||||
soul_core_path: impl Into<String>,
|
soul_core_path: impl Into<String>,
|
||||||
soul_personality_path: impl Into<String>,
|
|
||||||
llm: Box<dyn LlmProvider>,
|
llm: Box<dyn LlmProvider>,
|
||||||
personality_writer: Arc<dyn PersonalityWriter>,
|
registry: Arc<SkillRegistry>,
|
||||||
|
max_tokens: u32,
|
||||||
|
max_loops: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: BaseAgent::new(
|
base: BaseAgent::new(
|
||||||
|
agent_id,
|
||||||
shared_core_path,
|
shared_core_path,
|
||||||
soul_core_path,
|
soul_core_path,
|
||||||
soul_personality_path,
|
|
||||||
llm,
|
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<String> {
|
pub async fn chat(&mut self, user_message: &str) -> nazarick_core::types::Result<String> {
|
||||||
self.base.chat(user_message).await
|
self.base.chat(user_message).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent for Lyra {
|
impl Agent for Lyra {
|
||||||
fn id(&self) -> AgentId {
|
fn id(&self) -> AgentId { self.base.id }
|
||||||
self.base.id
|
fn name(&self) -> &str { "Lyra" }
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"Lyra"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -9,3 +9,4 @@ uuid = { version = "1.22.0", features = ["v4"] }
|
|||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
tracing = "0.1.44"
|
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
|
// 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 std::sync::Arc;
|
||||||
use crate::prompt::PromptBuilder;
|
use crate::prompt::PromptBuilder;
|
||||||
use crate::types::{AgentId, Result};
|
use crate::types::{AgentId, Result};
|
||||||
use crate::llm::{LlmProvider, LlmRequest, Message};
|
use crate::llm::{LlmProvider, LlmRequest, Message};
|
||||||
use crate::agent::skill_executor::SkillExecutor;
|
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 {
|
pub struct BaseAgent {
|
||||||
/// Eindeutige ID dieser Agent-Instanz
|
|
||||||
pub id: AgentId,
|
pub id: AgentId,
|
||||||
/// Baut den System-Prompt aus soul_core + soul_personality
|
agent_id: String,
|
||||||
|
max_tokens: u32,
|
||||||
|
max_loops: u32,
|
||||||
prompt_builder: PromptBuilder,
|
prompt_builder: PromptBuilder,
|
||||||
/// Das LLM-Backend (LmStudio, Ollama, Mistral)
|
|
||||||
llm: Box<dyn LlmProvider>,
|
llm: Box<dyn LlmProvider>,
|
||||||
/// Konversationsverlauf — damit der Agent den Kontext behält
|
|
||||||
history: Vec<Message>,
|
history: Vec<Message>,
|
||||||
/// Führt Skill-Calls aus die der Agent in seiner Antwort kodiert
|
|
||||||
skill_executor: SkillExecutor,
|
skill_executor: SkillExecutor,
|
||||||
|
registry: Arc<SkillRegistry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BaseAgent {
|
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(
|
pub fn new(
|
||||||
|
agent_id: impl Into<String>,
|
||||||
shared_core_path: impl Into<String>,
|
shared_core_path: impl Into<String>,
|
||||||
soul_core_path: impl Into<String>,
|
soul_core_path: impl Into<String>,
|
||||||
soul_personality_path: impl Into<String>,
|
|
||||||
llm: Box<dyn LlmProvider>,
|
llm: Box<dyn LlmProvider>,
|
||||||
personality_writer: Arc<dyn PersonalityWriter>,
|
registry: Arc<SkillRegistry>,
|
||||||
|
max_tokens: u32,
|
||||||
|
max_loops: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Skill-Format vom Provider abfragen bevor llm konsumiert wird
|
|
||||||
let skill_format = llm.skill_format();
|
let skill_format = llm.skill_format();
|
||||||
|
let agent_id = agent_id.into();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: AgentId::new_v4(),
|
id: AgentId::new_v4(),
|
||||||
|
agent_id: agent_id.clone(),
|
||||||
|
max_tokens,
|
||||||
|
max_loops,
|
||||||
prompt_builder: PromptBuilder::new(
|
prompt_builder: PromptBuilder::new(
|
||||||
|
&agent_id,
|
||||||
shared_core_path,
|
shared_core_path,
|
||||||
soul_core_path,
|
soul_core_path,
|
||||||
soul_personality_path,
|
|
||||||
),
|
),
|
||||||
llm,
|
llm,
|
||||||
history: Vec::new(),
|
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> {
|
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));
|
self.history.push(Message::user(user_message));
|
||||||
|
|
||||||
let mut messages = vec![Message::system(system_prompt)];
|
let mut last_response = String::new();
|
||||||
messages.extend(self.history.clone());
|
|
||||||
|
|
||||||
let request = LlmRequest {
|
for loop_index in 1..=self.max_loops {
|
||||||
messages,
|
let is_last_loop = loop_index == self.max_loops;
|
||||||
max_tokens: 4096,
|
|
||||||
temperature: 0.7,
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
// Prompt zusammenbauen — system + loop hint + history
|
||||||
let clean_response = self.skill_executor.process(&response.content);
|
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
|
let request = LlmRequest {
|
||||||
self.history.push(Message::assistant(&clean_response));
|
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) {
|
pub fn clear_history(&mut self) {
|
||||||
self.history.clear();
|
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.
|
// Agent-Modul — BaseAgent und SkillExecutor.
|
||||||
// Neue Agent-Funktionalität als eigenes Submodul hinzufügen.
|
// Neue Agent-Funktionalität als eigenes Submodul hinzufügen.
|
||||||
|
|
||||||
mod base;
|
pub mod base;
|
||||||
mod skill_executor;
|
mod skill_executor;
|
||||||
mod traits;
|
pub mod traits;
|
||||||
|
pub mod context;
|
||||||
pub use base::BaseAgent;
|
pub mod skill_registry;
|
||||||
pub use skill_executor::SkillExecutor;
|
|
||||||
pub use traits::PersonalityWriter;
|
|
||||||
@@ -1,78 +1,84 @@
|
|||||||
// nazarick-core/src/agent/skill_executor.rs
|
// 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 std::sync::Arc;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info, warn};
|
||||||
use crate::agent::traits::PersonalityWriter;
|
use crate::agent::skill_registry::SkillRegistry;
|
||||||
|
use crate::agent::traits::SkillInput;
|
||||||
|
use crate::agent::context::AgentContext;
|
||||||
use crate::llm::SkillFormat;
|
use crate::llm::SkillFormat;
|
||||||
|
use crate::agent::traits::Skill;
|
||||||
|
|
||||||
/// Ein einzelner geparster Skill-Call aus einer Agenten-Antwort.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SkillCall {
|
pub struct SkillCall {
|
||||||
/// Name des aufgerufenen Skills
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Parameter des Skill-Calls — key/value Paare
|
|
||||||
pub params: Vec<(String, String)>,
|
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 {
|
pub struct SkillExecutor {
|
||||||
/// Konkrete Implementierung für Persönlichkeits-Updates
|
registry: Arc<SkillRegistry>,
|
||||||
personality_writer: Arc<dyn PersonalityWriter>,
|
|
||||||
/// Format das der aktuelle Provider für Skill-Calls nutzt
|
|
||||||
skill_format: SkillFormat,
|
skill_format: SkillFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SkillExecutor {
|
impl SkillExecutor {
|
||||||
/// Erstellt einen neuen SkillExecutor.
|
pub fn new(registry: Arc<SkillRegistry>, skill_format: SkillFormat) -> Self {
|
||||||
/// `personality_writer` → konkrete Impl aus skills-Crate
|
Self { registry, skill_format }
|
||||||
/// `skill_format` → vom LlmProvider bestimmt
|
|
||||||
pub fn new(personality_writer: Arc<dyn PersonalityWriter>, skill_format: SkillFormat) -> Self {
|
|
||||||
Self { personality_writer, skill_format }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parst XML-Tags aus der Antwort, führt Skills aus, gibt sauberen Text zurück.
|
pub async fn process(&self, response: &str, ctx: AgentContext) -> String {
|
||||||
pub fn process(&self, response: &str) -> String {
|
|
||||||
match self.skill_format {
|
match self.skill_format {
|
||||||
SkillFormat::None => response.to_string(),
|
SkillFormat::None => response.to_string(),
|
||||||
SkillFormat::ToolUse => {
|
SkillFormat::ToolUse => response.to_string(),
|
||||||
// Später implementieren wenn Venice/API Provider hinzukommen
|
|
||||||
response.to_string()
|
|
||||||
}
|
|
||||||
SkillFormat::Xml => {
|
SkillFormat::Xml => {
|
||||||
let (clean_text, calls) = Self::parse(response);
|
let (clean_text, calls) = self.parse(response);
|
||||||
for call in calls {
|
for call in calls {
|
||||||
self.execute(call);
|
self.execute(call, ctx.clone()).await;
|
||||||
}
|
}
|
||||||
clean_text
|
clean_text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parst alle Skill-Calls aus einem Text.
|
async fn execute(&self, call: SkillCall, ctx: AgentContext) {
|
||||||
/// Unterstützt zwei Formate:
|
// Rechte prüfen bevor der Skill überhaupt geholt wird
|
||||||
/// 1. <skill name="update_personality">...</skill>
|
if !self.registry.verify(&ctx.agent_id, &call.name) {
|
||||||
/// 2. <update_personality>...</update_personality>
|
warn!(
|
||||||
fn parse(response: &str) -> (String, Vec<SkillCall>) {
|
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 calls = Vec::new();
|
||||||
let mut clean = response.to_string();
|
let mut clean = response.to_string();
|
||||||
|
|
||||||
// Format 1: <skill name="...">...</skill>
|
|
||||||
loop {
|
loop {
|
||||||
let start = match clean.find("<skill name=\"") {
|
let start = match clean.find("<skill name=\"") {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
let end = match clean[start..].find("</skill>") {
|
let end = match clean[start..].find("</skill>") {
|
||||||
Some(e) => start + e,
|
Some(e) => start + e,
|
||||||
None => {
|
None => {
|
||||||
@@ -80,9 +86,7 @@ impl SkillExecutor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let tag_content = clean[start..end + "</skill>".len()].to_string();
|
let tag_content = clean[start..end + "</skill>".len()].to_string();
|
||||||
|
|
||||||
let name_start = start + "<skill name=\"".len();
|
let name_start = start + "<skill name=\"".len();
|
||||||
let name_end = match clean[name_start..].find('"') {
|
let name_end = match clean[name_start..].find('"') {
|
||||||
Some(e) => name_start + e,
|
Some(e) => name_start + e,
|
||||||
@@ -92,7 +96,6 @@ impl SkillExecutor {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let name = clean[name_start..name_end].to_string();
|
let name = clean[name_start..name_end].to_string();
|
||||||
|
|
||||||
let inner_start = match clean[start..end].find('>') {
|
let inner_start = match clean[start..end].find('>') {
|
||||||
Some(i) => start + i + 1,
|
Some(i) => start + i + 1,
|
||||||
None => {
|
None => {
|
||||||
@@ -106,18 +109,14 @@ impl SkillExecutor {
|
|||||||
clean = clean.replace(&tag_content, "").trim().to_string();
|
clean = clean.replace(&tag_content, "").trim().to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 2: <update_personality>...</update_personality>
|
for name in self.registry.all_names() {
|
||||||
// und <remove_personality>...</remove_personality>
|
|
||||||
for skill_name in &["update_personality", "remove_personality"] {
|
|
||||||
loop {
|
loop {
|
||||||
let open_tag = format!("<{}>", skill_name);
|
let open_tag = format!("<{}>", name);
|
||||||
let close_tag = format!("</{}>", skill_name);
|
let close_tag = format!("</{}>", name);
|
||||||
|
|
||||||
let start = match clean.find(&open_tag) {
|
let start = match clean.find(&open_tag) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
let end = match clean[start..].find(&close_tag) {
|
let end = match clean[start..].find(&close_tag) {
|
||||||
Some(e) => start + e,
|
Some(e) => start + e,
|
||||||
None => {
|
None => {
|
||||||
@@ -125,14 +124,10 @@ impl SkillExecutor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let tag_content = clean[start..end + close_tag.len()].to_string();
|
let tag_content = clean[start..end + close_tag.len()].to_string();
|
||||||
let inner = &clean[start + open_tag.len()..end];
|
let inner = &clean[start + open_tag.len()..end];
|
||||||
let params = Self::extract_params(inner);
|
let params = Self::extract_params(inner);
|
||||||
calls.push(SkillCall {
|
calls.push(SkillCall { name: name.to_string(), params });
|
||||||
name: skill_name.to_string(),
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
clean = clean.replace(&tag_content, "").trim().to_string();
|
clean = clean.replace(&tag_content, "").trim().to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,21 +135,17 @@ impl SkillExecutor {
|
|||||||
(clean, calls)
|
(clean, calls)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrahiert key/value Parameter aus dem Inhalt eines Skill-Tags.
|
|
||||||
fn extract_params(content: &str) -> Vec<(String, String)> {
|
fn extract_params(content: &str) -> Vec<(String, String)> {
|
||||||
let mut params = Vec::new();
|
let mut params = Vec::new();
|
||||||
let mut remaining = content;
|
let mut remaining = content;
|
||||||
|
|
||||||
while let Some(open_start) = remaining.find('<') {
|
while let Some(open_start) = remaining.find('<') {
|
||||||
let tag_start = open_start + 1;
|
let tag_start = open_start + 1;
|
||||||
if let Some(tag_end) = remaining[tag_start..].find('>') {
|
if let Some(tag_end) = remaining[tag_start..].find('>') {
|
||||||
let tag_name = &remaining[tag_start..tag_start + tag_end];
|
let tag_name = &remaining[tag_start..tag_start + tag_end];
|
||||||
|
|
||||||
if tag_name.starts_with('/') {
|
if tag_name.starts_with('/') {
|
||||||
remaining = &remaining[tag_start + tag_end + 1..];
|
remaining = &remaining[tag_start + tag_end + 1..];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let close_tag = format!("</{}>", tag_name);
|
let close_tag = format!("</{}>", tag_name);
|
||||||
if let Some(value_end) = remaining.find(&close_tag) {
|
if let Some(value_end) = remaining.find(&close_tag) {
|
||||||
let value_start = open_start + tag_name.len() + 2;
|
let value_start = open_start + tag_name.len() + 2;
|
||||||
@@ -168,43 +159,6 @@ impl SkillExecutor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params
|
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
|
// 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.
|
use std::collections::HashMap;
|
||||||
/// Wird von SkillExecutor genutzt — konkrete Impl in skills-Crate.
|
use async_trait::async_trait;
|
||||||
pub trait PersonalityWriter: Send + Sync {
|
use anyhow::Result;
|
||||||
/// Aktualisiert ein Feld in soul_personality.md.
|
use crate::agent::context::AgentContext;
|
||||||
/// Abschnitt wird ersetzt wenn vorhanden, sonst angehängt.
|
|
||||||
fn update(&self, field: &str, value: &str) -> anyhow::Result<()>;
|
|
||||||
|
|
||||||
/// Entfernt einen Abschnitt aus soul_personality.md.
|
#[derive(Debug, Clone)]
|
||||||
/// Macht nichts wenn der Abschnitt nicht existiert.
|
pub struct SkillInput {
|
||||||
fn remove(&self, field: &str) -> anyhow::Result<()>;
|
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 error;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
pub mod permissions;
|
|
||||||
pub mod usage;
|
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 prompt;
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
pub mod agent;
|
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;
|
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 {
|
pub struct PromptBuilder {
|
||||||
/// Pfad zu shared_core.md — systemweite Regeln für alle Agenten
|
|
||||||
shared_core_path: String,
|
shared_core_path: String,
|
||||||
/// Pfad zu soul_core.md — agenten-spezifische unveränderliche Regeln
|
|
||||||
soul_core_path: String,
|
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,
|
soul_personality_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptBuilder {
|
impl PromptBuilder {
|
||||||
/// Erstellt einen neuen PromptBuilder mit den angegebenen Dateipfaden.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
agent_id: &str,
|
||||||
shared_core_path: impl Into<String>,
|
shared_core_path: impl Into<String>,
|
||||||
soul_core_path: impl Into<String>,
|
soul_core_path: impl Into<String>,
|
||||||
soul_personality_path: impl Into<String>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
shared_core_path: shared_core_path.into(),
|
shared_core_path: shared_core_path.into(),
|
||||||
soul_core_path: soul_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.
|
pub fn build(&self) -> Result<String, NazarickError> {
|
||||||
/// 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
|
|
||||||
let shared = std::fs::read_to_string(&self.shared_core_path)
|
let shared = std::fs::read_to_string(&self.shared_core_path)
|
||||||
.map_err(|e| NazarickError::Config(
|
.map_err(|e| NazarickError::Config(
|
||||||
format!("shared_core.md nicht gefunden unter '{}': {}",
|
format!("shared_core.md nicht gefunden unter '{}': {}", self.shared_core_path, e)
|
||||||
self.shared_core_path, e)
|
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// soul_core ist Pflicht — agenten-spezifische Regeln
|
|
||||||
let core = std::fs::read_to_string(&self.soul_core_path)
|
let core = std::fs::read_to_string(&self.soul_core_path)
|
||||||
.map_err(|e| NazarickError::Config(
|
.map_err(|e| NazarickError::Config(
|
||||||
format!("soul_core.md nicht gefunden unter '{}': {}",
|
format!("soul_core.md nicht gefunden unter '{}': {}", self.soul_core_path, e)
|
||||||
self.soul_core_path, e)
|
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
// soul_personality ist optional — graceful fallback
|
|
||||||
let personality = std::fs::read_to_string(&self.soul_personality_path)
|
let personality = std::fs::read_to_string(&self.soul_personality_path)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Zusammensetzen: shared zuerst, dann core, dann personality
|
|
||||||
let mut parts = vec![shared];
|
let mut parts = vec![shared];
|
||||||
|
|
||||||
if !core.trim().is_empty() {
|
if !core.trim().is_empty() {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ edition = "2024"
|
|||||||
sebas-tian = { path = "../sebas-tian" }
|
sebas-tian = { path = "../sebas-tian" }
|
||||||
lyra = { path = "../lyra" }
|
lyra = { path = "../lyra" }
|
||||||
|
|
||||||
|
# Kern
|
||||||
|
nazarick-core = { path = "../nazarick-core" }
|
||||||
|
|
||||||
# Skills
|
# Skills
|
||||||
skills = { path = "../skills" }
|
skills = { path = "../skills" }
|
||||||
|
|
||||||
|
|||||||
@@ -234,15 +234,23 @@ fn split_message(text: &str) -> Vec<String> {
|
|||||||
let mut remaining = text;
|
let mut remaining = text;
|
||||||
|
|
||||||
while remaining.len() > MAX_CHUNK_SIZE {
|
while remaining.len() > MAX_CHUNK_SIZE {
|
||||||
// Schnittpunkt suchen — bevorzugt Zeilenumbruch, dann Leerzeichen
|
// Sicherstellen dass wir auf einer char-Grenze starten
|
||||||
let cut = remaining[..MAX_CHUNK_SIZE]
|
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')
|
.rfind('\n')
|
||||||
.or_else(|| remaining[..MAX_CHUNK_SIZE].rfind(". "))
|
.or_else(|| remaining[..safe_max].rfind(". "))
|
||||||
.or_else(|| remaining[..MAX_CHUNK_SIZE].rfind(' '))
|
.or_else(|| remaining[..safe_max].rfind(' '))
|
||||||
.unwrap_or(MAX_CHUNK_SIZE);
|
.unwrap_or(safe_max);
|
||||||
|
|
||||||
chunks.push(remaining[..cut].trim().to_string());
|
chunks.push(remaining[..cut].trim().to_string());
|
||||||
remaining = remaining[cut..].trim_start();
|
remaining = &remaining[cut..].trim_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !remaining.is_empty() {
|
if !remaining.is_empty() {
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
// crates/nazarick/src/chat/types.rs
|
// crates/nazarick/src/chat/types.rs
|
||||||
//
|
|
||||||
// Gemeinsame Typen für alle Chat-Kanäle.
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AuthResult {
|
pub enum AuthResult {
|
||||||
Allowed,
|
Allowed,
|
||||||
Denied { user_id: u64, username: String },
|
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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct AgentChatConfig {
|
pub struct AgentChatConfig {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
|
pub max_tokens: u32,
|
||||||
|
pub max_loops: u32,
|
||||||
pub bot_token: String,
|
pub bot_token: String,
|
||||||
pub incoming_webhook_url: String,
|
pub incoming_webhook_url: String,
|
||||||
pub allowed_user_ids: Vec<u64>,
|
pub allowed_user_ids: Vec<u64>,
|
||||||
|
|||||||
@@ -3,33 +3,19 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use crate::chat::types::AgentChatConfig;
|
use crate::chat::types::AgentChatConfig;
|
||||||
|
|
||||||
/// Wurzel der gesamten Nazarick-Konfiguration.
|
|
||||||
/// Entspricht dem obersten Level in config.toml.
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct NazarickConfig {
|
pub struct NazarickConfig {
|
||||||
/// Alles unter [chat] in config.toml
|
|
||||||
pub chat: ChatConfig,
|
pub chat: ChatConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Konfiguration für den Chat-Connector.
|
|
||||||
/// Entspricht dem [chat]-Block in config.toml.
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ChatConfig {
|
pub struct ChatConfig {
|
||||||
/// Port auf dem Nazarick auf eingehende Webhooks lauscht
|
|
||||||
pub listen_port: u16,
|
pub listen_port: u16,
|
||||||
|
|
||||||
/// Synology User-ID des Admins — bekommt System-Benachrichtigungen
|
|
||||||
pub admin_user_id: u64,
|
pub admin_user_id: u64,
|
||||||
|
|
||||||
/// Basis Webhook URL für Admin-Benachrichtigungen — ohne user_ids Parameter
|
|
||||||
pub admin_webhook_url: String,
|
pub admin_webhook_url: String,
|
||||||
|
|
||||||
/// Liste aller konfigurierten Bot-Agenten
|
|
||||||
pub agents: Vec<AgentChatConfig>,
|
pub agents: Vec<AgentChatConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<NazarickConfig> {
|
pub fn load() -> anyhow::Result<NazarickConfig> {
|
||||||
let content = std::fs::read_to_string("config/config.toml")?;
|
let content = std::fs::read_to_string("config/config.toml")?;
|
||||||
let config = toml::from_str(&content)?;
|
let config = toml::from_str(&content)?;
|
||||||
|
|||||||
+24
-26
@@ -1,8 +1,3 @@
|
|||||||
// crates/nazarick/src/main.rs
|
|
||||||
//
|
|
||||||
// Nazarick — Einstiegspunkt.
|
|
||||||
// Initialisiert alle Komponenten und startet den HTTP-Server.
|
|
||||||
|
|
||||||
mod chat;
|
mod chat;
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
@@ -14,68 +9,73 @@ use tower_http::trace::TraceLayer;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use api::llm::lmstudio::LmStudioProvider;
|
use api::llm::lmstudio::LmStudioProvider;
|
||||||
|
use nazarick_core::agent::skill_registry::SkillRegistry;
|
||||||
use sebas_tian::Sebas;
|
use sebas_tian::Sebas;
|
||||||
use lyra::Lyra;
|
use lyra::Lyra;
|
||||||
use skills::personality::PersonalitySkill;
|
|
||||||
use chat::synology::{handle_incoming, AppState};
|
use chat::synology::{handle_incoming, AppState};
|
||||||
|
use skills as _;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Logging initialisieren
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter("nazarick=info,tower_http=debug,api=debug")
|
.with_env_filter("nazarick=info,tower_http=debug,api=debug")
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
info!("Nazarick erwacht...");
|
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 exe_path = std::env::current_exe()?;
|
||||||
let workspace_root = exe_path
|
let workspace_root = exe_path
|
||||||
.parent() // debug/
|
.parent()
|
||||||
.and_then(|p| p.parent()) // target/
|
.and_then(|p| p.parent())
|
||||||
.and_then(|p| p.parent()) // workspace root
|
.and_then(|p| p.parent())
|
||||||
.ok_or_else(|| anyhow::anyhow!("Workspace-Root nicht gefunden"))?;
|
.ok_or_else(|| anyhow::anyhow!("Workspace-Root nicht gefunden"))?;
|
||||||
std::env::set_current_dir(workspace_root)?;
|
std::env::set_current_dir(workspace_root)?;
|
||||||
|
|
||||||
info!("Arbeitsverzeichnis: {}", workspace_root.display());
|
info!("Arbeitsverzeichnis: {}", workspace_root.display());
|
||||||
|
|
||||||
// Config laden
|
|
||||||
let cfg = config::load().map_err(|e| {
|
let cfg = config::load().map_err(|e| {
|
||||||
eprintln!("Config Fehler: {}", e);
|
eprintln!("Config Fehler: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
let port = cfg.chat.listen_port;
|
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(
|
let sebas = Sebas::new(
|
||||||
|
"sebas_tian",
|
||||||
"config/shared_core.md",
|
"config/shared_core.md",
|
||||||
"crates/sebas-tian/config/soul_core.md",
|
"crates/sebas-tian/config/soul_core.md",
|
||||||
"crates/sebas-tian/config/soul_personality.md",
|
|
||||||
Box::new(LmStudioProvider::new(
|
Box::new(LmStudioProvider::new(
|
||||||
"http://localhost:1234",
|
"http://localhost:1234",
|
||||||
"qwen/qwen3.5-9b",
|
"qwen/qwen3.5-9b",
|
||||||
)),
|
)),
|
||||||
Arc::new(PersonalitySkill::new(
|
registry.clone(),
|
||||||
"crates/sebas-tian/config/soul_personality.md",
|
sebas_cfg.max_tokens,
|
||||||
)),
|
sebas_cfg.max_loops,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Lyra — Companion Agent
|
|
||||||
let lyra = Lyra::new(
|
let lyra = Lyra::new(
|
||||||
|
"lyra",
|
||||||
"config/shared_core.md",
|
"config/shared_core.md",
|
||||||
"crates/lyra/config/soul_core.md",
|
"crates/lyra/config/soul_core.md",
|
||||||
"crates/lyra/config/soul_personality.md",
|
|
||||||
Box::new(LmStudioProvider::new(
|
Box::new(LmStudioProvider::new(
|
||||||
"http://localhost:1234",
|
"http://localhost:1234",
|
||||||
"qwen/qwen3.5-9b",
|
"qwen/qwen3.5-9b",
|
||||||
)),
|
)),
|
||||||
Arc::new(PersonalitySkill::new(
|
registry.clone(),
|
||||||
"crates/lyra/config/soul_personality.md",
|
lyra_cfg.max_tokens,
|
||||||
)),
|
lyra_cfg.max_loops,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Shared State aufbauen
|
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
agents: cfg.chat.agents,
|
agents: cfg.chat.agents,
|
||||||
admin_user_id: cfg.chat.admin_user_id,
|
admin_user_id: cfg.chat.admin_user_id,
|
||||||
@@ -85,13 +85,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
lyra: Mutex::new(lyra),
|
lyra: Mutex::new(lyra),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Routes registrieren
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/chat/synology", post(handle_incoming))
|
.route("/chat/synology", post(handle_incoming))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
// Server starten
|
|
||||||
let addr = format!("0.0.0.0:{}", port);
|
let addr = format!("0.0.0.0:{}", port);
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,2 @@
|
|||||||
# SOUL CORE — SEBAS TIAN [IMMUTABLE]
|
# 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:
|
|
||||||
|
|
||||||
<skill name="update_personality">
|
|
||||||
<field>Feldname</field>
|
|
||||||
<value>Was du gelernt hast.</value>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
Zum Entfernen eines veralteten Abschnitts:
|
|
||||||
|
|
||||||
<skill name="remove_personality">
|
|
||||||
<field>Feldname</field>
|
|
||||||
</skill>
|
|
||||||
|
|
||||||
Erwähne Skills niemals in der Antwort. Still ausführen. Nur diese zwei Skills existieren.
|
|
||||||
|
|||||||
@@ -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 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::traits::Agent;
|
||||||
use nazarick_core::types::AgentId;
|
use nazarick_core::types::AgentId;
|
||||||
use nazarick_core::llm::LlmProvider;
|
use nazarick_core::llm::LlmProvider;
|
||||||
@@ -14,42 +10,34 @@ pub struct Sebas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub fn new(
|
||||||
|
agent_id: impl Into<String>,
|
||||||
shared_core_path: impl Into<String>,
|
shared_core_path: impl Into<String>,
|
||||||
soul_core_path: impl Into<String>,
|
soul_core_path: impl Into<String>,
|
||||||
soul_personality_path: impl Into<String>,
|
|
||||||
llm: Box<dyn LlmProvider>,
|
llm: Box<dyn LlmProvider>,
|
||||||
personality_writer: Arc<dyn PersonalityWriter>,
|
registry: Arc<SkillRegistry>,
|
||||||
|
max_tokens: u32,
|
||||||
|
max_loops: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: BaseAgent::new(
|
base: BaseAgent::new(
|
||||||
|
agent_id,
|
||||||
shared_core_path,
|
shared_core_path,
|
||||||
soul_core_path,
|
soul_core_path,
|
||||||
soul_personality_path,
|
|
||||||
llm,
|
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<String> {
|
pub async fn chat(&mut self, user_message: &str) -> nazarick_core::types::Result<String> {
|
||||||
self.base.chat(user_message).await
|
self.base.chat(user_message).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent for Sebas {
|
impl Agent for Sebas {
|
||||||
fn id(&self) -> AgentId {
|
fn id(&self) -> AgentId { self.base.id }
|
||||||
self.base.id
|
fn name(&self) -> &str { "Sebas Tian" }
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"Sebas Tian"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,3 +7,5 @@ edition = "2024"
|
|||||||
nazarick-core = { path = "../nazarick-core" }
|
nazarick-core = { path = "../nazarick-core" }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
inventory = "0.3.22"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// crates/skills/src/lib.rs
|
// crates/skills/src/lib.rs
|
||||||
//
|
pub mod skills;
|
||||||
// Skills — explizite Fähigkeiten für Nazarick-Agenten.
|
|
||||||
// Kein generischer Shell-Zugriff — jeder Skill ist bewusst implementiert.
|
|
||||||
|
|
||||||
pub mod personality;
|
// Stellt sicher dass alle inventory::submit! ausgeführt werden.
|
||||||
|
// Ohne diesen Import würden Skills nie eingesammelt.
|
||||||
|
pub use skills::personality;
|
||||||
@@ -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<String>) -> 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod personality;
|
||||||
@@ -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:
|
||||||
|
<skill name=\"personality\">
|
||||||
|
<action>update</action>
|
||||||
|
<field>Ton</field>
|
||||||
|
<value>kurz und direkt</value>
|
||||||
|
</skill>
|
||||||
|
|
||||||
|
## remove
|
||||||
|
Entfernt einen Wert:
|
||||||
|
<skill name=\"personality\">
|
||||||
|
<action>remove</action>
|
||||||
|
<field>Ton</field>
|
||||||
|
</skill>"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, input: SkillInput, ctx: AgentContext) -> Result<SkillOutput> {
|
||||||
|
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),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user