diff --git a/.gitea/workflows/backup.yml b/.gitea/workflows/backup.yml new file mode 100644 index 0000000..12ff4fc --- /dev/null +++ b/.gitea/workflows/backup.yml @@ -0,0 +1,45 @@ +name: Backup + +on: + schedule: + - cron: '0 2 * * *' # Täglich 02:00 UTC + +jobs: + backup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.SEBAS }} + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PI_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p 10022 localhost >> ~/.ssh/known_hosts + + - name: Pull backups from Pi + run: | + mkdir -p backup/data backup/config + scp -i ~/.ssh/deploy_key -P 10022 \ + "deploy@localhost:/opt/nazarick/data/*.sqlite" \ + backup/data/ || true + scp -i ~/.ssh/deploy_key -P 10022 \ + deploy@localhost:/opt/nazarick/config/config.toml \ + backup/config/config.toml || true + ssh -i ~/.ssh/deploy_key -p 10022 deploy@localhost \ + 'find /opt/nazarick/crates/*/config -type f -name "*.md"' | while read f; do + RELATIVE=${f#/opt/nazarick/} + mkdir -p "backup/$(dirname $RELATIVE)" + scp -i ~/.ssh/deploy_key -P 10022 \ + "deploy@localhost:$f" "backup/$RELATIVE" || true + done + + - name: Commit and push + run: | + git config user.name "Nazarick Backup Bot" + git config user.email "backup@nazarick" + git add backup/ + git diff --staged --quiet || git commit -m "chore: daily backup $(date +%Y-%m-%d)" + git push origin master diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f984a08 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,125 @@ +name: CI + +on: + push: + branches: [ '**' ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 3 * * 1' # Montags 03:00 UTC → deploy-infra (Ollama update) + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo check --all-targets + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy -- -D warnings + + deploy: + runs-on: ubuntu-latest + needs: [ check, test, clippy ] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu + + - uses: Swatinem/rust-cache@v2 + + - name: Build ARM64 + run: cargo build --release --target aarch64-unknown-linux-gnu + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PI_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p 10022 localhost >> ~/.ssh/known_hosts + + - name: Copy binary to Pi + run: | + scp -i ~/.ssh/deploy_key -P 10022 \ + target/aarch64-unknown-linux-gnu/release/nazarick \ + deploy@localhost:/opt/nazarick/nazarick.new + + - name: Copy Dockerfile to Pi + run: | + scp -i ~/.ssh/deploy_key -P 10022 \ + Dockerfile \ + deploy@localhost:/opt/nazarick/Dockerfile + + - name: Copy config files dynamically to Pi + run: | + # shared config + scp -i ~/.ssh/deploy_key -P 10022 \ + config/shared_core.md \ + deploy@localhost:/opt/nazarick/config/shared_core.md + + # Alle Agent-Config-Files dynamisch (soul_core.md, soul_personality.md etc.) + find crates/*/config -type f -name "*.md" | while read f; do + CRATE=$(echo "$f" | cut -d'/' -f1-3) + ssh -i ~/.ssh/deploy_key -p 10022 deploy@localhost "mkdir -p /opt/nazarick/$CRATE" + scp -i ~/.ssh/deploy_key -P 10022 "$f" "deploy@localhost:/opt/nazarick/$f" + done + + - name: Build image and restart nazarick + run: | + ssh -i ~/.ssh/deploy_key -p 10022 deploy@localhost ' + cd /opt/nazarick + mkdir -p target/release + mv nazarick.new target/release/nazarick + docker build -t nazarick:latest . + docker compose down nazarick || true + docker compose up -d nazarick + ' + + deploy-infra: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + steps: + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PI_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p 10022 localhost >> ~/.ssh/known_hosts + + - name: Update Ollama + pull latest Gemma + run: | + ssh -i ~/.ssh/deploy_key -p 10022 deploy@localhost ' + cd /opt/nazarick + docker compose pull ollama + docker compose up -d ollama + sleep 5 + docker exec ollama ollama pull gemma4:e2b + docker compose restart nazarick + ' \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1753423..84c1b82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ master ] + branches: [ '**' ] # alle Branches pull_request: branches: [ master ] @@ -32,3 +32,47 @@ jobs: with: components: clippy - run: cargo clippy --all-features -- -D warnings + + deploy: + runs-on: ubuntu-latest + needs: [ check, test, clippy ] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu + + - name: Build ARM64 + run: | + cargo build --release --target aarch64-unknown-linux-gnu + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PI_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p 10022 localhost >> ~/.ssh/known_hosts + + - name: Copy binary to Pi + run: | + scp -i ~/.ssh/deploy_key -P 10022 \ + target/aarch64-unknown-linux-gnu/release/nazarick \ + deploy@localhost:/opt/nazarick/nazarick.new + + - name: Restart on Pi + run: | + ssh -i ~/.ssh/deploy_key -p 10022 deploy@localhost ' + mv /opt/nazarick/nazarick.new /opt/nazarick/nazarick + cd /opt/nazarick + docker compose down || true + docker compose up -d + ' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 89a6f2b..38b24d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/.claude .env *.key config/private/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56666bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Pfad spiegelt die Cargo-Ausgabestruktur: +# current_exe() = /app/target/release/nazarick +# 3× .parent() → /app/target/release → /app/target → /app (workspace_root) ✓ +RUN mkdir -p /app/target/release +COPY target/aarch64-unknown-linux-gnu/release/nazarick /app/target/release/nazarick + +VOLUME ["/app/config", "/app/data"] + +EXPOSE 8765 + +CMD ["/app/target/release/nazarick"] diff --git a/config/config.toml b/config/config.toml index 19547be..5064a12 100644 --- a/config/config.toml +++ b/config/config.toml @@ -2,18 +2,27 @@ # ─── Modelle ────────────────────────────────────────────────────────────────── [models.default] +provider = "openai_compat" +url = "http://localhost:11434/v1" +model = "gemma3:2b" +api_key = "ollama" +skill_format = "xml" + +[models.summary] +provider = "openai_compat" +url = "http://localhost:11434" +model = "gemma3:2b" +api_key = "ollama" +skill_format = "xml" +max_summary_tokens = 2000 + +[models.openrouter-llama] provider = "openai_compat" url = "https://openrouter.ai/api/v1" model = "meta-llama/llama-3.3-70b-instruct" skill_format = "tool_use" api_key = "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" -[models.summary] -provider = "openai_compat" -url = "http://localhost:11434" -model = "llama3.1:8b" -max_summary_tokens = 5000 -skill_format = "xml" # ─── Chat ───────────────────────────────────────────────────────────────────── [chat] diff --git a/crates/lyra/src/lib.rs b/crates/lyra/src/lib.rs index 5073242..e139210 100644 --- a/crates/lyra/src/lib.rs +++ b/crates/lyra/src/lib.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use nazarick_core::agent::base::BaseAgent; +use nazarick_core::agent::base::{AgentConfig, BaseAgent}; use nazarick_core::agent::skill_registry::SkillRegistry; use nazarick_core::memory::Memory; use nazarick_core::summarizer::Summarizer; @@ -13,34 +13,14 @@ pub struct Lyra { impl Lyra { pub fn new( - agent_id: impl Into, - shared_core_path: impl Into, - soul_core_path: impl Into, + config: AgentConfig, llm: Box, registry: Arc, memory: Arc, summarizer: Arc, - max_tokens: u32, - max_loops: u32, - history_window: usize, - summary_every: usize, - conversation_timeout_mins: u64, ) -> Self { Self { - base: BaseAgent::new( - agent_id, - shared_core_path, - soul_core_path, - llm, - registry, - memory, - summarizer, - max_tokens, - max_loops, - history_window, - summary_every, - conversation_timeout_mins, - ), + base: BaseAgent::new(config, llm, registry, memory, summarizer), } } diff --git a/crates/nazarick-core/src/agent/base.rs b/crates/nazarick-core/src/agent/base.rs index 78f5059..615409e 100644 --- a/crates/nazarick-core/src/agent/base.rs +++ b/crates/nazarick-core/src/agent/base.rs @@ -13,6 +13,17 @@ use crate::agent::skill_registry::SkillRegistry; use crate::memory::Memory; use crate::summarizer::Summarizer; +pub struct AgentConfig { + pub agent_id: String, + pub shared_core_path: String, + pub soul_core_path: String, + pub max_tokens: u32, + pub max_loops: u32, + pub history_window: usize, + pub summary_every: usize, + pub conversation_timeout_mins: u64, +} + pub struct BaseAgent { pub id: AgentId, agent_id: String, @@ -36,36 +47,28 @@ pub struct BaseAgent { impl BaseAgent { pub fn new( - agent_id: impl Into, - shared_core_path: impl Into, - soul_core_path: impl Into, + config: AgentConfig, llm: Box, registry: Arc, memory: Arc, summarizer: Arc, - 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(); Self { id: AgentId::new_v4(), - agent_id: agent_id.clone(), - max_tokens, - max_loops, - history_window, - summary_every, - conversation_timeout_mins, + agent_id: config.agent_id.clone(), + max_tokens: config.max_tokens, + max_loops: config.max_loops, + history_window: config.history_window, + summary_every: config.summary_every, + conversation_timeout_mins: config.conversation_timeout_mins, conversation_id: 0, messages_since_summary: 0, prompt_builder: PromptBuilder::new( - &agent_id, - shared_core_path, - soul_core_path, + &config.agent_id, + config.shared_core_path, + config.soul_core_path, ), skill_executor: SkillExecutor::new(registry.clone(), skill_format.clone()), skill_format, @@ -243,17 +246,17 @@ impl BaseAgent { 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() - ); - loop_context.push(Message::assistant(&clean_raw)); - loop_context.push(Message::user(&details)); - continue; - } + if let Some(skill_name) = Self::parse_skill_info(&clean_raw) + && let Some(skill) = self.registry.get(&skill_name) + { + let details = format!( + "[Skill-Details für '{}']\n{}", + skill_name, + skill.details() + ); + loop_context.push(Message::assistant(&clean_raw)); + loop_context.push(Message::user(&details)); + continue; } let (clean, feedback) = self.skill_executor.process( diff --git a/crates/nazarick-core/src/agent/skill_executor.rs b/crates/nazarick-core/src/agent/skill_executor.rs index f9ff76c..143fd43 100644 --- a/crates/nazarick-core/src/agent/skill_executor.rs +++ b/crates/nazarick-core/src/agent/skill_executor.rs @@ -120,11 +120,7 @@ impl SkillExecutor { let mut calls = Vec::new(); let mut clean = response.to_string(); - loop { - let start = match clean.find(" s, - None => break, - }; + while let Some(start) = clean.find("") { Some(e) => start + e, None => { diff --git a/crates/nazarick-core/src/llm/traits.rs b/crates/nazarick-core/src/llm/traits.rs index 8f25d6d..f27fc62 100644 --- a/crates/nazarick-core/src/llm/traits.rs +++ b/crates/nazarick-core/src/llm/traits.rs @@ -1,5 +1,6 @@ // nazarick-core/src/llm/traits.rs +use std::str::FromStr; use crate::types::Result; use crate::llm::types::{LlmRequest, LlmResponse}; @@ -13,13 +14,14 @@ pub enum SkillFormat { None, } -impl SkillFormat { - /// Parsed aus config.toml String - pub fn from_str(s: &str) -> Self { +impl FromStr for SkillFormat { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { match s { - "tool_use" => Self::ToolUse, - "none" => Self::None, - _ => Self::Xml, // default + "tool_use" => Ok(Self::ToolUse), + "none" => Ok(Self::None), + _ => Ok(Self::Xml), } } } diff --git a/crates/nazarick/src/chat/synology.rs b/crates/nazarick/src/chat/synology.rs index 8eda4da..2b44ead 100644 --- a/crates/nazarick/src/chat/synology.rs +++ b/crates/nazarick/src/chat/synology.rs @@ -250,7 +250,7 @@ fn split_message(text: &str) -> Vec { .unwrap_or(safe_max); chunks.push(remaining[..cut].trim().to_string()); - remaining = &remaining[cut..].trim_start(); + remaining = remaining[cut..].trim_start(); } if !remaining.is_empty() { diff --git a/crates/nazarick/src/main.rs b/crates/nazarick/src/main.rs index a96600e..b171c6c 100644 --- a/crates/nazarick/src/main.rs +++ b/crates/nazarick/src/main.rs @@ -10,6 +10,7 @@ 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; @@ -26,7 +27,7 @@ use skills as _; fn build_provider(model_cfg: &ModelConfig) -> Box { let skill_format = model_cfg.skill_format .as_deref() - .map(SkillFormat::from_str) + .map(|s| s.parse::().unwrap_or(SkillFormat::Xml)) .unwrap_or(SkillFormat::Xml); match model_cfg.provider.as_str() { @@ -107,34 +108,38 @@ async fn main() -> anyhow::Result<()> { info!("Memory geladen"); let mut sebas = Sebas::new( - "sebas_tian", - "config/shared_core.md", - "crates/sebas-tian/config/soul_core.md", + 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_cfg.max_tokens, - sebas_cfg.max_loops, - sebas_cfg.history_window, - sebas_cfg.summary_every, - sebas_cfg.conversation_timeout_mins, ); sebas.init().await?; let mut lyra = Lyra::new( - "lyra", - "config/shared_core.md", - "crates/lyra/config/soul_core.md", + 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_cfg.max_tokens, - lyra_cfg.max_loops, - lyra_cfg.history_window, - lyra_cfg.summary_every, - lyra_cfg.conversation_timeout_mins, ); lyra.init().await?; diff --git a/crates/sebas-tian/src/lib.rs b/crates/sebas-tian/src/lib.rs index 9f0dad5..639409a 100644 --- a/crates/sebas-tian/src/lib.rs +++ b/crates/sebas-tian/src/lib.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use nazarick_core::agent::base::BaseAgent; +use nazarick_core::agent::base::{AgentConfig, BaseAgent}; use nazarick_core::agent::skill_registry::SkillRegistry; use nazarick_core::memory::Memory; use nazarick_core::summarizer::Summarizer; @@ -13,34 +13,14 @@ pub struct Sebas { impl Sebas { pub fn new( - agent_id: impl Into, - shared_core_path: impl Into, - soul_core_path: impl Into, + config: AgentConfig, llm: Box, registry: Arc, memory: Arc, summarizer: Arc, - max_tokens: u32, - max_loops: u32, - history_window: usize, - summary_every: usize, - conversation_timeout_mins: u64, ) -> Self { Self { - base: BaseAgent::new( - agent_id, - shared_core_path, - soul_core_path, - llm, - registry, - memory, - summarizer, - max_tokens, - max_loops, - history_window, - summary_every, - conversation_timeout_mins, - ), + base: BaseAgent::new(config, llm, registry, memory, summarizer), } } diff --git a/deploy/pi/docker-compose.yml b/deploy/pi/docker-compose.yml new file mode 100644 index 0000000..3851fe0 --- /dev/null +++ b/deploy/pi/docker-compose.yml @@ -0,0 +1,23 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: ollama + restart: unless-stopped + network_mode: host + volumes: + - /opt/nazarick/ollama:/root/.ollama + + nazarick: + image: nazarick:latest + container_name: nazarick + restart: unless-stopped + network_mode: host + depends_on: + - ollama + volumes: + # Binary-Pfad: /app/target/release/nazarick + # 3× parent() → workspace_root = /app ✓ + - /opt/nazarick/target:/app/target + - /opt/nazarick/config:/app/config + - /opt/nazarick/data:/app/data + working_dir: /app