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:
518
AGENTS.md
Normal file
518
AGENTS.md
Normal 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
160
SOFIIA_IN_OPENCODE.md
Normal 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
264
SOFIIA_NODA2_SETUP.md
Normal 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.**
|
||||
318
agromatrix_stepan_noda1_APPLY.md
Normal file
318
agromatrix_stepan_noda1_APPLY.md
Normal 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, **не** через Степана.
|
||||
|
||||
Перед тестами 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.
|
||||
468
agromatrix_stepan_noda1_prod.patch
Normal file
468
agromatrix_stepan_noda1_prod.patch
Normal 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)
|
||||
211
docker-compose.memory-node2.yml
Normal file
211
docker-compose.memory-node2.yml
Normal 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:
|
||||
81
docker-compose.node2-sofiia-supervisor.yml
Normal file
81
docker-compose.node2-sofiia-supervisor.yml
Normal 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
|
||||
4
gateway-bot/gateway_boot.py
Normal file
4
gateway-bot/gateway_boot.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Boot-time state for Gateway. Set by app startup; read by http_api.
|
||||
"""
|
||||
STEPAN_IMPORTS_OK = False
|
||||
33
gateway-bot/monitor_prompt.txt
Normal file
33
gateway-bot/monitor_prompt.txt
Normal 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
334
gateway-bot/vision_guard.py
Normal 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 "Це фото вже аналізував. Що саме хочеш перевірити?"
|
||||
13
models/Modelfile.qwen3.5-35b-a3b
Normal file
13
models/Modelfile.qwen3.5-35b-a3b
Normal 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
61
opencode.json
Normal 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
164
scripts/init-sofiia-memory.py
Executable 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()
|
||||
37
scripts/node2/install_qwen3_5_35b_a3b.sh
Executable file
37
scripts/node2/install_qwen3_5_35b_a3b.sh
Executable 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
135
scripts/start-memory-node2.sh
Executable 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
48
setup_sofiia_node2.sh
Executable 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"
|
||||
Reference in New Issue
Block a user