diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..eab8b168 --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/SOFIIA_IN_OPENCODE.md b/SOFIIA_IN_OPENCODE.md new file mode 100644 index 00000000..e2846f70 --- /dev/null +++ b/SOFIIA_IN_OPENCODE.md @@ -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! 🎉** diff --git a/SOFIIA_NODA2_SETUP.md b/SOFIIA_NODA2_SETUP.md new file mode 100644 index 00000000..236dc487 --- /dev/null +++ b/SOFIIA_NODA2_SETUP.md @@ -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.** diff --git a/agromatrix_stepan_noda1_APPLY.md b/agromatrix_stepan_noda1_APPLY.md new file mode 100644 index 00000000..73f06700 --- /dev/null +++ b/agromatrix_stepan_noda1_APPLY.md @@ -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, **не** через Степана. + +Перед тестами 2–4 переконайтеся, що в 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. diff --git a/agromatrix_stepan_noda1_prod.patch b/agromatrix_stepan_noda1_prod.patch new file mode 100644 index 00000000..9b165c5f --- /dev/null +++ b/agromatrix_stepan_noda1_prod.patch @@ -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) diff --git a/docker-compose.memory-node2.yml b/docker-compose.memory-node2.yml new file mode 100644 index 00000000..11fd3652 --- /dev/null +++ b/docker-compose.memory-node2.yml @@ -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 '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. diff --git a/gateway-bot/vision_guard.py b/gateway-bot/vision_guard.py new file mode 100644 index 00000000..269b134d --- /dev/null +++ b/gateway-bot/vision_guard.py @@ -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 "Це фото вже аналізував. Що саме хочеш перевірити?" diff --git a/models/Modelfile.qwen3.5-35b-a3b b/models/Modelfile.qwen3.5-35b-a3b new file mode 100644 index 00000000..5b58ea7a --- /dev/null +++ b/models/Modelfile.qwen3.5-35b-a3b @@ -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.""" diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..b82cfd19 --- /dev/null +++ b/opencode.json @@ -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" + } + } +} diff --git a/scripts/init-sofiia-memory.py b/scripts/init-sofiia-memory.py new file mode 100755 index 00000000..9a3bd191 --- /dev/null +++ b/scripts/init-sofiia-memory.py @@ -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() diff --git a/scripts/node2/install_qwen3_5_35b_a3b.sh b/scripts/node2/install_qwen3_5_35b_a3b.sh new file mode 100755 index 00000000..8617d214 --- /dev/null +++ b/scripts/node2/install_qwen3_5_35b_a3b.sh @@ -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" diff --git a/scripts/start-memory-node2.sh b/scripts/start-memory-node2.sh new file mode 100755 index 00000000..800c26bf --- /dev/null +++ b/scripts/start-memory-node2.sh @@ -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 "" diff --git a/setup_sofiia_node2.sh b/setup_sofiia_node2.sh new file mode 100755 index 00000000..309f46d4 --- /dev/null +++ b/setup_sofiia_node2.sh @@ -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"