// crates/nazarick/src/main.rs mod chat; mod config; use std::sync::Arc; use axum::{routing::post, Router}; use reqwest::Client; use tokio::sync::Mutex; use tower_http::trace::TraceLayer; use tracing::info; use nazarick_core::agent::base::AgentConfig; use nazarick_core::agent::skill_registry::SkillRegistry; use nazarick_core::llm::{LlmProvider, SkillFormat}; use api::llm::openai_compat::OpenAiCompatProvider; use nazarick_core::memory::Memory; use nazarick_core::summarizer::Summarizer; use memory::store::MemoryStore; use memory::summarizer::Summarizer as MemorySummarizer; use sebas_tian::Sebas; use lyra::Lyra; use chat::synology::{handle_incoming, AppState}; use config::ModelConfig; use skills as _; fn build_provider(model_cfg: &ModelConfig) -> Box { let skill_format = model_cfg.skill_format .as_deref() .map(|s| s.parse::().unwrap_or(SkillFormat::Xml)) .unwrap_or(SkillFormat::Xml); match model_cfg.provider.as_str() { "openai_compat" => Box::new(OpenAiCompatProvider::new( &model_cfg.url, &model_cfg.model, model_cfg.api_key.clone(), skill_format, )), unknown => panic!("Unbekannter Provider: '{}'", unknown), } } async fn build_memory(agent_id: &str) -> anyhow::Result> { let store = MemoryStore::open(agent_id).await?; Ok(Arc::new(store)) } fn build_summarizer(model_cfg: &ModelConfig) -> Arc { Arc::new(MemorySummarizer::new( &model_cfg.url, &model_cfg.model, model_cfg.max_summary_tokens.unwrap_or(4000), )) } #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter("nazarick=info,tower_http=debug,api=debug") .init(); info!("Nazarick erwacht..."); let exe_path = std::env::current_exe()?; let workspace_root = exe_path .parent() .and_then(|p| p.parent()) .and_then(|p| p.parent()) .ok_or_else(|| anyhow::anyhow!("Workspace-Root nicht gefunden"))?; std::env::set_current_dir(workspace_root)?; info!("Arbeitsverzeichnis: {}", workspace_root.display()); let cfg = config::load().map_err(|e| { eprintln!("Config Fehler: {}", e); e })?; let port = cfg.chat.listen_port; 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_model = cfg.models .get(&sebas_cfg.model) .ok_or_else(|| anyhow::anyhow!("Modell '{}' nicht in [models] config", sebas_cfg.model))?; let lyra_model = cfg.models .get(&lyra_cfg.model) .ok_or_else(|| anyhow::anyhow!("Modell '{}' nicht in [models] config", lyra_cfg.model))?; let summary_model = cfg.models .get("summary") .ok_or_else(|| anyhow::anyhow!("'summary' nicht in [models] config"))?; let sebas_memory = build_memory("sebas_tian").await?; let lyra_memory = build_memory("lyra").await?; let summarizer = build_summarizer(summary_model); info!("Memory geladen"); let mut sebas = Sebas::new( AgentConfig { agent_id: "sebas_tian".to_string(), shared_core_path: "config/shared_core.md".to_string(), soul_core_path: "crates/sebas-tian/config/soul_core.md".to_string(), max_tokens: sebas_cfg.max_tokens, max_loops: sebas_cfg.max_loops, history_window: sebas_cfg.history_window, summary_every: sebas_cfg.summary_every, conversation_timeout_mins: sebas_cfg.conversation_timeout_mins, }, build_provider(sebas_model), registry.clone(), sebas_memory, summarizer.clone(), ); sebas.init().await?; let mut lyra = Lyra::new( AgentConfig { agent_id: "lyra".to_string(), shared_core_path: "config/shared_core.md".to_string(), soul_core_path: "crates/lyra/config/soul_core.md".to_string(), max_tokens: lyra_cfg.max_tokens, max_loops: lyra_cfg.max_loops, history_window: lyra_cfg.history_window, summary_every: lyra_cfg.summary_every, conversation_timeout_mins: lyra_cfg.conversation_timeout_mins, }, build_provider(lyra_model), registry.clone(), lyra_memory, summarizer.clone(), ); lyra.init().await?; info!("Agenten initialisiert"); let state = Arc::new(AppState { agents: cfg.chat.agents, admin_user_id: cfg.chat.admin_user_id, admin_webhook_url: cfg.chat.admin_webhook_url, http: Client::new(), sebas: Mutex::new(sebas), lyra: Mutex::new(lyra), }); let app = Router::new() .route("/chat/synology", post(handle_incoming)) .with_state(state) .layer(TraceLayer::new_for_http()); let addr = format!("0.0.0.0:{}", port); let listener = tokio::net::TcpListener::bind(&addr).await?; info!("Lausche auf {}", addr); axum::serve(listener, app).await?; Ok(()) }