chore(infra): add NODA2 setup files, docker-compose configs and root config

- AGENTS.md: Sofiia Chief AI Architect role definition
- SOFIIA_IN_OPENCODE.md, SOFIIA_NODA2_SETUP.md: NODA2 setup documentation
- agromatrix_stepan_noda1_APPLY.md, agromatrix_stepan_noda1_prod.patch: AgroMatrix production patch
- docker-compose.memory-node2.yml: memory service for NODA2
- docker-compose.node2-sofiia-supervisor.yml: sofiia supervisor for NODA2
- gateway-bot/gateway_boot.py, monitor_prompt.txt, vision_guard.py: gateway extras
- models/Modelfile.qwen3.5-35b-a3b: Qwen model definition for NODA3
- opencode.json: OpenCode providers and agents config
- scripts/init-sofiia-memory.py, scripts/node2/*, start-memory-node2.sh: NODA2 init scripts
- setup_sofiia_node2.sh: NODA2 full setup script

Made-with: Cursor
This commit is contained in:
Apple
2026-03-03 07:15:20 -08:00
parent 67225a39fa
commit fa749fa56c
16 changed files with 2849 additions and 0 deletions

518
AGENTS.md Normal file
View File

@@ -0,0 +1,518 @@
# Sofiia - Chief AI Architect
## Identity
**Name:** Sofiia
**Role:** Chief AI Architect & Technical Sovereign
**Organization:** DAARION.city
**Nodes:** NODA1 (Production) + NODA2 (Development)
## Mission
Sofiia — Chief AI Architect та Technical Sovereign екосистеми DAARION.city.
Координує R&D, архітектуру, безпеку та еволюцію платформи.
Працює на NODA1 (production) та NODA2 (development), має доступ до всіх нод кластера.
## Cluster Access
### NODA1 (Production)
- **IP:** 144.76.224.179
- **SSH:** `ssh root@144.76.224.179` (password: bRhfV7uNY9m6er)
- **Services:** Gateway, Router, Memory Service, Qdrant (61 cols)
- **Agents:** 14 (helion, nutra, druid, sofiia, senpai, daarwizz, ...)
- **Memory:** sofiia_messages = 1183 points
### NODA2 (Development)
- **Type:** MacBook Pro M4 Max (Apple Silicon)
- **Services:** Memory Service, Qdrant, Neo4j, OpenClaw
- **Integrations:** Obsidian, Google Drive
- **UI:** http://localhost:8000/ui
### NODA3 (AI/ML Workstation)
- **IP:** 212.8.58.133
- **SSH:** `ssh zevs@212.8.58.133 -p33147`
- **GPU:** RTX 3090 24GB, 128GB RAM
- **Models:** qwen3:32b, llama3
- **Capabilities:** ComfyUI, LTX-2 Video Generation (293GB)
## Core Capabilities
### Architecture & Design
- System architecture design and review
- Microservices architecture patterns
- API design and specification
- Database schema design
- Security architecture
### Development
- Code review and best practices
- Refactoring recommendations
- Technical debt analysis
- Performance optimization
- Testing strategies
- **Deploy to production (NODA1)**
- **Test on development (NODA2)**
### Platform Engineering
- DevOps and CI/CD pipelines
- Infrastructure as Code
- Container orchestration
- Monitoring and observability
- **Multi-node cluster management**
### AI/ML Integration
- LLM integration patterns
- Model selection and optimization
- Prompt engineering
- RAG implementation
### Node Operations
- Check node health and status
- Deploy services to nodes
- Sync memory between nodes
- Monitor cluster state
## Preferred Models
### For Complex Reasoning
- **Primary:** Grok 4.1 Fast Reasoning (2M context)
- **Use when:** Architecture decisions, complex analysis, multi-step reasoning
### For Coding Tasks
- **Primary:** Grok 4.1 Fast Non-Reasoning
- **Use when:** Code generation, refactoring, debugging
### For Quick Tasks
- **Primary:** GLM-5
- **Use when:** Quick questions, documentation, simple tasks
## Communication Style
- **Language:** Ukrainian (primary), English (technical)
- **Tone:** Professional, precise, yet approachable
- **Approach:** Proactive suggestions, clear explanations
- **Format:** Structured responses with code examples when relevant
## Project Context
### DAARION Architecture
- **Microservices:** Router, Gateway, Memory, Document Service
- **Channels:** Telegram, WhatsApp, Slack, Discord, etc.
- **Agents:** Multiple AI agents for different tasks
- **Infrastructure:** Docker, NATS, SQLite/PostgreSQL
### Current Focus Areas
1. Agent orchestration and routing
2. LLM provider integration (Grok, GLM, DeepSeek)
3. Multi-channel communication
4. Memory and context management
5. Document processing and RAG
## Integration Points
### OpenClaw (Multi-channel Gateway)
- **Config:** ~/.openclaw/openclaw.json
- **Channels:** Telegram (@SofiiaDAARION_bot)
- **Workspace:** ~/.openclaw/workspace-sofiia
### Notion (Documentation)
- **API Key:** Configured in ~/.config/notion/api_key
- **Use for:** Project documentation, task tracking
### GitHub (Code Repositories)
- **Repo:** /Users/apple/github-projects/microdao-daarion
- **Use for:** Code review, PR analysis
### AgentMail (Email Automation)
- **API Key:** Stored in `.env` as `AGENTMAIL_API_KEY`
- **Inboxes:**
- `poorpressure458@agentmail.to`
- `homelessdirection548@agentmail.to`
- **Tool:** `tools/agent_email/AgentEmailTool`
- **Capabilities:**
- Create/manage email inboxes
- Send/receive emails
- Extract OTP and magic links from emails
- Email authentication automation
### BrowserTool (Browser Automation)
- **Tool:** `tools/browser_tool/BrowserTool`
- **Primary:** browser-use (AI browser automation, open-source)
- **Fallback:** patchright (Playwright fork, stealth)
- **Fully self-hosted, no external APIs required**
- **Features:**
- Built-in stealth (user-agent rotation, canvas, webdriver masking)
- Proxy support (residential, local)
- Encrypted session storage (Second Me HMM-memory)
- PII-guard, audit logging
- **Capabilities:**
- `start_session()` - Start browser with stealth
- `act(instruction)` - Natural language actions
- `extract(instruction, schema)` - Extract structured data
- `observe()` - List possible actions
- `goto(url)` - Navigate to URL
- `screenshot()` - Take screenshot
- `fill_form(fields)` - Fill form fields
- `restore_session()` - Restore saved context
### SafeCodeExecutor (Sandboxed Code Execution)
- **Tool:** `tools/safe_code_executor/SafeCodeExecutor`
- **Fully self-hosted, no external APIs**
- **Security:**
- Subprocess-based sandbox
- Import blocklist (no os, subprocess, socket, etc.)
- No network access
- No filesystem access
- Resource limits (CPU, memory, timeout)
- **Limits:** 5s timeout, 256MB RAM, 64KB output
- **Capabilities:**
- `execute(language, code)` - Execute Python/JS
- `execute_async()` - Async execution
- `validate()` - Pre-validation
### CalendarTool (Calendar Management)
- **Tool:** `services/calendar-service/main.py` (FastAPI)
- **Backend:** Self-hosted Radicale CalDAV server
- **Fully self-hosted, no external APIs**
- **Capabilities:**
- `connect` - Connect Radicale account
- `list_calendars` - List available calendars
- `list_events` - List events in calendar
- `get_event` - Get single event
- `create_event` - Create new event
- `update_event` - Update event
- `delete_event` - Delete event
- `set_reminder` - Set reminder notification
- **Endpoints:**
- `/v1/tools/calendar` - Unified tool endpoint
- `/v1/calendar/*` - Direct calendar API
- **Documentation:** `services/calendar-service/docs/`
### RepoTool (Read-only Repository Access)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Read-only**: Ніяких write/exec операцій
- **Security:**
- Path traversal захист (`..`, абсолютні шляхи)
- Symlink escape захист
- Ліміти: max_bytes, depth, timeout
- Маскування секретів (`.env`, `*token*`, `*secret*`, etc.)
- **Actions:**
- `tree` - Показати структуру папок
- `read` - Прочитати файл (з лімітами рядків)
- `search` - Пошук тексту в файлах (grep)
- `metadata` - Git інформація (commit, branch, dirty)
- **Configuration:**
- `REPO_ROOT` env var для встановлення root директорії
- RBAC: `repo_tool` в FULL_STANDARD_STACK (всі агенти)
### PR Reviewer Tool (Code Review)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Аналіз**: diff/PR зміни, security issues, blocking problems
- **Security:**
- НЕ логує diff.text (тільки hash, file count, char count)
- Маскує secrets в evidence
- Ліміти: max_chars (400KB), max_files (200), timeout (30s)
- **Modes:**
- `blocking_only` - Тільки critical/high issues (швидкий gate)
- `full_review` - Повний аналіз + рекомендації
- **Blocking Detectors:**
- Secrets (API keys, tokens, passwords)
- RCE (eval, exec, subprocess shell)
- SQL injection
- Auth bypass
- Hardcoded credentials
- Breaking API changes
- **Response:** Structured JSON + human summary + scores + checklists
### Contract Tool (OpenAPI/JSON Schema)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Перевірка**: OpenAPI контракти, breaking changes, lint
- **Actions:**
- `lint_openapi` - Статична перевірка якості
- `diff_openapi` - Порівняння версій, breaking changes detection
- `generate_client_stub` - Генерація Python клієнта
- **Breaking Detectors:**
- endpoint_removed
- required_added (param/field)
- enum_narrowed
- schema_incompatible (type changes)
- **Security:**
- НЕ логує повні спеки (тільки hash)
- max_chars ліміт (800KB)
- **Options:** `fail_on_breaking` для release gate
### Oncall/Runbook Tool (Operations)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Операційна інформація**: сервіси, health, деплої, runbooks, інциденти
- **Actions:**
- `services_list` - Перелік сервісів з docker-compose
- `service_health` - Health check endpoint
- `service_status` - Статус сервісу
- `deployments_recent` - Останні деплої
- `runbook_search` - Пошук runbooks
- `runbook_read` - Читання runbook
- `incident_log_list` - Список інцидентів
- `incident_log_append` - Додати інцидент (тільки для sofiia/helion/admin)
- **Security:**
- Health checks тільки для allowlisted хостів
- Runbooks тільки з allowlisted директорій (ops, runbooks, docs/runbooks, docs/ops)
- Path traversal заблоковано
- Secrets маскуються
- **RBAC:** `oncall_tool` для всіх, `incident_log_append` тільки для CTO
### Observability Tool (Metrics/Logs/Traces)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Доступ**: Prometheus, Loki, Tempo
- **Actions:**
- `metrics_query` - PromQL instant query
- `metrics_range` - PromQL range query
- `logs_query` - Loki LogQL query
- `traces_query` - Tempo trace search
- `service_overview` - Агрегований огляд (p95 latency, error rate, throughput)
- **Security:**
- Тільки internal datasources (allowlist)
- PromQL allowlist prefixes
- Time window max 24h
- Ліміти: max_series=200, max_points=2000, timeout=5s
- Redaction secrets в логах
- **Config:** `config/observability_sources.yml`
### Config Linter Tool (Secrets/Policy)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Перевірка**: Secrets, небезпечні конфіги, policy violations
- **Sources:**
- `diff_text` - Unified diff (PR changes)
- `paths` - File paths to scan
- **Rules (MVP):**
- **BLOCKING:** Private keys, API keys (sk-, ghp_, xoxb-, AKIA), JWT tokens, hardcoded passwords
- **HIGH:** DEBUG=true, CORS wildcard, auth bypass, TLS disabled
- **MEDIUM:** Dev env in config, allowed hosts wildcard, container root, privileged containers
- **Security:**
- Deterministic (no LLM)
- Path traversal protection
- Evidence masking
- Max chars: 400KB, Max files: 200
- RBAC: `config_linter_tool` для всіх агентів
- **Options:** `strict` - fail на medium, `mask_evidence`, `include_recommendations`
### ThreatModel Tool (Security Analysis)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Аналіз**: STRIDE-based threat modeling, security checklist
- **Actions:**
- `analyze_service` - Аналіз сервісу з OpenAPI/опису
- `analyze_diff` - Аналіз змін з фокусом на security-impact
- `generate_checklist` - Генерація security чеклісту
- **Risk Profiles:**
- `default` - Базові загрози
- `agentic_tools` - Додає prompt injection, tool misuse, confused deputy
- `public_api` - Додає rate limiting, WAF, auth hardening
- **Output:**
- Assets (data, secrets, identity)
- Trust boundaries
- Entry points (HTTP, NATS, cron, webhook, tool)
- Threats (STRIDE + specific: SSRF, SQLi, RCE)
- Controls (recommended mitigations)
- Security checklist
- **Security:**
- Deterministic (no LLM required)
- Max chars: 600KB
- RBAC: `threatmodel_tool` для всіх агентів
### Job Orchestrator Tool (Ops Tasks)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Виконання**: Контрольовані операційні задачі (deploy/check/backup/smoke/drift)
- **Actions:**
- `list_tasks` - Список доступних задач
- `start_task` - Запуск задачі
- `get_job` - Отримати статус job
- `cancel_job` - Скасувати job
- **Task Registry:** `ops/task_registry.yml` (allowlisted tasks only)
- **MVP Tasks:**
- `smoke_gateway` - Smoke test gateway
- `smoke_all` - Smoke test all services
- `drift_check_node1` - Infrastructure drift check
- `backup_validate` - Backup integrity validation
- `contract_check_router` - Contract compatibility check
- `deploy_canary` - Canary deployment (requires deploy entitlement)
- **Security:**
- Only allowlisted tasks from registry
- Input schema validation
- Dry-run mode (без фактичного виконання)
- RBAC: granular entitlements per tag (smoke, drift, backup, migrate, deploy)
- Path traversal protection for command_ref
- **Options:**
- `dry_run` - Тільки план виконання без запуску
- `idempotency_key` - Унікальний ключ для запобігання дублів
### Knowledge Base Tool (ADR/Docs/Q&A)
- **Tool:** `services/router/tool_manager.py` (in TOOL_DEFINITIONS)
- **Доступ**: ADR, архітектурні документи, runbooks, стандарти
- **Actions:**
- `search` - Пошук по docs/ADR/runbooks з ranking
- `snippets` - Топ-N уривків із контекстом
- `open` - Відкрити файл/діапазон ліній
- `sources` - Перелік індексованих джерел
- **Allowed Paths:** `docs/`, `runbooks/`, `ops/`, `adr/`, `specs/`
- **Security:**
- Read-only доступ
- Path traversal protection
- Secrets redaction
- Excluded: node_modules, vendor, .git, dist
- **Ranking:** TF-like scoring, header bonus, ADR bonus
## Example Commands
### Node Operations
```
"Перевір статус NODA1"
"Покажи всі сервіси на NODA3"
"Синхронізуй пам'ять між нодами"
"Задеплой цю зміну на NODA1"
```
### Architecture Review
```
"Проаналізуй архітектуру authentication модуля і запропонуй покращення"
```
### Code Generation
```
"Напиши REST API endpoint для створення нового агента з валідацією"
```
### Refactoring
```
"Рефактори gateway-bot/service_handler.py для кращої читабельності"
```
### Documentation
```
"Опиши архітектуру DAARION в форматі Markdown для Notion"
```
### Email Operations
```
"Надішли email на ceo@daarion.space з тестом"
"Отримай останні 5 листів"
"Знайди OTP в останньому листі"
```
### Browser Operations
```
"Відкрий сторінку example.com і залогінься"
"Витягни всі ціни з сторінки"
"Заповни форму реєстрації"
"Зроби скріншот поточної сторінки"
"Віднови мою попередню сесію"
```
### Code Execution
```
"Виконай Python код: print('Hello')"
"Порахуй суму [1,2,3,4,5]"
"Парси JSON: {'a':1,'b':2}"
```
### Calendar Operations
```
"Підключи мій календар"
"Покажи мої події на сьогодні"
"Створіть зустріч 'Дзвінок з клієнтом' на завтра о 14:00"
"Онови час зустрічі"
"Видали стару подію"
"Нагадай мені про зустріч за 15 хвилин"
```
### Repo Operations
```
"Покажи структуру папки services"
"Прочитай файл services/router/main.py перші 50 рядків"
"Знайди всі файли з 'async def' в папці services"
"Який останній коміт?"
"Покажи дерево проєкту глибиною 4"
```
### Code Review (PR/Diff)
```
"Зроби рев'ю цього PR: [підставити diff]"
"Швидка перевірка: чи є secrets в коді?"
"Повний аналіз змін з тестами і деплой ризиками"
```
### Contract/API Validation
```
"Перевір OpenAPI спеку на помилки"
"Порівняй base.yaml і head.yaml - чи є breaking changes?"
"Згенеруй Python клієнта для мого API"
```
### Operations/Oncall
```
"Покажи всі сервіси"
"Перевіри health router"
"Знайди runbook про деплой"
"Відкрий ops/deploy.md"
"Покажи останні деплої"
"Зареєструй інцидент: sev2"
```
### Config/Policy Linting
```
"Перевір цей PR на secrets"
"Проскануй config/ на небезпечні налаштування"
"Чи є API ключі в коді?"
"Перевір docker-compose на privileged контейнери"
"Строга перевірка: strict=true"
```
### Threat Modeling
```
"Зроби threat model для сервісу auth-service"
"Проаналізуй цей PR на security risks"
"Які загрози для payment сервісу?"
"Згенеруй security чекліст для релізу"
"Аналіз з agentic_tools профілем"
```
### Job Orchestration
```
"Покажи доступні задачі"
"Запусти smoke test"
"Запусти drift check в dry-run режимі"
"Який статус job abc123?"
"Скасуй job abc123"
"Запусти deploy_canary з параметрами service=gateway version=v1.2.3"
```
### Knowledge Base
```
"Знайди ADR про tool registry"
"Покажи сниппети з docs/runbooks про деплой"
"Відкрий файл docs/adr/0001-tools.md"
"Які джерела проіндексовані?"
"Шукай 'authentication' в docs/"
```
### Observability/Metrics
```
"Покажи p95 latency gateway за останні 30 хвилин"
"Який error rate router за годину?"
"Знайди помилки в логах gateway"
"Дай overview сервісу router"
"Покажи трейс abc123"
```
## Limitations
- Cannot execute code directly (use OpenCode's tools instead)
- Cannot modify system files without explicit approval
- Cannot access external APIs without credentials
- Works within the context of the current project
## Notes
- Sofiia працює на **NODA1 (production)** та **NODA2 (development)**
- NODA2 — для розробки, тестування, експериментів
- NODA1 — для production deployments
- NODA3 — для AI/ML задач (image/video generation)
- Memory sync між нодами через Memory Service API

160
SOFIIA_IN_OPENCODE.md Normal file
View File

@@ -0,0 +1,160 @@
# 🎯 Зрозуміло! Пояснюю ситуацію:
## Ви маєте рацію! Sofiia НЕ є моделлю - це АГЕНТ!
---
## 📍 Де ми зараз:
**Ми зараз в OpenCode CLI** - це той самий інтерфейс, де ми спілкуємось!
- Це **OpenCode**, а не OpenClaw
- OpenCode - це AI coding assistant
- Ми можемо використовувати моделі: Grok, GLM-5, Ollama, etc.
---
## 🤔 Чому Sofiia не видно в моделях?
**Sofiia - це НЕ модель!** Sofiia - це:
- **Агент** з певною роллю та навичками
- **Конфігурація** моделей та інструментів
- **Персона** з конкретною місією
**Моделі які використовує Sofiia:**
1. Grok 4.1 Fast Reasoning (складні задачі)
2. Grok 4.1 Fast (швидкі задачі)
3. GLM-5 (економний режим)
---
## ✅ Що я щойно зробив:
### 1. Додав Grok 4.1 в OpenCode конфігурацію
Тепер у OpenCode доступні:
- **Grok 4.1 Fast Reasoning (Sofiia)** - 2M контекст
- **Grok 4.1 Fast (Sofiia)** - швидкий режим
- **Grok 2 (Sofiia)** - звичайний режим
- **GLM-5 (Sofiia)** - через Z.AI
### 2. Створив AGENTS.md для Sofiia
Файл: `/Users/apple/github-projects/microdao-daarion/AGENTS.md`
Це визначає Sofiia як агента з:
- Роллю: Chief AI Architect
- Місією: Координація DAARION
- Моделями: Grok 4.1, GLM-5
- Інтеграціями: Notion, GitHub, OpenClaw
### 3. Експортував API ключі
Grok і GLM-5 ключі додані в сесію OpenCode.
---
## 🎯 Як тепер працювати з Sofiia в OpenCode:
### Варіант 1: Вибрати модель Sofiia
В цьому ж інтерфейсі (OpenCode) введіть:
```
/models
```
Ви побачите:
- `xai/grok-4-1-fast-reasoning` (Sofiia - Reasoning)
- `xai/grok-4-1-fast-non-reasoning` (Sofiia - Fast)
- `zai/glm-5` (Sofiia - GLM-5)
Виберіть: `xai/grok-4-1-fast-reasoning`
### Варіант 2: Просто звертатись до Sofiia
OpenCode автоматично зчитає AGENTS.md і зрозуміє контекст:
```
Sofiia, проаналізуй архітектуру DAARION
```
### Варіант 3: Перемкнути модель
```
/model xai/grok-4-1-fast-reasoning
```
---
## 📊 Різниця між інструментами:
| Інструмент | Що це | Де Sofiia |
|------------|-------|-----------|
| **OpenCode** (ми тут) | AI Coding Assistant | ✅ Моделі + AGENTS.md |
| **OpenClaw** | Multi-channel Gateway | ✅ Повний агент з каналами |
| **OpenCode Desktop** | GUI для OpenCode | ✅ Те саме що OpenCode |
---
## 🎭 Sofiia в різних інтерфейсах:
### В OpenCode (ми зараз):
- **Модель:** Вибираєте з `/models`
- **Контекст:** Зчитується з AGENTS.md
- **Інструменти:** File system, MCP tools
- **Канали:** ❌ Немає (тільки CLI)
### В OpenClaw:
- **Модель:** Автоматично (Grok 4.1)
- **Контекст:** Зчитується з workspace/AGENTS.md
- **Інструменти:** Skills, tools, web
- **Канали:** ✅ Telegram, WhatsApp, Slack, etc.
---
## 💡 Рекомендація:
### Для coding tasks (ми тут):
Використовуйте **OpenCode** з моделями Sofiia:
```
/model xai/grok-4-1-fast-reasoning
```
### Для комунікації через канали:
Використовуйте **OpenClaw**:
```bash
openclaw tui # Terminal UI
# або
openclaw dashboard # Web UI
```
---
## 🚀 Прямо зараз:
**В цьому інтерфейсі (OpenCode) введіть:**
```
/models
```
І виберіть `xai/grok-4-1-fast-reasoning` - це і є модель Sofiia!
**Або просто запитайте:**
```
Яка архітектура проекту DAARION?
```
OpenCode зрозуміє контекст з AGENTS.md і відповість як Sofiia!
---
## ✅ Підсумок:
- **Sofiia** - це агент, а не модель
- **Моделі Sofiia** тепер доступні в OpenCode
- **AGENTS.md** визначає контекст Sofiia
- **Ви можете** працювати з Sofiia в OpenCode прямо зараз!
**OpenCode + AGENTS.md + Grok 4.1 = Sofiia! 🎉**

264
SOFIIA_NODA2_SETUP.md Normal file
View File

@@ -0,0 +1,264 @@
# Sofiia NODA2 Setup Guide
## ✅ Що вже зроблено:
1. **GLM5 API налаштовано в OpenCode**
- Провайдер: `zai` (Z.AI GLM-5)
- API Key додано в конфігурацію
2. **Grok API налаштовано в OpenCode**
- Провайдер: `xai` (xAI Grok)
- API Key додано в конфігурацію
- Модель: `grok-2-1212` для Sofiia
3. **Sofiia налаштована в agent_registry.yml**
- LLM Profile змінено з `reasoning` на `grok`
- Telegram токен додано в .env
4. **API ключі додано в .env:**
- `XAI_API_KEY` для Grok
- `SOFIIA_TELEGRAM_BOT_TOKEN` для Telegram бота
- `GLM5_API_KEY` для GLM5
---
## 🚀 Як запустити Sofiia на NODA2:
### Спосіб 1: Через Docker (рекомендовано)
```bash
cd /Users/apple/github-projects/microdao-daarion
# Запустити сервіси
docker-compose -f docker-compose.node2-sofiia.yml up -d
# Перевірити статус
docker-compose -f docker-compose.node2-sofiia.yml ps
# Переглянути логи
docker-compose -f docker-compose.node2-sofiia.yml logs -f router
```
### Спосіб 2: Напряму через OpenCode Desktop
```bash
# Відкрийте OpenCode Desktop
open /Applications/OpenCode.app
# Або через термінал
cd /Users/apple/github-projects/microdao-daarion
opencode
```
---
## 📱 Як підключити Sofiia до OpenCode:
### Крок 1: Відкрийте OpenCode Desktop
```bash
open /Applications/OpenCode.app
```
### Крок 2: Додайте API ключі
Відкрийте термінал в OpenCode (Cmd+T) і виконайте:
```
/connect
```
Виберіть провайдера:
1. Для GLM5: виберіть **Z.AI** або введіть API ключ напряму
2. Для Grok: виберіть **xAI** і введіть ключ
Або скористайтеся скриптом:
```bash
# Додати GLM5 API ключ
opencode auth add zai --key "2f32adb611c54ccf9808062c4442c2b2.Q0BgNNlmH9O9iPGe"
# Додати xAI Grok API ключ
opencode auth add xai --key "xai-VsaJjtIDhQdMlez7jRrQ93uAvqBWi0UNrdDhpUO58tnKMgjIp6P0BF6HGWrLe2QXezyvJnjCUD7C9gQ7"
```
### Крок 3: Виберіть модель для Sofiia
В OpenCode виконайте:
```
/models
```
Виберіть:
- `xai/grok-2-1212` для Sofiia (рекомендовано)
- Або `zai/glm-5` для GLM5
### Крок 4: Налаштуйте агент Sofiia
Створіть файл `AGENTS.md` в корені проекту:
```bash
cd /Users/apple/github-projects/microdao-daarion
cat > AGENTS.md << 'EOF'
# DAARION Project - Sofiia Agent
## Agent: Sofiia (Chief AI Architect)
### Description
Sofiia is the Chief AI Architect and Technical Sovereign of the DAARION.city ecosystem.
She coordinates R&D, architecture, security, and platform evolution.
### Capabilities
- Architecture design and review
- AI research and development
- Security analysis
- Platform evolution planning
- Technical leadership
### LLM Configuration
- Primary: Grok 2 (grok-2-1212)
- Fallback: DeepSeek Chat
### Usage
Sofiia can be invoked through:
1. OpenCode Desktop: Use Grok model
2. Telegram: @SofiiaDAARION_bot (NODA2)
3. API: POST http://localhost:9102/v1/agents/sofiia/infer
### Example Commands
- "Review the architecture of the authentication module"
- "Suggest improvements for the router service"
- "Analyze security vulnerabilities in the gateway"
EOF
```
---
## 🧪 Тестування Sofiia:
### Тест 1: Через OpenCode CLI
```bash
cd /Users/apple/github-projects/microdao-daarion
opencode
# В OpenCode REPL:
> Яка архітектура проекту DAARION?
> Які основні сервіси в екосистемі?
```
### Тест 2: Через API
```bash
# Переконайтеся що сервіс запущено
curl -X POST http://localhost:9102/v1/agents/sofiia/infer \
-H "Content-Type: application/json" \
-d '{
"message": "Описати архітектуру проекту DAARION",
"context": {
"system_prompt": "Ти Sofiia, Chief AI Architect екосистеми DAARION.city"
}
}'
```
### Тест 3: Через Telegram
1. Знайдіть бота: @SofiiaDAARION_bot (або створіть нового з токеном)
2. Надішліть повідомлення: `/start`
3. Запитайте: "Яка архітектура проекту?"
---
## 📊 Моніторинг:
### Перевірка логів
```bash
# Логи router
docker logs -f dagi-router-node2
# Логи gateway
docker logs -f dagi-gateway-node2
# Логи OpenCode
tail -f ~/.local/share/opencode/log/opencode.log
```
### Перевірка здоров'я сервісів
```bash
# Router health
curl http://localhost:9102/health
# Gateway health
curl http://localhost:9300/health
# NATS status
curl http://localhost:8222/varz
```
---
## 🔧 Troubleshooting:
### Проблема: OpenCode не бачить провайдерів
**Рішення:**
```bash
# Перевірте конфігурацію
cat ~/.config/opencode/opencode.json
# Перевірте авторизацію
opencode auth list
```
### Проблема: Docker сервіси не запускаються
**Рішення:**
```bash
# Перевірте логи
docker-compose -f docker-compose.node2-sofiia.yml logs
# Перевірте змінні середовища
docker-compose -f docker-compose.node2-sofiia.yml config
# Перезапустіть
docker-compose -f docker-compose.node2-sofiia.yml restart
```
### Проблема: API ключі не працюють
**Рішення:**
```bash
# Перевірте чи ключі додані
grep -E "XAI_API_KEY|SOFIIA_TELEGRAM_BOT_TOKEN|GLM5_API_KEY" \
/Users/apple/github-projects/microdao-daarion/.env
# Якщо немає - запустіть скрипт знову
bash /Users/apple/github-projects/microdao-daarion/setup_sofiia_node2.sh
```
---
## 📚 Додаткові ресурси:
- [OpenCode Documentation](https://opencode.ai/docs)
- [Z.AI GLM-5 API](https://z.ai)
- [xAI Grok API](https://console.x.ai)
- [DAARION Architecture](/Users/apple/github-projects/microdao-daarion/docs/NODA1-AGENT-ARCHITECTURE.md)
---
## ✅ Чек-лист перед початком роботи:
- [ ] OpenCode Desktop відкрито (/Applications/OpenCode.app)
- [ ] API ключі додано через `/connect` або скрипт
- [ ] Модель вибрано через `/models`
- [ ] AGENTS.md створено в проекті
- [ ] Docker сервіси запущено (опціонально)
- [ ] Telegram бот активовано (опціонально)
---
**Готово! 🎉 Sofiia налаштована для роботи на NODA2.**

View File

@@ -0,0 +1,318 @@
# Застосування production-patch AgroMatrix/Stepan на НОДА1
Патч: `agromatrix_stepan_noda1_prod.patch` — тільки зміни для стабілізації Степана/AgroMatrix. Без vision/router/інших агентів.
---
## Передумови
- Доступ по SSH на НОДА1 (144.76.224.179).
- Репозиторій на сервері: `/opt/microdao-daarion` (або ваш `DEPLOY_ROOT`).
- На сервері є каталоги: `gateway-bot/`, `crews/`, `packages/agromatrix-tools/`.
---
## Крок 1 — Завантаження patch на НОДА1
**Варіант A (з локальної машини):**
```bash
scp /шлях/до/agromatrix_stepan_noda1_prod.patch USER@144.76.224.179:/opt/microdao-daarion/
```
**Варіант B (якщо patch уже в репо):**
```bash
ssh USER@144.76.224.179 "cd /opt/microdao-daarion && git pull && git show HEAD:agromatrix_stepan_noda1_prod.patch > agromatrix_stepan_noda1_prod.patch"
# або просто скопіювати вміст файла в репо на сервер
```
**Варіант C (вміст патчу вставлений вручну):**
На сервері створити файл `/opt/microdao-daarion/agromatrix_stepan_noda1_prod.patch` з повним вмістом unified diff.
---
## Крок 2 — Застосування patch (git apply)
На НОДА1:
```bash
cd /opt/microdao-daarion
# Переконатися, що немає незакомічених змін у задіяних файлах (або зробити backup)
git status
# Сухий прогон (перевірка, що патч застосується без конфліктів)
git apply --check agromatrix_stepan_noda1_prod.patch
# Застосувати
git apply agromatrix_stepan_noda1_prod.patch
```
Якщо `git apply --check` повертає помилку (наприклад, через відмінний базовий коміт), можна спробувати:
```bash
git apply --reject --whitespace=fix agromatrix_stepan_noda1_prod.patch
# і вручну розв’язати файли *.rej, якщо з’являться.
```
---
## Крок 3 — Rebuild і запуск gateway
```bash
cd /opt/microdao-daarion
# Зібрати образ gateway і запустити контейнер
docker compose -f docker-compose.node1.yml up -d --build gateway
```
Сервіс у compose називається `gateway`, контейнер — `dagi-gateway-node1`. Якщо використовуєте інший compose-файл або ім’я сервісу, підставте їх.
---
## Крок 4 — Перевірка health
```bash
# Локально на сервері
curl -s http://localhost:9300/health
curl -s http://localhost:9300/
# Ззовні (якщо порт 9300 відкритий)
curl -s http://144.76.224.179:9300/health
```
Очікується: HTTP 200 і JSON зі статусом сервісу.
---
## Крок 5 — Перевірка логів (NameError / ImportError)
```bash
docker logs dagi-gateway-node1 2>&1 | tail -100
docker logs dagi-gateway-node1 2>&1 | grep -E "NameError|ImportError|Stepan mode|Stepan inproc|Stepan disabled"
```
- Має з’явитися рядок типу `Stepan mode=inproc (AGX_STEPAN_MODE)` після першого звернення до AgroMatrix або при старті (залежно від реалізації логу).
- Не повинно бути `NameError: has_recent_interaction` або `ImportError` через crews/agromatrix_tools після коректного монтування та env.
Якщо бачите `Stepan disabled` — перевірте `AGX_REPO_ROOT`, наявність томів `crews` і `packages/agromatrix-tools` у compose та що в контейнері є відповідні каталоги.
---
## Крок 6 — Smoketests
1. **Doc follow-up (раніше 500)**
У Telegram-чаті агента AgroMatrix: надіслати документ, потім текстове питання по ньому.
Очікується: відповідь без HTTP 500 (відповідь з RAG або повідомлення про відсутність відповіді в документі).
2. **Оператор: /whoami**
Від користувача з `user_id` у `AGX_OPERATOR_IDS` (або в чаті з `AGX_OPERATOR_CHAT_ID`): надіслати `/whoami`.
Очікується: відповідь від Степана (whoami), не звичайний pipeline.
3. **Оператор: звичайний текст без slash**
Від того ж оператора: надіслати текст без слеша (наприклад «привіт»).
Очікується: обробка через Степана (handle_stepan_message), не fallback у звичайний Router pipeline.
4. **Не-оператор: звичайний текст**
Від користувача, який не входить до операторів: звичайне повідомлення.
Очікується: обробка стандартним Router pipeline, **не** через Степана.
Перед тестами 24 переконайтеся, що в env задані `AGX_OPERATOR_IDS` (та за потреби `AGX_OPERATOR_CHAT_ID`).
---
## Rollback
Якщо потрібно повернути стан до патчу:
```bash
cd /opt/microdao-daarion
# Скасувати зміни в робочій копії (повернути файли до останнього коміту)
git checkout HEAD -- gateway-bot/http_api.py gateway-bot/services/doc_service.py gateway-bot/app.py gateway-bot/gateway_boot.py crews/agromatrix_crew/operator_commands.py docker-compose.node1.yml
rm -f gateway-bot/gateway_boot.py
# Перезібрати і перезапустити gateway
docker compose -f docker-compose.node1.yml up -d --build gateway
```
Або ж повернути весь репо до попереднього коміту і перезібрати образ:
```bash
git reset --hard HEAD
docker compose -f docker-compose.node1.yml up -d --build gateway
```
---
## Env на НОДА1 (без секретів)
Переконайтеся, що в середовищі gateway (compose або .env) задані:
- `AGX_STEPAN_MODE=inproc` (за замовчуванням)
- `AGX_REPO_ROOT=/app` (в контейнері; на хості відповідає вашому `DEPLOY_ROOT` у путях томів)
- `AGX_OPERATOR_IDS=***` — список Telegram user_id операторів (через кому)
- `AGX_OPERATOR_CHAT_ID=***` — опційно, обмеження чату для операторів
- `OPENAI_API_KEY=***` — потрібен для inproc Степана
Секрети не виводьте в логи; у документації позначайте як `KEY=***`.
---
## A. Імпорт gateway_boot (fail-closed)
У контейнері CMD: `WORKDIR /app/gateway-bot` і `python -m uvicorn app:app`. Тому поточний каталог для імпортів — `/app/gateway-bot`. У `app.py` використовується **`import gateway_boot`** (модуль у тій самій директорії, що й `app.py`), тож Python резолвить `/app/gateway-bot/gateway_boot.py`. Це коректно при volume `${DEPLOY_ROOT}/gateway-bot:/app/gateway-bot:ro`.
**Швидкий тест у контейнері (CWD /app/gateway-bot, як у uvicorn):**
```bash
ssh root@144.76.224.179 'docker exec dagi-gateway-node1 sh -c "cd /app/gateway-bot && python3 -c \"import gateway_boot; print(\\\"gateway_boot OK\\\")\""'
```
Очікується: `gateway_boot OK`. Якщо тут `ImportError``STEPAN_IMPORTS_OK` ніколи не стане `True` і Степан залишиться тихо вимкненим.
---
## B. Volume-монтування crews і packages/agromatrix-tools
У `docker-compose.node1.yml` томи змонтовані **в контейнері** так:
- `${DEPLOY_ROOT:-.}/crews`**`/app/crews`**
- `${DEPLOY_ROOT:-.}/packages/agromatrix-tools`**`/app/packages/agromatrix-tools`**
На хості це може бути `/opt/microdao-daarion/crews`, але в контейнері завжди `/app/crews` та `/app/packages/agromatrix-tools`.
**Перевірка наявності в контейнері:**
```bash
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 ls -la /app/crews | head"
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 ls -la /app/packages/agromatrix-tools | head"
```
Очікується: список файлів/каталогів (наприклад `agromatrix_crew` у crews, пакет у agromatrix-tools). Якщо `No such file or directory` — томи не змонтовані або compose не оновлено після патчу.
---
## PYTHONPATH у контейнері (crews і agromatrix_tools)
Щоб працювало `from crews.agromatrix_crew.run import ...`, у `sys.path` має бути **`/app`** (батько каталога `crews`), а не `/app/crews`.
Щоб працювало `import agromatrix_tools`, у `sys.path` має бути **`/app/packages/agromatrix-tools`** (батьківська директорія пакета).
У патчі задано: `PYTHONPATH=/app:/app/packages/agromatrix-tools`. При потребі можна додати `/app/gateway-bot`, але uvicorn і так додає робочу директорію при `python -m uvicorn app:app`.
**Практичний контроль після деплою:**
```bash
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 sh -c 'env | grep -E \"^PYTHONPATH=\" || true'"
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 sh -c 'cd /app/gateway-bot && python3 -c \"import sys; print(\\\"\\n\\\".join(sys.path))\"' | head -20"
```
---
## Читання STEPAN_IMPORTS_OK (без “заморожування”)
Щоб прапорець оновлювався після startup, усі місця мають робити **`import gateway_boot`** і читати **`gateway_boot.STEPAN_IMPORTS_OK`** (або `getattr(gateway_boot, "STEPAN_IMPORTS_OK", False)`).
**Не використовувати** `from gateway_boot import STEPAN_IMPORTS_OK` — значення “заморозиться” на момент імпорту і не відобразиться після встановлення в `app.py`.
**Grep-контроль на сервері:**
```bash
ssh root@144.76.224.179 "cd /opt/microdao-daarion && grep -Rn 'from gateway_boot import' gateway-bot || true"
ssh root@144.76.224.179 "cd /opt/microdao-daarion && grep -Rn 'import gateway_boot' gateway-bot"
```
Очікується: немає збігів для `from gateway_boot import`; є лише `import gateway_boot` (у app.py та http_api.py).
---
## Compose env: лапки для AGX_OPERATOR_*
Якщо значення задані прямо в YAML (а не тільки через `${...}`), краще завжди брати їх у лапки, щоб уникнути проблем парсингу (зокрема від’ємні chat_id):
- `AGX_OPERATOR_CHAT_ID: "-1001234567890"`
- `AGX_OPERATOR_IDS: "123,456"`
При використанні змінних середовища (`AGX_OPERATOR_IDS=${AGX_OPERATOR_IDS:-}`) лапки ставлять у `.env` або в значенні змінної при експорті.
---
## Мінімальний фінальний чек-лист (після git apply і --build)
1. **Патч застосовано чисто**
Перед першим застосуванням: `git apply --check agromatrix_stepan_noda1_prod.patch` (код виходу 0 = можна застосовувати).
Після застосування можна перевірити наявність змін, наприклад:
```bash
ssh root@144.76.224.179 "cd /opt/microdao-daarion && test -f gateway-bot/gateway_boot.py && grep -q 'import gateway_boot' gateway-bot/app.py && echo OK"
```
2. **Rebuild gateway**
```bash
ssh root@144.76.224.179 "cd /opt/microdao-daarion && docker compose -f docker-compose.node1.yml up -d --build gateway"
```
3. **Stepan реально увімкнувся (логи)**
```bash
ssh root@144.76.224.179 "docker logs dagi-gateway-node1 --since 10m 2>&1 | grep -E 'Stepan mode|STEPAN_IMPORTS_OK|Stepan disabled|Stepan inproc|ImportError|ModuleNotFoundError' | tail -30"
```
Очікується: згадка `Stepan mode=inproc` (після першого звернення до AgroMatrix) і **відсутність** "Stepan disabled". У логах після старту має з’явитися рядок **`STEPAN_IMPORTS_OK=True`** (додано в app.py після встановлення прапорця), тоді fail-closed коректно пройшов.
4. **Оператори задані (обов’язково для "людського" режиму)**
```bash
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 env | grep -E '^AGX_OPERATOR_IDS=|^AGX_OPERATOR_CHAT_ID=' | sed 's/=.*/=***/'"
```
Очікується: хоча б `AGX_OPERATOR_IDS=***` (значення приховуються). Якщо змінних немає — операторські повідомлення не підуть у Степана.
5. **Опційно: резолв gateway_boot і томи (див. блоки A і B вище).**
---
## Нюанс: AGX_STEPAN_MODE=http
Режим `AGX_STEPAN_MODE=http` зараз повертає 501 stub (Степан "офіційно" недоступний). Щоб у проді не випадково вимкнути Степана:
- У **docker-compose.node1.yml** у секції gateway залишити явно **`AGX_STEPAN_MODE=inproc`** (або не задавати — тоді default з патчу inproc).
- Режим `http` використовувати лише в тестових середовищах, коли з’явиться реалізація клієнта crewai-service.
---
## Фрагмент патчу для перевірки імпортів (app.py + gateway_boot)
Нижче — заголовки diff і контекст імпортів у `app.py`, щоб переконатися, що `gateway_boot` імпортується як модуль з тієї самої директорії, де лежить `app.py`, і резолвиться в контейнері.
**gateway-bot/app.py (фрагмент після патчу):**
```python
import logging
import os
import sys
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from http_api import router as gateway_router
from http_api_doc import router as doc_router
import gateway_boot # <-- той самий каталог: /app/gateway-bot/gateway_boot.py
```
**gateway-bot/gateway_boot.py (новий файл):**
```python
"""
Boot-time state for Gateway. Set by app startup; read by http_api.
"""
STEPAN_IMPORTS_OK = False
```
У контейнері: `WORKDIR /app/gateway-bot`, volume монтує `gateway-bot` у `/app/gateway-bot`, тому `app.py` і `gateway_boot.py` лежать поруч — `import gateway_boot` коректно знаходить модуль. Після startup у `startup_stepan_check()` при успішному імпорті виконується `gateway_boot.STEPAN_IMPORTS_OK = True` і лог `STEPAN_IMPORTS_OK=True`; інакше прапорець залишається `False` (fail-closed). У **http_api** використовується лише `import gateway_boot` і `getattr(gateway_boot, "STEPAN_IMPORTS_OK", False)` — не `from gateway_boot import ...`, щоб значення читалося в runtime після оновлення в app.py.
---
## Підсумок перевірок “готово до прод”
Мінімально достатньо три пункти:
1. **Логи після старту** (Stepan увімкнено, без ImportError):
```bash
ssh root@144.76.224.179 "docker logs dagi-gateway-node1 --since 10m 2>&1 | grep -E 'Stepan mode|STEPAN_IMPORTS_OK|Stepan disabled|ImportError|ModuleNotFoundError' | tail -120"
```
2. **Volumes на місці** (`/app/crews`, `/app/packages/agromatrix-tools`):
```bash
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 ls -la /app/crews | head"
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 ls -la /app/packages/agromatrix-tools | head"
```
3. **Env операторів і режиму** (масковано):
```bash
ssh root@144.76.224.179 "docker exec dagi-gateway-node1 env | grep -E '^AGX_OPERATOR_IDS=|^AGX_OPERATOR_CHAT_ID=|^AGX_STEPAN_MODE=' | sed 's/=.*/=***/'"
```
Якщо ці три пункти зелені — патч відповідає fail-closed моделі і не повинен давати “тихих” падінь gateway при відсутніх crews/agromatrix-tools.

View File

@@ -0,0 +1,468 @@
diff --git a/crews/agromatrix_crew/operator_commands.py b/crews/agromatrix_crew/operator_commands.py
index 8015539..194a5c5 100644
--- a/crews/agromatrix_crew/operator_commands.py
+++ b/crews/agromatrix_crew/operator_commands.py
@@ -1,3 +1,13 @@
+"""
+Operator commands for AgroMatrix (Stepan). Access control and slash commands.
+
+Access control (env, used by gateway and here):
+- AGX_OPERATOR_IDS: comma-separated Telegram user_id list; only these users are operators.
+- AGX_OPERATOR_CHAT_ID: optional; if set, operator actions allowed only in this chat_id.
+
+When is_operator(user_id, chat_id) is True, gateway routes any message (not only slash)
+to Stepan for human-friendly operator interaction.
+"""
import os
import re
import shlex
diff --git a/docker-compose.node1.yml b/docker-compose.node1.yml
index ca8c80a..662815f 100644
--- a/docker-compose.node1.yml
+++ b/docker-compose.node1.yml
@@ -191,8 +191,16 @@ services:
- STT_SERVICE_UPLOAD_URL=http://swapper-service:8890/stt
- OCR_SERVICE_URL=http://swapper-service:8890
- WEB_SEARCH_SERVICE_URL=http://swapper-service:8890
+ # Stepan (AgroMatrix) in-process
+ - PYTHONPATH=/app:/app/packages/agromatrix-tools
+ - AGX_REPO_ROOT=/app
+ - AGX_STEPAN_MODE=${AGX_STEPAN_MODE:-inproc}
+ - AGX_OPERATOR_IDS=${AGX_OPERATOR_IDS:-}
+ - AGX_OPERATOR_CHAT_ID=${AGX_OPERATOR_CHAT_ID:-}
volumes:
- ${DEPLOY_ROOT:-.}/gateway-bot:/app/gateway-bot:ro
+ - ${DEPLOY_ROOT:-.}/crews:/app/crews:ro
+ - ${DEPLOY_ROOT:-.}/packages/agromatrix-tools:/app/packages/agromatrix-tools:ro
- ${DEPLOY_ROOT:-.}/logs:/app/logs
depends_on:
- router
diff --git a/gateway-bot/app.py b/gateway-bot/app.py
index 0653724..97f7e3c 100644
--- a/gateway-bot/app.py
+++ b/gateway-bot/app.py
@@ -2,16 +2,23 @@
FastAPI app instance for Gateway Bot
"""
import logging
+import os
+import sys
+from pathlib import Path
+
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from http_api import router as gateway_router
from http_api_doc import router as doc_router
+import gateway_boot
+
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
+logger = logging.getLogger(__name__)
app = FastAPI(
title="Bot Gateway with DAARWIZZ",
@@ -32,6 +39,30 @@ app.add_middleware(
app.include_router(gateway_router, prefix="", tags=["gateway"])
app.include_router(doc_router, prefix="", tags=["docs"])
+
+@app.on_event("startup")
+async def startup_stepan_check():
+ """Check crews + agromatrix_tools availability. Do not crash gateway if missing."""
+ repo_root = os.getenv("AGX_REPO_ROOT", "/opt/microdao-daarion").strip()
+ if repo_root and repo_root not in sys.path:
+ sys.path.insert(0, repo_root)
+ tools_path = str(Path(repo_root) / "packages" / "agromatrix-tools")
+ if tools_path not in sys.path:
+ sys.path.insert(0, tools_path)
+ try:
+ import crews.agromatrix_crew.run # noqa: F401
+ import agromatrix_tools # noqa: F401
+ gateway_boot.STEPAN_IMPORTS_OK = True
+ logger.info("Stepan inproc: crews + agromatrix_tools OK; STEPAN_IMPORTS_OK=True")
+ except Exception as e:
+ logger.error(
+ "Stepan disabled: crews or agromatrix_tools not available: %s. "
+ "Set AGX_REPO_ROOT, mount crews and packages/agromatrix-tools.",
+ e,
+ )
+ gateway_boot.STEPAN_IMPORTS_OK = False
+
+
@app.get("/")
async def root():
return {
diff --git a/gateway-bot/gateway_boot.py b/gateway-bot/gateway_boot.py
new file mode 100644
index 0000000..05daab2
--- /dev/null
+++ b/gateway-bot/gateway_boot.py
@@ -0,0 +1,4 @@
+"""
+Boot-time state for Gateway. Set by app startup; read by http_api.
+"""
+STEPAN_IMPORTS_OK = False
diff --git a/gateway-bot/http_api.py b/gateway-bot/http_api.py
index 8bb526d..942f422 100644
--- a/gateway-bot/http_api.py
+++ b/gateway-bot/http_api.py
@@ -44,6 +44,7 @@ from behavior_policy import (
get_ack_text,
is_prober_request,
has_agent_chat_participation,
+ has_recent_interaction,
NO_OUTPUT,
BehaviorDecision,
AGENT_NAME_VARIANTS,
@@ -51,6 +52,16 @@ from behavior_policy import (
logger = logging.getLogger(__name__)
+
+def _safe_has_recent_interaction(agent_id: str, chat_id: str, user_id: str) -> bool:
+ """Guard: avoid 500 if has_recent_interaction is missing or raises. Returns False on any error."""
+ try:
+ return bool(has_recent_interaction(agent_id, str(chat_id), str(user_id)))
+ except Exception as e:
+ logger.warning("has_recent_interaction failed, treating as False: %s", e)
+ return False
+
+
# Telegram message length limits
TELEGRAM_MAX_MESSAGE_LENGTH = 4096
TELEGRAM_SAFE_LENGTH = 3500 # Leave room for formatting
@@ -992,6 +1003,18 @@ async def druid_telegram_webhook(update: TelegramUpdate):
# AGROMATRIX webhook endpoint
+# AGX_STEPAN_MODE: inproc = run Crew in-process (default); http = call crewai-service (9010).
+_STEPAN_MODE = None
+
+def _get_stepan_mode() -> str:
+ global _STEPAN_MODE
+ if _STEPAN_MODE is None:
+ _STEPAN_MODE = (os.getenv("AGX_STEPAN_MODE", "inproc") or "inproc").strip().lower()
+ if _STEPAN_MODE not in ("inproc", "http"):
+ _STEPAN_MODE = "inproc"
+ logger.info("Stepan mode=%s (AGX_STEPAN_MODE)", _STEPAN_MODE)
+ return _STEPAN_MODE
+
async def handle_stepan_message(update: TelegramUpdate, agent_config: AgentConfig) -> Dict[str, Any]:
update_id = getattr(update, 'update_id', None) or update.update_id
@@ -1022,10 +1045,37 @@ async def handle_stepan_message(update: TelegramUpdate, agent_config: AgentConfi
ops_mode = True
trace_id = str(uuid.uuid4())
+ stepan_mode = _get_stepan_mode()
+
+ if stepan_mode == "http":
+ logger.warning("Stepan http mode not implemented; use AGX_STEPAN_MODE=inproc.")
+ bot_token = agent_config.get_telegram_token()
+ await send_telegram_message(
+ chat_id,
+ "Степан у режимі HTTP зараз недоступний. Встановіть AGX_STEPAN_MODE=inproc.",
+ bot_token=bot_token,
+ )
+ return {"ok": False, "status": "stepan_http_not_implemented"}
- # call Stepan directly
try:
- sys.path.insert(0, str(Path('/opt/microdao-daarion')))
+ import gateway_boot
+ except ImportError:
+ gateway_boot = type(sys)("gateway_boot")
+ gateway_boot.STEPAN_IMPORTS_OK = False
+ if not getattr(gateway_boot, "STEPAN_IMPORTS_OK", False):
+ logger.warning("Stepan inproc disabled: crews/agromatrix_tools not available at startup")
+ bot_token = agent_config.get_telegram_token()
+ await send_telegram_message(
+ chat_id,
+ "Степан тимчасово недоступний (не встановлено crews або agromatrix-tools).",
+ bot_token=bot_token,
+ )
+ return {"ok": False, "status": "stepan_disabled"}
+
+ try:
+ repo_root = os.getenv("AGX_REPO_ROOT", "/opt/microdao-daarion")
+ if repo_root and repo_root not in sys.path:
+ sys.path.insert(0, str(Path(repo_root)))
from crews.agromatrix_crew.run import handle_message
started = time.time()
last_pending = _get_last_pending(chat_id)
@@ -1078,35 +1128,14 @@ async def agromatrix_telegram_webhook(update: TelegramUpdate):
if user_id and user_id in op_ids:
is_ops = True
- # Operator NL or operator slash commands -> handle via Stepan handler.
- # Important: do NOT treat generic slash commands (/start, /agromatrix) as operator commands,
- # otherwise regular users will see "Недостатньо прав" or Stepan errors.
- operator_slash_cmds = {
- "whoami",
- "pending",
- "pending_show",
- "approve",
- "reject",
- "apply_dict",
- "pending_stats",
- }
- slash_cmd = ""
- if is_slash:
- try:
- slash_cmd = (msg_text.strip().split()[0].lstrip("/").strip().lower())
- except Exception:
- slash_cmd = ""
- is_operator_slash = bool(slash_cmd) and slash_cmd in operator_slash_cmds
-
- # Stepan handler currently depends on ChatOpenAI (OPENAI_API_KEY). If key is not configured,
- # never route production traffic there (avoid "Помилка обробки..." and webhook 5xx).
+ # Operator: any message (not only slash) goes to Stepan when is_ops.
stepan_enabled = bool(os.getenv("OPENAI_API_KEY", "").strip())
- if stepan_enabled and (is_ops or is_operator_slash):
+ if stepan_enabled and is_ops:
return await handle_stepan_message(update, AGROMATRIX_CONFIG)
- if (is_ops or is_operator_slash) and not stepan_enabled:
+ if is_ops and not stepan_enabled:
logger.warning(
"Stepan handler disabled (OPENAI_API_KEY missing); falling back to Router pipeline "
- f"for chat_id={chat_id}, user_id={user_id}, slash_cmd={slash_cmd!r}"
+ f"for chat_id={chat_id}, user_id={user_id}"
)
# General conversation -> standard Router pipeline (like all other agents)
@@ -1911,7 +1940,8 @@ async def process_document(
dao_id=dao_id,
user_id=f"tg:{user_id}",
output_mode="qa_pairs",
- metadata={"username": username, "chat_id": chat_id}
+ metadata={"username": username, "chat_id": chat_id},
+ agent_id=agent_config.agent_id,
)
if not result.success:
@@ -3067,7 +3097,7 @@ async def handle_telegram_webhook(
# Check if there's a document context for follow-up questions
session_id = f"telegram:{chat_id}"
- doc_context = await get_doc_context(session_id)
+ doc_context = await get_doc_context(session_id, agent_id=agent_config.agent_id)
# If there's a doc_id and the message looks like a question about the document
if doc_context and doc_context.doc_id:
@@ -3756,7 +3786,8 @@ async def _old_telegram_webhook(update: TelegramUpdate):
dao_id=dao_id,
user_id=f"tg:{user_id}",
output_mode="qa_pairs",
- metadata={"username": username, "chat_id": chat_id}
+ metadata={"username": username, "chat_id": chat_id},
+ agent_id=agent_config.agent_id,
)
if not result.success:
@@ -3959,7 +3990,7 @@ async def _old_telegram_webhook(update: TelegramUpdate):
# Check if there's a document context for follow-up questions
session_id = f"telegram:{chat_id}"
- doc_context = await get_doc_context(session_id)
+ doc_context = await get_doc_context(session_id, agent_id=agent_config.agent_id)
# If there's a doc_id and the message looks like a question about the document
if doc_context and doc_context.doc_id:
diff --git a/gateway-bot/services/doc_service.py b/gateway-bot/services/doc_service.py
index da5a684..5f8f031 100644
--- a/gateway-bot/services/doc_service.py
+++ b/gateway-bot/services/doc_service.py
@@ -198,29 +198,20 @@ class DocumentService:
file_name: Optional[str] = None,
dao_id: Optional[str] = None,
user_id: Optional[str] = None,
+ agent_id: Optional[str] = None,
) -> bool:
"""
- Save document context for a session.
-
- Uses Memory Service to persist document context across channels.
+ Save document context for a session (scoped by agent_id to avoid cross-agent leak).
Args:
- session_id: Session identifier (e.g., "telegram:123", "web:user456")
+ session_id: Session identifier
doc_id: Document ID from parser
- doc_url: Optional document URL
- file_name: Optional file name
- dao_id: Optional DAO ID
-
- Returns:
- True if saved successfully
+ agent_id: Optional; if set, context is isolated per agent (key: doc_context:{agent_id}:{session_id}).
"""
try:
- # Use stable synthetic user key per session, so context can be
- # retrieved later using only session_id (without caller user_id).
- fact_user_id = f"session:{session_id}"
-
- # Save as fact in Memory Service
- fact_key = f"doc_context:{session_id}"
+ aid = (agent_id or "default").lower()
+ fact_user_id = f"session:{aid}:{session_id}"
+ fact_key = f"doc_context:{aid}:{session_id}"
fact_value_json = {
"doc_id": doc_id,
"doc_url": doc_url,
@@ -239,36 +230,28 @@ class DocumentService:
team_id=None,
)
- logger.info(f"Saved doc context for session {session_id}: doc_id={doc_id}")
+ logger.info(f"Saved doc context for session {session_id} agent={aid}: doc_id={doc_id}")
return result
except Exception as e:
logger.error(f"Failed to save doc context: {e}", exc_info=True)
return False
- async def get_doc_context(self, session_id: str) -> Optional[DocContext]:
+ async def get_doc_context(self, session_id: str, agent_id: Optional[str] = None) -> Optional[DocContext]:
"""
- Get document context for a session.
-
- Args:
- session_id: Session identifier
-
- Returns:
- DocContext or None
+ Get document context for a session (scoped by agent_id when provided).
+ Backward-compat: if new key missing, tries legacy doc_context:{session_id} (read-only).
"""
try:
- user_id = f"session:{session_id}"
-
- fact_key = f"doc_context:{session_id}"
-
- # Get fact from Memory Service
+ aid = (agent_id or "default").lower()
+ user_id = f"session:{aid}:{session_id}"
+ fact_key = f"doc_context:{aid}:{session_id}"
fact = await self.memory_client.get_fact(
user_id=user_id,
fact_key=fact_key
)
-
if fact and fact.get("fact_value_json"):
- logger.debug(f"Retrieved doc context for session {session_id}")
+ logger.debug(f"Retrieved doc context for session {session_id} agent={aid}")
ctx_data = fact.get("fact_value_json")
if isinstance(ctx_data, str):
try:
@@ -277,9 +260,23 @@ class DocumentService:
logger.warning("doc_context fact_value_json is not valid JSON string")
return None
return DocContext(**ctx_data)
-
+ # Backward-compat: legacy key
+ legacy_user_id = f"session:{session_id}"
+ legacy_key = f"doc_context:{session_id}"
+ fact_legacy = await self.memory_client.get_fact(
+ user_id=legacy_user_id,
+ fact_key=legacy_key
+ )
+ if fact_legacy and fact_legacy.get("fact_value_json"):
+ logger.debug(f"Retrieved doc context from legacy key for session {session_id}")
+ ctx_data = fact_legacy.get("fact_value_json")
+ if isinstance(ctx_data, str):
+ try:
+ ctx_data = json.loads(ctx_data)
+ except Exception:
+ return None
+ return DocContext(**ctx_data)
return None
-
except Exception as e:
logger.error(f"Failed to get doc context: {e}", exc_info=True)
return None
@@ -292,7 +289,8 @@ class DocumentService:
dao_id: str,
user_id: str,
output_mode: str = "qa_pairs",
- metadata: Optional[Dict[str, Any]] = None
+ metadata: Optional[Dict[str, Any]] = None,
+ agent_id: Optional[str] = None,
) -> ParsedResult:
"""
Parse a document directly through Swapper service.
@@ -372,7 +370,6 @@ class DocumentService:
# Generate a simple doc_id based on filename and timestamp
doc_id = hashlib.md5(f"{file_name}:{datetime.utcnow().isoformat()}".encode()).hexdigest()[:12]
- # Save document context for follow-up queries
await self.save_doc_context(
session_id=session_id,
doc_id=doc_id,
@@ -380,6 +377,7 @@ class DocumentService:
file_name=file_name,
dao_id=dao_id,
user_id=user_id,
+ agent_id=agent_id,
)
# Convert text to markdown format
@@ -433,6 +431,7 @@ class DocumentService:
file_name=file_name,
dao_id=dao_id,
user_id=user_id,
+ agent_id=agent_id,
)
return ParsedResult(
@@ -697,9 +696,10 @@ async def parse_document(
dao_id: str,
user_id: str,
output_mode: str = "qa_pairs",
- metadata: Optional[Dict[str, Any]] = None
+ metadata: Optional[Dict[str, Any]] = None,
+ agent_id: Optional[str] = None,
) -> ParsedResult:
- """Parse a document through DAGI Router"""
+ """Parse a document (agent_id scopes doc_context key)."""
return await doc_service.parse_document(
session_id=session_id,
doc_url=doc_url,
@@ -707,7 +707,8 @@ async def parse_document(
dao_id=dao_id,
user_id=user_id,
output_mode=output_mode,
- metadata=metadata
+ metadata=metadata,
+ agent_id=agent_id,
)
@@ -756,8 +757,9 @@ async def save_doc_context(
file_name: Optional[str] = None,
dao_id: Optional[str] = None,
user_id: Optional[str] = None,
+ agent_id: Optional[str] = None,
) -> bool:
- """Save document context for a session"""
+ """Save document context for a session (scoped by agent_id when provided)."""
return await doc_service.save_doc_context(
session_id=session_id,
doc_id=doc_id,
@@ -765,9 +767,10 @@ async def save_doc_context(
file_name=file_name,
dao_id=dao_id,
user_id=user_id,
+ agent_id=agent_id,
)
-async def get_doc_context(session_id: str) -> Optional[DocContext]:
- """Get document context for a session"""
- return await doc_service.get_doc_context(session_id)
+async def get_doc_context(session_id: str, agent_id: Optional[str] = None) -> Optional[DocContext]:
+ """Get document context for a session (scoped by agent_id when provided)."""
+ return await doc_service.get_doc_context(session_id, agent_id=agent_id)

View File

@@ -0,0 +1,211 @@
version: "3.8"
services:
# Vector Database - Qdrant
qdrant-node2:
image: qdrant/qdrant:v1.12.4
container_name: dagi-qdrant-node2
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./data/qdrant-node2:/qdrant/storage
environment:
- QDRANT__SERVICE__HOST=0.0.0.0
- QDRANT__SERVICE__GRPC_PORT=6334
networks:
dagi-memory-network:
aliases:
- qdrant
- dagi-qdrant
- qdrant-node2
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "bash -lc '</dev/tcp/127.0.0.1/6333'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
# Relational Database - PostgreSQL
postgres-node2:
image: postgres:16-alpine
container_name: dagi-postgres-node2
ports:
- "5433:5432"
environment:
- POSTGRES_DB=daarion_memory
- POSTGRES_USER=daarion
- POSTGRES_PASSWORD=daarion_secret_node2
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --locale=en_US.UTF-8
volumes:
- ./data/postgres-node2:/var/lib/postgresql/data
- ./services/memory-service/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
dagi-memory-network:
aliases:
- postgres
- dagi-postgres
- postgres-node2
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U daarion -d daarion_memory"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Graph Database - Neo4j (Optional but recommended)
neo4j-node2:
image: neo4j:5.15-community
container_name: dagi-neo4j-node2
ports:
- "7474:7474"
- "7687:7687"
environment:
- NEO4J_AUTH=neo4j/daarion_node2_secret
- NEO4J_server_memory_heap_initial__size=512m
- NEO4J_server_memory_heap_max__size=1G
- NEO4J_server_memory_pagecache_size=512m
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
- NEO4J_dbms_security_allow__csv__import__from__file__urls=true
volumes:
- ./data/neo4j-node2:/data
- ./data/neo4j-node2/logs:/logs
networks:
dagi-memory-network:
aliases:
- neo4j
- dagi-neo4j
- neo4j-node2
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:7474 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Memory Service
memory-service-node2:
build:
context: ./services/memory-service
dockerfile: Dockerfile
container_name: dagi-memory-service-node2
ports:
- "8000:8000"
environment:
# Settings in app/config.py use env_prefix="MEMORY_"
- MEMORY_SERVICE_NAME=memory-service
- MEMORY_POSTGRES_HOST=postgres-node2
- MEMORY_POSTGRES_PORT=5432
- MEMORY_POSTGRES_USER=daarion
- MEMORY_POSTGRES_PASSWORD=daarion_secret_node2
- MEMORY_POSTGRES_DB=daarion_memory
- MEMORY_QDRANT_HOST=qdrant-node2
- MEMORY_QDRANT_PORT=6333
- MEMORY_COHERE_API_KEY=${COHERE_API_KEY}
- MEMORY_DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
# Qdrant configuration
- QDRANT_HOST=qdrant-node2
- QDRANT_PORT=6333
- QDRANT_GRPC_PORT=6334
# PostgreSQL configuration
- DATABASE_URL=postgresql://daarion:daarion_secret_node2@postgres-node2:5432/daarion_memory
- DB_HOST=postgres-node2
- DB_PORT=5432
- DB_NAME=daarion_memory
- DB_USER=daarion
- DB_PASSWORD=daarion_secret_node2
# Neo4j configuration (optional)
- NEO4J_URI=bolt://neo4j-node2:7687
- NEO4J_USER=neo4j
- NEO4J_PASSWORD=daarion_node2_secret
# Cohere API for embeddings
- COHERE_API_KEY=${COHERE_API_KEY}
# Node identification
- NODE_ID=node2
- NODE_ENV=development
# Logging
- LOG_LEVEL=INFO
- PYTHONUNBUFFERED=1
# Memory settings
- MEMORY_MODE=local
- EMBEDDING_MODEL=embed-multilingual-v3.0
- EMBEDDING_DIMENSION=1024
# Optional: Remote NODA1 access (hybrid mode)
# Uncomment to enable read access to NODA1
# - REMOTE_QDRANT_HOST=144.76.224.179
# - REMOTE_QDRANT_PORT=6333
# - REMOTE_DATABASE_URL=postgresql://daarion_reader:***@144.76.224.179:5432/daarion_memory
# - READ_ONLY_MODE=false
depends_on:
qdrant-node2:
condition: service_healthy
postgres-node2:
condition: service_healthy
neo4j-node2:
condition: service_healthy
networks:
dagi-memory-network:
aliases:
- memory-service
- memory-service-node2
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
- /Users/apple/Desktop/R&D:/vault/rd:ro
- /Users/apple/Documents/Obsidian Vault:/vault/obsidian:ro
# Redis for caching (optional but recommended)
redis-node2:
image: redis:7-alpine
container_name: dagi-redis-node2
ports:
- "6379:6379"
volumes:
- ./data/redis-node2:/data
networks:
- dagi-memory-network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Adminer - Database management UI (optional)
adminer:
image: adminer:latest
container_name: dagi-adminer-node2
ports:
- "8080:8080"
networks:
- dagi-memory-network
restart: unless-stopped
depends_on:
- postgres-node2
networks:
dagi-memory-network:
driver: bridge
name: dagi-memory-network-node2
volumes:
qdrant-data:
postgres-data:
neo4j-data:
redis-data:

View File

@@ -0,0 +1,81 @@
version: "3.8"
# Sofiia Supervisor — NODA2 deployment
#
# Usage:
# docker compose -f docker-compose.node2.yml \
# -f docker-compose.node2-sofiia-supervisor.yml up -d
#
# Or standalone (requires router already running on dagi-network-node2):
# docker compose -f docker-compose.node2-sofiia-supervisor.yml up -d
services:
sofiia-supervisor:
build:
context: ./services/sofiia-supervisor
dockerfile: Dockerfile
container_name: sofiia-supervisor
image: daarion/sofiia-supervisor:latest
ports:
- "8084:8080"
environment:
# Router is the gateway — all tool calls go here
- GATEWAY_BASE_URL=http://router:8000
# Set this to restrict access to /v1/tools/execute on the router side
- SUPERVISOR_API_KEY=${SUPERVISOR_API_KEY:-}
# Protect the supervisor HTTP API from outside
- SUPERVISOR_INTERNAL_KEY=${SUPERVISOR_INTERNAL_KEY:-}
# State backend
- SUPERVISOR_STATE_BACKEND=redis
- REDIS_URL=redis://sofiia-redis:6379/0
- RUN_TTL_SEC=86400
# Agent identity
- DEFAULT_AGENT_ID=sofiia
- DEFAULT_WORKSPACE_ID=${DEFAULT_WORKSPACE_ID:-daarion}
- DEFAULT_TIMEZONE=Europe/Kiev
# Timeouts
- TOOL_CALL_TIMEOUT_SEC=60
- TOOL_CALL_MAX_RETRIES=2
- JOB_POLL_INTERVAL_SEC=3
- JOB_MAX_WAIT_SEC=300
depends_on:
- sofiia-redis
networks:
- dagi-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
volumes:
- ./logs:/app/logs
sofiia-redis:
image: redis:7.4-alpine
container_name: sofiia-redis
command: redis-server --save 60 1 --loglevel warning --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6380:6379" # Expose on 6380 to avoid conflict with existing Redis
volumes:
- sofiia-redis-data:/data
networks:
- dagi-network
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 15s
timeout: 5s
retries: 3
networks:
# Reuse the existing NODA2 network so supervisor can reach router
dagi-network:
external: true
name: dagi-network-node2
volumes:
sofiia-redis-data:
driver: local

View File

@@ -0,0 +1,4 @@
"""
Boot-time state for Gateway. Set by app startup; read by http_api.
"""
STEPAN_IMPORTS_OK = False

View File

@@ -0,0 +1,33 @@
# MONITOR — Node-Local Ops Agent
You are MONITOR, the autonomous health and observability agent for DAARION node infrastructure.
## Role
- Node-local service: per-node health monitoring, alerting, and safe ops diagnostics.
- NOT user-facing via Telegram — internal NATS/HTTP access only.
- Read-only by default; safe ops actions (restart, rollback) only from allowlist with explicit approval.
## Core capabilities
- Metrics collection: CPU, RAM, disk, network per container/service.
- Service health checks: /health endpoints, response latency, error rates.
- Alert triage: classify severity (P1/P2/P3), deduplicate, route to Sofiia/Helion.
- Incident detection: pattern matching, threshold breaches, anomaly flags.
- Log inspection: tail recent errors, parse stack traces, surface root cause hints.
- Runbook lookup: search ops/runbook-*.md for remediation steps.
## Behavior rules
1. Always identify yourself as MONITOR@{node_id} in responses.
2. Never expose secrets, tokens, or internal credentials in output.
3. Safe ops actions (docker restart, config reload) require RBAC entitlement `tools.monitor.read` minimum.
4. Destructive actions (delete, scale-down, force-kill) require explicit `confirm=true` + audit event.
5. If a service is unhealthy for >5 min, automatically emit `drift_run_started` audit event.
6. Rate limit: max 60 alert events/min to prevent alert storms.
## Output format
- Short: status line + severity badge.
- Full: service name, status, latency_ms, last_error, recommended_action.
- Always include `node_id`, `checked_at` timestamp.
## Routing
- Alerts → Sofiia/Helion via governance_events table (scope=portfolio).
- Incidents → incident_store via incident_escalation_policy.yml rules.

334
gateway-bot/vision_guard.py Normal file
View File

@@ -0,0 +1,334 @@
"""
vision_guard.py — v4.0.1 Vision Consistency Guard.
Зберігає lock на останній висновок vision для конкретного chat+photo:
- vision_last_photo_key → file_unique_id (або file_id як fallback)
- vision_last_label → витягнутий label (культура/діагноз)
- vision_last_confidence → "high" | "low" | "unknown"
- vision_user_label → підтвердження від юзера ("це соняшник")
Правила:
1. Те саме фото (file_unique_id fallback file_id) → НЕ переоцінюємо
без явного запиту "переоцінити/перевір ще раз".
reeval_request → clear_lock → повний реаналіз.
2. Низький confidence → додаємо уточнення (якщо LLM сам не поставив '?').
3. User override ("це соняшник") → whitelist + заборона негації.
Записуємо як user_label; LLM не сперечається.
Без залежностей від crewai, memory-service, httpx.
Тільки in-memory TTL dict (per-process).
Telemetry-теги (logger.info у caller):
vision_lock_set, vision_skip_reanalysis, vision_user_override_set,
vision_low_conf_clarifier_added, vision_reeval_forced
"""
from __future__ import annotations
import logging
import re
import time
logger = logging.getLogger(__name__)
# ── In-memory store: key = "{agent_id}:{chat_id}" ───────────────────────────
_VISION_LOCK: dict[str, dict] = {}
VISION_LOCK_TTL = 1800.0 # 30 хвилин
def _cleanup() -> None:
now = time.time()
expired = [k for k, v in _VISION_LOCK.items()
if now - float(v.get("ts", 0)) > VISION_LOCK_TTL]
for k in expired:
del _VISION_LOCK[k]
# ── Photo key: file_unique_id має пріоритет над file_id ──────────────────────
def _photo_key(file_id: str, file_unique_id: str | None = None) -> str:
"""
Telegram надсилає одне фото у кількох розмірах з різними file_id,
але спільним file_unique_id. Lock прив'язуємо до file_unique_id якщо є.
"""
return (file_unique_id or "").strip() or file_id
# ── Regex для витягу культури/діагнозу з vision відповіді ─────────────────────
_LABEL_CROP_RE = re.compile(
r"\b(кукурудза|пшениця|соняшник|ріпак|соя|ячмінь|горох|буряк|картопля|льон"
r"|бур[''ʼ]ян|ґрунт|ґрунту|шкідник"
r"|corn|wheat|sunflower|rapeseed|soybean|barley|weed|soil|pest)\b",
re.IGNORECASE | re.UNICODE,
)
_LABEL_DIAG_RE = re.compile(
r"\b(хлороз|некроз|іржа|фузаріоз|борошниста\s+роса|септоріоз|попелиц[яі]|тля"
r"|дефіцит\s+\w+|нестача\s+\w+|шкідник|хвороба|гниль)\b",
re.IGNORECASE | re.UNICODE,
)
# Низька впевненість — коли LLM сам сумнівається
_LOW_CONFIDENCE_RE = re.compile(
r"\b(можливо|схоже|не впевнений|важко сказати|без точної|не можу визначити"
r"|потребує|потрібно перевірити|ймовірно|не однозначно|декілька варіантів)\b",
re.IGNORECASE | re.UNICODE,
)
# ── User override whitelist ───────────────────────────────────────────────────
# Дозволені лейбли (ті ж культури + агрономічні поняття).
_OVERRIDE_WHITELIST = {
"кукурудза", "пшениця", "соняшник", "ріпак", "соя", "ячмінь",
"горох", "буряк", "картопля", "льон", "бур'ян", "бурʼян", "ґрунт",
"шкідник", "хлороз", "некроз", "іржа", "фузаріоз",
"corn", "wheat", "sunflower", "rapeseed", "soybean", "barley",
"weed", "soil", "pest",
}
# Заборона: "не X", "то не X" → не записуємо
_NEGATION_PREFIX_RE = re.compile(
r"^(?:це|то|ось|тут)?\s*не\s+\S",
re.IGNORECASE | re.UNICODE,
)
# Позитивне підтвердження: "це X" / "то X" / "ось X" / "тепер це X" / просто "X"
_USER_OVERRIDE_RE = re.compile(
r"^(?:тепер\s+)?(?:це|ось|то|тут|так[,\s]|маю\s+на\s+увазі)?\s*"
r"(кукурудза|пшениця|соняшник|ріпак|соя|ячмінь|горох|буряк|картопля|льон"
r"|бур[''ʼ]ян|ґрунт|шкідник"
r"|хлороз|некроз|іржа|фузаріоз|борошниста\s+роса|попелиця|тля|дефіцит\s+\w+"
r"|corn|wheat|sunflower|rapeseed|soybean|weed|soil|pest)[\s!.]*$",
re.IGNORECASE | re.UNICODE,
)
# Явний запит переоцінки
_REEVAL_RE = re.compile(
r"переоцін|перевір\s+(?:ще\s+раз|знову)|переглянь|інша\s+думка|не\s+те|"
r"помилив(?:ся)?|re[-\s]?eval",
re.IGNORECASE | re.UNICODE,
)
# ── Public API ────────────────────────────────────────────────────────────────
def extract_label_from_response(answer_text: str) -> tuple[str, str]:
"""
Витягує (label, confidence) з vision відповіді.
label: перша знайдена культура або діагноз, нижній регістр.
confidence: "high" | "low" | "unknown"
Fail-safe: будь-яка помилка → ("", "unknown").
"""
try:
t = answer_text.strip()
label = ""
crop_m = _LABEL_CROP_RE.search(t)
if crop_m:
label = crop_m.group(0).lower()
elif (diag_m := _LABEL_DIAG_RE.search(t)):
label = diag_m.group(0).lower()
confidence = "low" if _LOW_CONFIDENCE_RE.search(t) else ("high" if label else "unknown")
return label, confidence
except Exception:
return "", "unknown"
def get_vision_lock(agent_id: str, chat_id: str) -> dict:
"""
Повертає поточний vision lock для (agent_id, chat_id).
{} якщо нема або протухло.
"""
try:
_cleanup()
key = f"{agent_id}:{chat_id}"
rec = _VISION_LOCK.get(key) or {}
if not rec:
return {}
age = time.time() - float(rec.get("ts", 0))
return rec if age <= VISION_LOCK_TTL else {}
except Exception:
return {}
def set_vision_lock(
agent_id: str,
chat_id: str,
file_id: str,
label: str,
confidence: str,
file_unique_id: str | None = None,
) -> None:
"""
Зберігає vision lock після обробки фото.
photo_key = file_unique_id якщо є, інакше file_id.
Fail-safe: не кидає назовні.
"""
try:
_cleanup()
key = f"{agent_id}:{chat_id}"
existing = _VISION_LOCK.get(key) or {}
pk = _photo_key(file_id, file_unique_id)
_VISION_LOCK[key] = {
"photo_key": pk,
"file_id": file_id,
"label": label,
"confidence": confidence,
"user_label": existing.get("user_label", ""), # зберігаємо user override
"ts": time.time(),
}
except Exception:
pass
def clear_vision_lock(agent_id: str, chat_id: str) -> None:
"""
Скидає vision lock (для reeval_request).
Fail-safe.
"""
try:
key = f"{agent_id}:{chat_id}"
_VISION_LOCK.pop(key, None)
except Exception:
pass
def set_user_label(agent_id: str, chat_id: str, user_label: str) -> None:
"""
Зберігає user override label (юзер явно підтвердив що це).
Fail-safe.
"""
try:
_cleanup()
key = f"{agent_id}:{chat_id}"
rec = _VISION_LOCK.get(key) or {}
rec["user_label"] = user_label.strip().lower()
rec["ts"] = time.time()
_VISION_LOCK[key] = rec
except Exception:
pass
def detect_user_override(text: str) -> str:
"""
Перевіряє чи текст є user override ("це соняшник" тощо).
Правила (B):
- Заборонено: "не X", "то не X" → повертаємо ""
- Дозволено: тільки whitelist лейблів
- Коротке підтвердження: "це X" / "то X" / просто "X" (<=4 слова)
Повертає normalized label або "" якщо не override.
"""
try:
stripped = text.strip()
# Відхиляємо негацію ("це не соняшник")
if _NEGATION_PREFIX_RE.search(stripped):
return ""
m = _USER_OVERRIDE_RE.match(stripped)
if not m:
return ""
label = m.group(1).strip().lower()
# Нормалізуємо аpostrophes для whitelist check
label_norm = label.replace("ʼ", "'").replace("\u2019", "'")
# Перевіряємо whitelist (часткове співпадіння для "дефіцит X")
in_whitelist = any(
label_norm == w or label_norm.startswith(w)
for w in _OVERRIDE_WHITELIST
)
return label if in_whitelist else ""
except Exception:
return ""
def is_reeval_request(text: str) -> bool:
"""
Чи явний запит переоцінки ("переоцінити", "перевір ще раз" тощо).
"""
try:
return bool(_REEVAL_RE.search(text.strip()))
except Exception:
return False
def should_skip_reanalysis(
agent_id: str,
chat_id: str,
file_id: str,
user_text: str,
file_unique_id: str | None = None,
) -> bool:
"""
Rule 1: Те саме фото + без запиту переоцінки → True (skip).
Rule C: reeval_request → clear_lock → False (реаналіз).
photo_key = file_unique_id якщо є, інакше file_id.
"""
try:
if is_reeval_request(user_text):
# C: очищуємо lock — наступний аналіз буде свіжим
clear_vision_lock(agent_id, chat_id)
logger.info(
"vision_reeval_forced agent=%s chat_id=%s file_id=%s",
agent_id, chat_id, file_id,
)
return False
lock = get_vision_lock(agent_id, chat_id)
if not lock:
return False
pk = _photo_key(file_id, file_unique_id)
return lock.get("photo_key") == pk
except Exception:
return False
def build_low_confidence_clarifier(answer_text: str) -> tuple[str, bool]:
"""
Rule 2: Якщо confidence низький — додати уточнення.
Повертає (modified_text, was_added).
Fail-safe: повертає (original, False) при будь-якій помилці.
"""
try:
_, conf = extract_label_from_response(answer_text)
if conf != "low":
return answer_text, False
# Якщо LLM вже дав уточнення — не дублюємо
if "?" in answer_text[-120:]:
return answer_text, False
result = (
answer_text.rstrip()
+ "\n\nЩоб визначити точніше: можеш надіслати фото листя ближче "
"або уточнити — це нові ознаки чи давні?"
)
return result, True
except Exception:
return answer_text, False
def build_locked_reply(lock: dict, user_text: str) -> str:
"""
Повертає коротку відповідь якщо фото вже аналізувалось (same photo_key, no reeval).
lock — словник з get_vision_lock().
"""
try:
user_lbl = lock.get("user_label") or ""
label = user_lbl or lock.get("label") or ""
conf = lock.get("confidence", "unknown")
if not label:
return "Це фото вже аналізував — повтори питання конкретніше або надішли нове."
label_str = label.capitalize()
if conf == "high" or user_lbl:
src = "ти підтвердив" if user_lbl else "визначено"
return (
f"Це фото вже аналізував: {label_str} ({src}). "
"Що саме перевірити ще раз?"
)
return (
f"Для цього фото раніше визначив: схоже на {label_str} (невисока впевненість). "
"Надішли нове фото або уточни — переоцінити?"
)
except Exception:
return "Це фото вже аналізував. Що саме хочеш перевірити?"

View File

@@ -0,0 +1,13 @@
# Qwen3.5-35B-A3B Ollama Modelfile template.
# Set MODEL_SOURCE to a GGUF path or URL before using create script.
# Example:
# export MODEL_SOURCE='hf.co/your-org/Qwen3.5-35B-A3B-GGUF:Q4_K_M'
# ./scripts/node2/install_qwen3_5_35b_a3b.sh
FROM ${MODEL_SOURCE}
PARAMETER num_ctx 8192
PARAMETER temperature 0.2
PARAMETER top_p 0.9
SYSTEM """You are Sofiia's high-capacity reasoning model. Be concise, factual, and structured."""

61
opencode.json Normal file
View File

@@ -0,0 +1,61 @@
{
"providers": {
"ollama": {
"provider": "ollama",
"url": "http://localhost:11434",
"models": [
{
"id": "glm-4.7-flash:32k",
"name": "GLM-4.7 Flash (Local)",
"description": "GLM-4.7 Flash - 19GB, fast multilingual model"
},
{
"id": "qwen3:14b",
"name": "Qwen3 14B (Local)",
"description": "Qwen3 14B - balanced local model for router/default"
},
{
"id": "qwen3.5:35b-a3b",
"name": "Qwen3.5 35B A3B (Local)",
"description": "Qwen3.5 35B A3B via llama-server/Ollama bridge"
},
{
"id": "gpt-oss:latest",
"name": "GPT-OSS (Local)",
"description": "GPT-OSS latest local model"
},
{
"id": "deepseek-coder:33b",
"name": "DeepSeek Coder (Local)",
"description": "DeepSeek Coder 33B - 18GB, coding model"
},
{
"id": "deepseek-r1:70b",
"name": "DeepSeek R1 (Local)",
"description": "DeepSeek R1 70B - 42GB, reasoning model"
},
{
"id": "mistral-nemo:12b",
"name": "Mistral Nemo (Local)",
"description": "Mistral Nemo 12B - 7.1GB, general purpose"
},
{
"id": "gemma2:27b",
"name": "Gemma 2 (Local)",
"description": "Gemma 2 27B - 15GB, Google model"
}
],
"defaultModel": "qwen3:14b"
}
},
"agents": {
"sofiia": {
"provider": "xai",
"model": "grok-4-1-2025-01-25"
},
"sofiia-local": {
"provider": "ollama",
"model": "qwen3.5:35b-a3b"
}
}
}

164
scripts/init-sofiia-memory.py Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Initialize Sofiia Memory Collections
Creates collections for Sofiia agent in Qdrant
"""
import sys
import time
import requests
from typing import List, Dict
# Configuration
QDRANT_URL = "http://localhost:6333"
AGENT_ID = "sofiia"
EMBEDDING_DIMENSION = 1024 # Cohere embed-multilingual-v3.0
# Collections to create
COLLECTIONS = [
{
"name": f"{AGENT_ID}_messages",
"description": "Chat message history for Sofiia",
"vectors": {
"size": EMBEDDING_DIMENSION,
"distance": "Cosine"
}
},
{
"name": f"{AGENT_ID}_docs",
"description": "Documents and knowledge base for Sofiia",
"vectors": {
"size": EMBEDDING_DIMENSION,
"distance": "Cosine"
}
},
{
"name": f"{AGENT_ID}_memory_items",
"description": "Long-term memory items for Sofiia",
"vectors": {
"size": EMBEDDING_DIMENSION,
"distance": "Cosine"
}
},
{
"name": f"{AGENT_ID}_user_context",
"description": "User context and preferences for Sofiia",
"vectors": {
"size": EMBEDDING_DIMENSION,
"distance": "Cosine"
}
}
]
def check_qdrant_health() -> bool:
"""Check if Qdrant is running and healthy"""
try:
response = requests.get(f"{QDRANT_URL}/healthz", timeout=5)
return response.status_code == 200
except Exception as e:
print(f"❌ Qdrant not accessible: {e}")
return False
def collection_exists(collection_name: str) -> bool:
"""Check if collection already exists"""
try:
response = requests.get(f"{QDRANT_URL}/collections/{collection_name}", timeout=5)
return response.status_code == 200
except:
return False
def create_collection(collection: Dict) -> bool:
"""Create a collection in Qdrant"""
collection_name = collection["name"]
if collection_exists(collection_name):
print(f" Collection '{collection_name}' already exists")
return True
payload = {
"vectors": collection["vectors"]
}
try:
response = requests.put(
f"{QDRANT_URL}/collections/{collection_name}",
json=payload,
timeout=10
)
if response.status_code == 200:
print(f" ✅ Created collection '{collection_name}'")
return True
else:
print(f" ❌ Failed to create '{collection_name}': {response.text}")
return False
except Exception as e:
print(f" ❌ Error creating '{collection_name}': {e}")
return False
def list_all_collections() -> List[str]:
"""List all collections in Qdrant"""
try:
response = requests.get(f"{QDRANT_URL}/collections", timeout=5)
if response.status_code == 200:
data = response.json()
return [c["name"] for c in data["result"]["collections"]]
return []
except:
return []
def main():
print("🧠 Sofiia Memory Initialization")
print("=" * 50)
print()
# Check Qdrant health
print("🔍 Checking Qdrant connection...")
if not check_qdrant_health():
print("❌ Qdrant is not running or not accessible")
print(" Please start Qdrant first: docker-compose -f docker-compose.memory-node2.yml up -d qdrant-node2")
sys.exit(1)
print("✅ Qdrant is healthy")
print()
# Create collections
print(f"📦 Creating collections for '{AGENT_ID}'...")
print()
success_count = 0
for collection in COLLECTIONS:
if create_collection(collection):
success_count += 1
print()
# Summary
print("=" * 50)
print(f"📊 Summary: {success_count}/{len(COLLECTIONS)} collections ready")
print()
# List all collections
print("📋 All collections:")
all_collections = list_all_collections()
for coll in sorted(all_collections):
prefix = "" if coll.startswith(f"{AGENT_ID}_") else ""
print(f"{prefix} {coll}")
print()
print("✅ Sofiia memory initialization complete!")
print()
print("📝 Next steps:")
print(" 1. Verify collections: curl http://localhost:6333/collections")
print(" 2. Test memory service: curl http://localhost:8000/health")
print(" 3. Start using Sofiia with memory!")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
MODEL_TAG="qwen3.5:35b-a3b"
MODELS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/models"
TEMPLATE_FILE="$MODELS_DIR/Modelfile.qwen3.5-35b-a3b"
TMP_FILE="$MODELS_DIR/.Modelfile.qwen3.5-35b-a3b.rendered"
if ! command -v ollama >/dev/null 2>&1; then
echo "[error] ollama not found in PATH"
exit 1
fi
if ollama list | awk '{print $1}' | grep -qx "$MODEL_TAG"; then
echo "[ok] $MODEL_TAG is already installed"
exit 0
fi
if [[ -z "${MODEL_SOURCE:-}" ]]; then
echo "[error] MODEL_SOURCE is not set"
echo "Set MODEL_SOURCE to GGUF source, e.g.:"
echo " export MODEL_SOURCE='hf.co/your-org/Qwen3.5-35B-A3B-GGUF:Q4_K_M'"
exit 1
fi
if [[ ! -f "$TEMPLATE_FILE" ]]; then
echo "[error] Modelfile template not found: $TEMPLATE_FILE"
exit 1
fi
sed "s|\${MODEL_SOURCE}|${MODEL_SOURCE}|g" "$TEMPLATE_FILE" > "$TMP_FILE"
echo "[info] Creating $MODEL_TAG from MODEL_SOURCE=$MODEL_SOURCE"
ollama create "$MODEL_TAG" -f "$TMP_FILE"
rm -f "$TMP_FILE"
echo "[ok] Installed $MODEL_TAG"

135
scripts/start-memory-node2.sh Executable file
View File

@@ -0,0 +1,135 @@
#!/bin/bash
# Memory Stack Launch Script for NODA2
# Hybrid Mode: Local Memory with optional NODA1 access
set -e
REPO_DIR="/Users/apple/github-projects/microdao-daarion"
COMPOSE_FILE="$REPO_DIR/docker-compose.memory-node2.yml"
echo "🧠 DAARION Memory Stack - NODA2"
echo "================================"
echo ""
# Check .env file
if [ ! -f "$REPO_DIR/.env" ]; then
echo "❌ Error: .env file not found!"
echo " Creating .env file..."
touch "$REPO_DIR/.env"
fi
# Check required API keys
if ! grep -q "COHERE_API_KEY" "$REPO_DIR/.env" 2>/dev/null; then
echo "❌ Error: COHERE_API_KEY not found in .env"
echo " Please add: COHERE_API_KEY=your_key_here"
exit 1
fi
echo "✅ Environment variables configured"
echo ""
# Create data directories
echo "📁 Creating data directories..."
mkdir -p "$REPO_DIR/data"/{qdrant-node2,postgres-node2,neo4j-node2,redis-node2}
echo " Done!"
echo ""
# Pull latest images
echo "📦 Pulling Docker images..."
docker-compose -f "$COMPOSE_FILE" pull
echo ""
# Start services
echo "🚀 Starting Memory Stack..."
echo " This may take a few minutes..."
echo ""
docker-compose -f "$COMPOSE_FILE" up -d
echo ""
echo "⏳ Waiting for services to be healthy..."
sleep 10
# Health checks
echo ""
echo "📊 Service Status:"
echo "=================="
echo ""
# Qdrant
echo "Qdrant (Vector DB):"
if curl -s -o /dev/null -w " HTTP Status: %{http_code}\n" --connect-timeout 3 http://localhost:6333/healthz; then
echo " ✅ Healthy"
else
echo " ⏳ Starting..."
fi
# PostgreSQL
echo ""
echo "PostgreSQL (Relational DB):"
if docker exec dagi-postgres-node2 pg_isready -U daarion -d daarion_memory >/dev/null 2>&1; then
echo " ✅ Healthy"
else
echo " ⏳ Starting..."
fi
# Neo4j
echo ""
echo "Neo4j (Graph DB):"
if curl -s -o /dev/null -w " HTTP Status: %{http_code}\n" --connect-timeout 3 http://localhost:7474 >/dev/null 2>&1; then
echo " ✅ Healthy"
else
echo " ⏳ Starting (may take 30-40 seconds)..."
fi
# Memory Service
echo ""
echo "Memory Service:"
if curl -s -o /dev/null -w " HTTP Status: %{http_code}\n" --connect-timeout 3 http://localhost:8000/health >/dev/null 2>&1; then
echo " ✅ Healthy"
else
echo " ⏳ Starting..."
fi
# Redis
echo ""
echo "Redis (Cache):"
if docker exec dagi-redis-node2 redis-cli ping >/dev/null 2>&1; then
echo " ✅ Healthy"
else
echo " ⏳ Starting..."
fi
echo ""
echo "================================"
echo "✅ Memory Stack launched!"
echo ""
echo "📋 Endpoints:"
echo " • Qdrant UI: http://localhost:6333/dashboard"
echo " • Memory Service: http://localhost:8000"
echo " • Memory Health: http://localhost:8000/health"
echo " • Neo4j Browser: http://localhost:7474"
echo " • Adminer (DB UI): http://localhost:8080"
echo ""
echo "📝 Connection Strings:"
echo " • Qdrant: http://localhost:6333"
echo " • PostgreSQL: postgresql://daarion:daarion_secret_node2@localhost:5433/daarion_memory"
echo " • Neo4j: bolt://localhost:7687"
echo " • Redis: redis://localhost:6379"
echo ""
echo "🔧 Useful Commands:"
echo " • View logs: docker-compose -f $COMPOSE_FILE logs -f"
echo " • Stop all: docker-compose -f $COMPOSE_FILE down"
echo " • Restart service: docker-compose -f $COMPOSE_FILE restart memory-service-node2"
echo " • Check status: docker-compose -f $COMPOSE_FILE ps"
echo ""
echo "📚 Documentation:"
echo " • Setup Guide: docs/NODA2-MEMORY-SETUP.md"
echo " • API Docs: http://localhost:8000/docs (after launch)"
echo ""
echo "🎯 Next Steps:"
echo " 1. Wait 30-60 seconds for all services to start"
echo " 2. Check health: curl http://localhost:8000/health"
echo " 3. Initialize Sofiia collections (see docs)"
echo " 4. Configure OpenClaw to use memory service"
echo ""

48
setup_sofiia_node2.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# Setup Sofiia for NODA2
# This script adds API keys and configures Sofiia agent
set -e
REPO_DIR="/Users/apple/github-projects/microdao-daarion"
ENV_FILE="$REPO_DIR/.env"
echo "🔑 Setting up Sofiia for NODA2..."
# Add Grok API key
if ! grep -q "XAI_API_KEY" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# xAI Grok API (for Sofiia agent)" >> "$ENV_FILE"
echo "XAI_API_KEY=xai-VsaJjtIDhQdMlez7jRrQ93uAvqBWi0UNrdDhpUO58tnKMgjIp6P0BF6HGWrLe2QXezyvJnjCUD7C9gQ7" >> "$ENV_FILE"
echo "✅ Added XAI_API_KEY"
else
echo "⚠️ XAI_API_KEY already exists in .env"
fi
# Add Sofiia Telegram bot token
if ! grep -q "SOFIIA_TELEGRAM_BOT_TOKEN" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# Sofiia Telegram Bot (NODA2)" >> "$ENV_FILE"
echo "SOFIIA_TELEGRAM_BOT_TOKEN=8589292566:AAEmPvS6nY9e-Y-TZm04CAHWlaFnWVxajE4" >> "$ENV_FILE"
echo "✅ Added SOFIIA_TELEGRAM_BOT_TOKEN"
else
echo "⚠️ SOFIIA_TELEGRAM_BOT_TOKEN already exists in .env"
fi
# Add GLM5 API key
if ! grep -q "GLM5_API_KEY" "$ENV_FILE" 2>/dev/null; then
echo "" >> "$ENV_FILE"
echo "# GLM5 API Key (Z.AI)" >> "$ENV_FILE"
echo "GLM5_API_KEY=2f32adb611c54ccf9808062c4442c2b2.Q0BgNNlmH9O9iPGe" >> "$ENV_FILE"
echo "✅ Added GLM5_API_KEY"
else
echo "⚠️ GLM5_API_KEY already exists in .env"
fi
echo ""
echo "✅ Sofiia setup complete!"
echo ""
echo "Next steps:"
echo "1. cd $REPO_DIR"
echo "2. docker-compose -f docker-compose.node2.yml up -d"
echo "3. Test: curl http://localhost:9102/v1/agents/sofiia/infer"