added Tokens, openrouter, memory system
This commit is contained in:
@@ -1,23 +1,37 @@
|
||||
// nazarick-core/src/agent/base.rs
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::spawn;
|
||||
use tracing::{info, warn};
|
||||
use crate::prompt::PromptBuilder;
|
||||
use crate::types::{AgentId, Result};
|
||||
use crate::llm::{LlmProvider, LlmRequest, Message};
|
||||
use crate::error::NazarickError;
|
||||
use crate::llm::{LlmProvider, LlmRequest, Message, SkillFormat};
|
||||
use crate::agent::skill_executor::SkillExecutor;
|
||||
use crate::agent::context::AgentContext;
|
||||
use crate::agent::skill_registry::SkillRegistry;
|
||||
use crate::memory::Memory;
|
||||
use crate::summarizer::Summarizer;
|
||||
|
||||
pub struct BaseAgent {
|
||||
pub id: AgentId,
|
||||
agent_id: String,
|
||||
max_tokens: u32,
|
||||
max_loops: u32,
|
||||
history_window: usize,
|
||||
summary_every: usize,
|
||||
conversation_timeout_mins: u64,
|
||||
conversation_id: i64,
|
||||
messages_since_summary: usize,
|
||||
prompt_builder: PromptBuilder,
|
||||
llm: Box<dyn LlmProvider>,
|
||||
/// Nur echte User/Assistant Nachrichten
|
||||
history: Vec<Message>,
|
||||
skill_executor: SkillExecutor,
|
||||
registry: Arc<SkillRegistry>,
|
||||
memory: Arc<dyn Memory>,
|
||||
summarizer: Arc<dyn Summarizer>,
|
||||
skill_format: SkillFormat,
|
||||
}
|
||||
|
||||
impl BaseAgent {
|
||||
@@ -27,8 +41,13 @@ impl BaseAgent {
|
||||
soul_core_path: impl Into<String>,
|
||||
llm: Box<dyn LlmProvider>,
|
||||
registry: Arc<SkillRegistry>,
|
||||
memory: Arc<dyn Memory>,
|
||||
summarizer: Arc<dyn Summarizer>,
|
||||
max_tokens: u32,
|
||||
max_loops: u32,
|
||||
history_window: usize,
|
||||
summary_every: usize,
|
||||
conversation_timeout_mins: u64,
|
||||
) -> Self {
|
||||
let skill_format = llm.skill_format();
|
||||
let agent_id = agent_id.into();
|
||||
@@ -38,101 +57,317 @@ impl BaseAgent {
|
||||
agent_id: agent_id.clone(),
|
||||
max_tokens,
|
||||
max_loops,
|
||||
history_window,
|
||||
summary_every,
|
||||
conversation_timeout_mins,
|
||||
conversation_id: 0,
|
||||
messages_since_summary: 0,
|
||||
prompt_builder: PromptBuilder::new(
|
||||
&agent_id,
|
||||
shared_core_path,
|
||||
soul_core_path,
|
||||
),
|
||||
skill_executor: SkillExecutor::new(registry.clone(), skill_format.clone()),
|
||||
skill_format,
|
||||
llm,
|
||||
history: Vec::new(),
|
||||
skill_executor: SkillExecutor::new(registry.clone(), skill_format),
|
||||
registry,
|
||||
memory,
|
||||
summarizer,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn chat(&mut self, user_message: &str) -> Result<String> {
|
||||
let ctx = AgentContext { agent_id: self.agent_id.clone() };
|
||||
pub async fn init(&mut self) -> Result<()> {
|
||||
let conv_id = self.memory
|
||||
.get_or_create_conversation(self.conversation_timeout_mins)
|
||||
.await
|
||||
.map_err(|e| NazarickError::Memory(e.to_string()))?;
|
||||
self.conversation_id = conv_id;
|
||||
|
||||
let messages = self.memory
|
||||
.load_window(conv_id, self.history_window)
|
||||
.await
|
||||
.map_err(|e| NazarickError::Memory(e.to_string()))?;
|
||||
|
||||
self.messages_since_summary = messages.len();
|
||||
self.history = messages.into_iter()
|
||||
.map(|m| match m.role.as_str() {
|
||||
"user" => Message::user(&m.content),
|
||||
_ => Message::assistant(&m.content),
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!(agent = %self.agent_id, conversation_id = %self.conversation_id,
|
||||
messages = %self.history.len(), "Agent initialisiert");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn chat(&mut self, user_message: &str) -> Result<String> {
|
||||
self.maybe_rolling_summary().await;
|
||||
|
||||
let ctx = AgentContext {
|
||||
agent_id: self.agent_id.clone(),
|
||||
memory: self.memory.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);
|
||||
|
||||
match self.skill_format {
|
||||
SkillFormat::Xml => {
|
||||
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);
|
||||
system_prompt.push_str("\n\n## Skill-Verwendung\n");
|
||||
system_prompt.push_str("Nutze ausschließlich dieses Format:\n");
|
||||
system_prompt.push_str("<skill name=\"skill_name\">\n");
|
||||
system_prompt.push_str(" <param>wert</param>\n");
|
||||
system_prompt.push_str("</skill>\n\n");
|
||||
system_prompt.push_str("Beispiele:\n");
|
||||
system_prompt.push_str("<skill name=\"personality\">\n");
|
||||
system_prompt.push_str(" <action>update</action>\n");
|
||||
system_prompt.push_str(" <field>Ton</field>\n");
|
||||
system_prompt.push_str(" <value>kurz und direkt</value>\n");
|
||||
system_prompt.push_str("</skill>\n\n");
|
||||
system_prompt.push_str("<skill name=\"remember\">\n");
|
||||
system_prompt.push_str(" <action>update</action>\n");
|
||||
system_prompt.push_str(" <category>persönlich</category>\n");
|
||||
system_prompt.push_str(" <key>name</key>\n");
|
||||
system_prompt.push_str(" <value>Thomas</value>\n");
|
||||
system_prompt.push_str("</skill>\n");
|
||||
system_prompt.push_str("\nFür Details: <skill_info>skill_name</skill_info>");
|
||||
}
|
||||
}
|
||||
SkillFormat::ToolUse => {
|
||||
let names: Vec<&str> = self.registry.all_names();
|
||||
if !names.is_empty() {
|
||||
system_prompt.push_str("\n\n=== Verfügbare Skills ===\n");
|
||||
for name in &names {
|
||||
if let Some(skill) = self.registry.get(name) {
|
||||
system_prompt.push_str(&format!(
|
||||
"- {}: {}\n", name, skill.summary()
|
||||
));
|
||||
}
|
||||
}
|
||||
system_prompt.push_str(
|
||||
"\nNutze Tools direkt wenn nötig. Nicht auflisten."
|
||||
);
|
||||
}
|
||||
}
|
||||
SkillFormat::None => {}
|
||||
}
|
||||
|
||||
let summaries = self.memory.category_summaries().await
|
||||
.unwrap_or_default();
|
||||
if !summaries.is_empty() {
|
||||
system_prompt.push_str("\n\n## Bekannte Fakten-Kategorien\n");
|
||||
for s in &summaries {
|
||||
system_prompt.push_str(&format!("- {} ({} Einträge)\n", s.category, s.count));
|
||||
}
|
||||
system_prompt.push_str(
|
||||
"\nNutze <skill_info>remember</skill_info> um Details zu sehen."
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(Some(summary)) = self.memory.last_summary().await {
|
||||
system_prompt.push_str(&format!("\n\n## Vorheriges Gespräch\n{}", summary));
|
||||
}
|
||||
|
||||
// User-Nachricht zur History hinzufügen
|
||||
self.history.push(Message::user(user_message));
|
||||
self.messages_since_summary += 1;
|
||||
{
|
||||
let memory = self.memory.clone();
|
||||
let conv_id = self.conversation_id;
|
||||
let content = user_message.to_string();
|
||||
spawn(async move {
|
||||
let _ = memory.save_message(conv_id, "user", &content).await;
|
||||
});
|
||||
}
|
||||
|
||||
let tools = match self.skill_format {
|
||||
SkillFormat::ToolUse => {
|
||||
let defs = self.registry.tool_definitions(&self.agent_id);
|
||||
if defs.is_empty() { None } else { Some(defs) }
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut last_response = String::new();
|
||||
let mut loop_context: Vec<Message> = Vec::new();
|
||||
|
||||
for loop_index in 1..=self.max_loops {
|
||||
let is_last_loop = loop_index == self.max_loops;
|
||||
|
||||
// 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
|
||||
)
|
||||
"Antworte jetzt direkt dem User.".to_string()
|
||||
} 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
|
||||
)
|
||||
"Führe nötige Skills aus und antworte dann direkt.".to_string()
|
||||
};
|
||||
|
||||
// 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)];
|
||||
let mut messages = vec![Message::system(system_prompt.clone())];
|
||||
messages.extend(self.history.clone());
|
||||
messages.extend(loop_context.clone());
|
||||
messages.push(Message::system(loop_hint));
|
||||
|
||||
let request = LlmRequest {
|
||||
messages,
|
||||
max_tokens: self.max_tokens,
|
||||
temperature: 0.7,
|
||||
tools: tools.clone(),
|
||||
};
|
||||
|
||||
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) {
|
||||
// Usage fire-and-forget loggen
|
||||
{
|
||||
let memory = self.memory.clone();
|
||||
let t_in = response.tokens_input;
|
||||
let t_out = response.tokens_output;
|
||||
let cost = response.cost;
|
||||
let finish = if response.tool_calls.is_some() {
|
||||
"tool_calls"
|
||||
} else {
|
||||
"stop"
|
||||
}.to_string();
|
||||
spawn(async move {
|
||||
let _ = memory.log_usage(t_in, t_out, cost, Some(&finish)).await;
|
||||
});
|
||||
}
|
||||
|
||||
let raw = response.content.clone();
|
||||
let tool_calls = response.tool_calls.clone();
|
||||
let clean_raw = Self::strip_thinking(&raw);
|
||||
|
||||
// Leere Antwort überspringen
|
||||
if clean_raw.is_empty() && tool_calls.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(skill_name) = Self::parse_skill_info(&clean_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));
|
||||
loop_context.push(Message::assistant(&clean_raw));
|
||||
loop_context.push(Message::user(&details));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skill-Calls ausführen — sauberen Text zurückbekommen
|
||||
let clean = self.skill_executor.process(&raw, ctx.clone()).await;
|
||||
let (clean, feedback) = self.skill_executor.process(
|
||||
&clean_raw,
|
||||
tool_calls,
|
||||
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;
|
||||
if let Some(fb) = feedback {
|
||||
loop_context.push(Message::assistant(&clean));
|
||||
loop_context.push(Message::user(format!("[Skill Feedback]\n{}", fb)));
|
||||
last_response = clean;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skill wurde ausgeführt — nächste Iteration
|
||||
last_response = clean.clone();
|
||||
self.history.push(Message::assistant(&clean));
|
||||
self.messages_since_summary += 1;
|
||||
{
|
||||
let memory = self.memory.clone();
|
||||
let conv_id = self.conversation_id;
|
||||
let content = clean.clone();
|
||||
spawn(async move {
|
||||
let _ = memory.save_message(conv_id, "assistant", &content).await;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback — Agent hat nur Skills aufgerufen ohne zu antworten
|
||||
if last_response.is_empty() {
|
||||
let mut messages = vec![Message::system(system_prompt.clone())];
|
||||
messages.extend(self.history.clone());
|
||||
messages.push(Message::system(
|
||||
"Skills wurden ausgeführt. Antworte jetzt direkt dem User.".to_string()
|
||||
));
|
||||
let request = LlmRequest {
|
||||
messages,
|
||||
max_tokens: self.max_tokens,
|
||||
temperature: 0.7,
|
||||
tools: None,
|
||||
};
|
||||
if let Ok(response) = self.llm.complete(request).await {
|
||||
// Usage loggen
|
||||
{
|
||||
let memory = self.memory.clone();
|
||||
let t_in = response.tokens_input;
|
||||
let t_out = response.tokens_output;
|
||||
let cost = response.cost;
|
||||
spawn(async move {
|
||||
let _ = memory.log_usage(t_in, t_out, cost, Some("fallback")).await;
|
||||
});
|
||||
}
|
||||
last_response = Self::strip_thinking(&response.content);
|
||||
self.history.push(Message::assistant(&last_response));
|
||||
let memory = self.memory.clone();
|
||||
let conv_id = self.conversation_id;
|
||||
let content = last_response.clone();
|
||||
spawn(async move {
|
||||
let _ = memory.save_message(conv_id, "assistant", &content).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(last_response)
|
||||
}
|
||||
|
||||
/// Parst <skill_info>skill_name</skill_info> aus einer Antwort.
|
||||
fn strip_thinking(text: &str) -> String {
|
||||
let mut result = text.to_string();
|
||||
while let Some(start) = result.find("<think>") {
|
||||
if let Some(end) = result.find("</think>") {
|
||||
let tag = result[start..end + "</think>".len()].to_string();
|
||||
result = result.replace(&tag, "");
|
||||
} else {
|
||||
result = result[..start].to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
result.trim().to_string()
|
||||
}
|
||||
|
||||
async fn maybe_rolling_summary(&mut self) {
|
||||
if self.messages_since_summary < self.summary_every {
|
||||
return;
|
||||
}
|
||||
|
||||
let to_summarize: Vec<(String, String)> = self.history.iter()
|
||||
.map(|m| (m.role.clone(), m.content.clone()))
|
||||
.collect();
|
||||
|
||||
if to_summarize.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let summarizer = self.summarizer.clone();
|
||||
let memory = self.memory.clone();
|
||||
let conv_id = self.conversation_id;
|
||||
let agent_id = self.agent_id.clone();
|
||||
|
||||
spawn(async move {
|
||||
match summarizer.summarize(&to_summarize).await {
|
||||
Ok(summary) => {
|
||||
let _ = memory.close_conversation(conv_id, Some(&summary)).await;
|
||||
info!(agent = %agent_id, "Rolling Summary erstellt");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent = %agent_id, error = %e, "Rolling Summary fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.messages_since_summary = 0;
|
||||
}
|
||||
|
||||
fn parse_skill_info(response: &str) -> Option<String> {
|
||||
let open = "<skill_info>";
|
||||
let close = "</skill_info>";
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// nazarick-core/src/agent/context.rs
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
use std::sync::Arc;
|
||||
use crate::memory::Memory;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AgentContext {
|
||||
pub agent_id: String,
|
||||
pub memory: Arc<dyn Memory>,
|
||||
}
|
||||
@@ -5,8 +5,7 @@ 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;
|
||||
use crate::llm::{SkillFormat, ToolCall};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SkillCall {
|
||||
@@ -24,34 +23,78 @@ impl SkillExecutor {
|
||||
Self { registry, skill_format }
|
||||
}
|
||||
|
||||
pub async fn process(&self, response: &str, ctx: AgentContext) -> String {
|
||||
pub async fn process(
|
||||
&self,
|
||||
response: &str,
|
||||
tool_calls: Option<Vec<ToolCall>>,
|
||||
ctx: AgentContext,
|
||||
) -> (String, Option<String>) {
|
||||
match self.skill_format {
|
||||
SkillFormat::None => response.to_string(),
|
||||
SkillFormat::ToolUse => response.to_string(),
|
||||
SkillFormat::None => (response.to_string(), None),
|
||||
SkillFormat::Xml => {
|
||||
let (clean_text, calls) = self.parse(response);
|
||||
let mut feedback: Option<String> = None;
|
||||
for call in calls {
|
||||
self.execute(call, ctx.clone()).await;
|
||||
if let Some(fb) = self.execute_call(call, ctx.clone()).await {
|
||||
match feedback {
|
||||
Some(ref mut existing) => {
|
||||
existing.push('\n');
|
||||
existing.push_str(&fb);
|
||||
}
|
||||
None => feedback = Some(fb),
|
||||
}
|
||||
}
|
||||
}
|
||||
clean_text
|
||||
(clean_text, feedback)
|
||||
}
|
||||
SkillFormat::ToolUse => {
|
||||
let Some(calls) = tool_calls else {
|
||||
return (response.to_string(), None);
|
||||
};
|
||||
|
||||
let mut feedback: Option<String> = None;
|
||||
for call in calls {
|
||||
let params: std::collections::HashMap<String, String> =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(
|
||||
&call.function.arguments
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| {
|
||||
v.as_str().map(|s| (k.clone(), s.to_string()))
|
||||
.or_else(|| Some((k, v.to_string())))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let skill_call = SkillCall {
|
||||
name: call.function.name.clone(),
|
||||
params: params.into_iter().collect(),
|
||||
};
|
||||
|
||||
if let Some(fb) = self.execute_call(skill_call, ctx.clone()).await {
|
||||
match feedback {
|
||||
Some(ref mut existing) => {
|
||||
existing.push('\n');
|
||||
existing.push_str(&fb);
|
||||
}
|
||||
None => feedback = Some(fb),
|
||||
}
|
||||
}
|
||||
}
|
||||
(response.to_string(), feedback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(&self, call: SkillCall, ctx: AgentContext) {
|
||||
// Rechte prüfen bevor der Skill überhaupt geholt wird
|
||||
async fn execute_call(&self, call: SkillCall, ctx: AgentContext) -> Option<String> {
|
||||
if !self.registry.verify(&ctx.agent_id, &call.name) {
|
||||
warn!(
|
||||
skill = %call.name,
|
||||
agent = %ctx.agent_id,
|
||||
"Skill-Aufruf verweigert — keine Berechtigung"
|
||||
);
|
||||
return;
|
||||
warn!(skill = %call.name, agent = %ctx.agent_id, "Skill-Aufruf verweigert");
|
||||
return Some(format!("Skill '{}' ist nicht erlaubt.", call.name));
|
||||
}
|
||||
|
||||
let Some(skill): Option<Arc<dyn Skill>> = self.registry.get(&call.name) else {
|
||||
let Some(skill) = self.registry.get(&call.name) else {
|
||||
warn!(skill = %call.name, "Skill nicht gefunden");
|
||||
return;
|
||||
return Some(format!("Skill '{}' existiert nicht.", call.name));
|
||||
};
|
||||
|
||||
let params = call.params.into_iter().collect();
|
||||
@@ -60,12 +103,15 @@ impl SkillExecutor {
|
||||
match skill.execute(input, ctx).await {
|
||||
Ok(output) if output.success => {
|
||||
info!(skill = %call.name, "{}", output.message);
|
||||
output.feedback
|
||||
}
|
||||
Ok(output) => {
|
||||
error!(skill = %call.name, "Fehlgeschlagen: {}", output.message);
|
||||
output.feedback
|
||||
}
|
||||
Err(e) => {
|
||||
error!(skill = %call.name, error = %e, "Skill abgebrochen");
|
||||
Some(format!("Skill '{}' Fehler: {}. Bitte korrigiere den Aufruf.", call.name, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// nazarick-core/src/agent/skill_registry.rs
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
@@ -6,8 +8,6 @@ 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>,
|
||||
}
|
||||
@@ -45,6 +45,7 @@ impl SkillRegistry {
|
||||
self.skills.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Prompt-Block für XML Format — nur Namen + Summary
|
||||
pub fn prompt_block(&self, agent_id: &str) -> String {
|
||||
let skills: Vec<_> = self.skills.values()
|
||||
.filter(|meta| Self::is_allowed(meta, agent_id))
|
||||
@@ -63,16 +64,32 @@ impl SkillRegistry {
|
||||
"[fire-and-forget]"
|
||||
};
|
||||
block.push_str(&format!(
|
||||
"- {} {}: {}\n",
|
||||
meta.name, mode, instance.summary()
|
||||
"- {} {}: {}\n", meta.name, mode, instance.summary()
|
||||
));
|
||||
}
|
||||
block.push_str(
|
||||
"\nFür Details und Verwendung eines Skills:\n<skill_info>skill_name</skill_info>"
|
||||
"\nFür Details: <skill_info>skill_name</skill_info>"
|
||||
);
|
||||
block
|
||||
}
|
||||
|
||||
/// Tool Definitions für ToolUse Format — JSON Schema Array
|
||||
/// Wird direkt in den API Request eingebettet
|
||||
pub fn tool_definitions(&self, agent_id: &str) -> Vec<serde_json::Value> {
|
||||
self.skills.values()
|
||||
.filter(|meta| Self::is_allowed(meta, agent_id))
|
||||
.map(|meta| (meta.skill)().tool_definition())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Gibt awaits_result für einen Skill zurück
|
||||
/// Wird vom Executor genutzt um zu entscheiden ob Feedback erwartet wird
|
||||
pub fn awaits_result(&self, skill_name: &str) -> bool {
|
||||
self.skills.get(skill_name)
|
||||
.map(|meta| meta.awaits_result)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_allowed(meta: &SkillMeta, agent_id: &str) -> bool {
|
||||
meta.allowed.contains(&"all") || meta.allowed.contains(&agent_id)
|
||||
}
|
||||
|
||||
@@ -27,20 +27,36 @@ impl SkillInput {
|
||||
pub struct SkillOutput {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub feedback: Option<String>,
|
||||
}
|
||||
|
||||
impl SkillOutput {
|
||||
pub fn ok(msg: impl Into<String>) -> Self {
|
||||
Self { success: true, message: msg.into() }
|
||||
Self { success: true, message: msg.into(), feedback: None }
|
||||
}
|
||||
pub fn ok_with_feedback(msg: impl Into<String>, feedback: impl Into<String>) -> Self {
|
||||
Self { success: true, message: msg.into(), feedback: Some(feedback.into()) }
|
||||
}
|
||||
pub fn err(msg: impl Into<String>) -> Self {
|
||||
Self { success: false, message: msg.into() }
|
||||
let msg = msg.into();
|
||||
Self { success: false, feedback: Some(format!(
|
||||
"Skill fehlgeschlagen: {}. Bitte korrigiere den Aufruf.", msg
|
||||
)), message: msg }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Skill: Send + Sync {
|
||||
/// Kurze Beschreibung für den Skill-Katalog im Prompt
|
||||
fn summary(&self) -> &str;
|
||||
|
||||
/// Vollständige Beschreibung — wird bei skill_info Anfrage zurückgegeben
|
||||
fn details(&self) -> &str;
|
||||
|
||||
/// JSON Schema für Function Calling (ToolUse Format)
|
||||
/// Wird in den API Request als Tool Definition eingebettet
|
||||
fn tool_definition(&self) -> serde_json::Value;
|
||||
|
||||
/// Führt den Skill aus
|
||||
async fn execute(&self, input: SkillInput, ctx: AgentContext) -> Result<SkillOutput>;
|
||||
}
|
||||
@@ -4,4 +4,6 @@ pub mod traits;
|
||||
pub mod usage;
|
||||
pub mod prompt;
|
||||
pub mod llm;
|
||||
pub mod agent;
|
||||
pub mod agent;
|
||||
pub mod memory;
|
||||
pub mod summarizer;
|
||||
@@ -1,10 +1,7 @@
|
||||
// nazarick-core/src/llm/mod.rs
|
||||
//
|
||||
// LLM-Modul — Typen und Traits für alle LLM-Provider.
|
||||
// Re-exportiert alles damit Nutzer nur `nazarick_core::llm::X` schreiben müssen.
|
||||
|
||||
mod types;
|
||||
mod traits;
|
||||
|
||||
pub use types::{Message, LlmRequest, LlmResponse};
|
||||
pub use types::{Message, LlmRequest, LlmResponse, ToolCall, ToolCallFunction};
|
||||
pub use traits::{LlmProvider, SkillFormat};
|
||||
@@ -1,34 +1,33 @@
|
||||
// nazarick-core/src/llm/traits.rs
|
||||
//
|
||||
// LlmProvider Trait — gemeinsame Schnittstelle für alle LLM-Backends.
|
||||
// Neue Provider (Ollama, Mistral) implementieren diesen Trait.
|
||||
|
||||
use crate::types::Result;
|
||||
use crate::llm::types::{LlmRequest, LlmResponse};
|
||||
|
||||
/// Format für Skill-Calls das dieser Provider unterstützt.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SkillFormat {
|
||||
/// XML-Tags — funktioniert mit lokalen Modellen
|
||||
/// <skill name="update_personality">...</skill>
|
||||
/// XML-Tags — für lokale Modelle ohne Function Calling
|
||||
Xml,
|
||||
/// Native Tool Use — Claude, GPT-4, Mistral API
|
||||
/// Strukturierter JSON-basierter Funktionsaufruf
|
||||
/// Native Tool Use — Ollama, Mistral API, OpenRouter
|
||||
ToolUse,
|
||||
/// Skills deaktiviert — Modell folgt keinem Format zuverlässig
|
||||
/// Skills deaktiviert
|
||||
None,
|
||||
}
|
||||
|
||||
impl SkillFormat {
|
||||
/// Parsed aus config.toml String
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"tool_use" => Self::ToolUse,
|
||||
"none" => Self::None,
|
||||
_ => Self::Xml, // default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
/// Sendet eine Anfrage an das LLM und gibt die Antwort zurück.
|
||||
async fn complete(&self, request: LlmRequest) -> Result<LlmResponse>;
|
||||
|
||||
/// Gibt den Namen des Providers zurück.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Gibt das Skill-Format zurück das dieser Provider unterstützt.
|
||||
/// Standard: Xml — für lokale Modelle.
|
||||
fn skill_format(&self) -> SkillFormat {
|
||||
SkillFormat::Xml
|
||||
}
|
||||
|
||||
@@ -1,53 +1,56 @@
|
||||
// nazarick-core/src/llm/types.rs
|
||||
//
|
||||
// Gemeinsame Datentypen für alle LLM-Provider.
|
||||
// Jeder Provider (LmStudio, Ollama, Mistral) nutzt diese Typen.
|
||||
|
||||
/// Repräsentiert eine einzelne Nachricht in einem Gespräch.
|
||||
/// Entspricht dem Message-Format das alle gängigen LLM APIs verwenden.
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
/// Rolle des Absenders: "system", "user" oder "assistant"
|
||||
pub role: String,
|
||||
/// Inhalt der Nachricht
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Erstellt eine System-Nachricht (z.B. den Persönlichkeits-Prompt)
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self { role: "system".to_string(), content: content.into() }
|
||||
}
|
||||
|
||||
/// Erstellt eine User-Nachricht
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self { role: "user".to_string(), content: content.into() }
|
||||
}
|
||||
|
||||
/// Erstellt eine Assistant-Nachricht (vorherige Antworten für Kontext)
|
||||
pub fn assistant(content: impl Into<String>) -> Self {
|
||||
Self { role: "assistant".to_string(), content: content.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Konfiguration für einen einzelnen LLM-Aufruf.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LlmRequest {
|
||||
/// Der vollständige Gesprächsverlauf inklusive System-Prompt
|
||||
pub messages: Vec<Message>,
|
||||
/// Maximale Anzahl Token in der Antwort
|
||||
pub max_tokens: u32,
|
||||
/// Kreativität der Antwort (0.0 = deterministisch, 1.0 = sehr kreativ)
|
||||
pub temperature: f32,
|
||||
pub tools: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl LlmRequest {
|
||||
pub fn simple(messages: Vec<Message>, max_tokens: u32, temperature: f32) -> Self {
|
||||
Self { messages, max_tokens, temperature, tools: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ToolCall {
|
||||
pub id: Option<String>,
|
||||
pub function: ToolCallFunction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ToolCallFunction {
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
/// Antwort eines LLM-Aufrufs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LlmResponse {
|
||||
/// Der generierte Text
|
||||
pub content: String,
|
||||
/// Anzahl der Input-Token (für Usage-Tracking)
|
||||
pub tokens_input: u64,
|
||||
/// Anzahl der Output-Token (für Usage-Tracking)
|
||||
pub tokens_output: u64,
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
pub cost: Option<f64>,
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// nazarick-core/src/memory.rs
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::error::NazarickError;
|
||||
|
||||
type Result<T> = std::result::Result<T, NazarickError>;
|
||||
|
||||
// ─── Schlanke Structs — nur was BaseAgent braucht ────────────────────────────
|
||||
|
||||
pub struct MemoryMessage {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub struct MemoryFact {
|
||||
pub category: String,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub struct MemoryCategorySummary {
|
||||
pub category: String,
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
// ─── Trait ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
pub trait Memory: Send + Sync {
|
||||
// ─── Konversation ───────────────────────────────────────────────
|
||||
|
||||
/// Aktives Gespräch holen oder neu anlegen
|
||||
async fn get_or_create_conversation(&self, timeout_mins: u64) -> Result<i64>;
|
||||
|
||||
/// Nachricht speichern
|
||||
async fn save_message(&self, conversation_id: i64, role: &str, content: &str) -> Result<()>;
|
||||
|
||||
/// Letzte N Nachrichten laden
|
||||
async fn load_window(&self, conversation_id: i64, window: usize) -> Result<Vec<MemoryMessage>>;
|
||||
|
||||
/// Letzten Summary laden
|
||||
async fn last_summary(&self) -> Result<Option<String>>;
|
||||
|
||||
/// Gespräch schließen
|
||||
async fn close_conversation(&self, conversation_id: i64, summary: Option<&str>) -> Result<()>;
|
||||
|
||||
// ─── Facts ──────────────────────────────────────────────────────
|
||||
|
||||
/// Fakt speichern/updaten
|
||||
async fn upsert_fact(&self, category: &str, key: &str, value: &str) -> Result<()>;
|
||||
|
||||
/// Fakt löschen
|
||||
async fn delete_fact(&self, category: &str, key: &str) -> Result<()>;
|
||||
|
||||
/// Kategorie laden
|
||||
async fn get_category(&self, category: &str) -> Result<Vec<MemoryFact>>;
|
||||
|
||||
/// Kategorien-Übersicht für Prompt
|
||||
async fn category_summaries(&self) -> Result<Vec<MemoryCategorySummary>>;
|
||||
|
||||
// ─── Usage Logging ──────────────────────────────────────────────
|
||||
|
||||
/// LLM-Call Kosten und Token-Verbrauch loggen
|
||||
async fn log_usage(
|
||||
&self,
|
||||
tokens_input: u64,
|
||||
tokens_output: u64,
|
||||
cost: Option<f64>,
|
||||
finish_reason: Option<&str>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// nazarick-core/src/summarizer.rs
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::error::NazarickError;
|
||||
|
||||
type Result<T> = std::result::Result<T, NazarickError>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Summarizer: Send + Sync {
|
||||
async fn summarize(&self, messages: &[(String, String)]) -> Result<String>;
|
||||
}
|
||||
Reference in New Issue
Block a user