feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,542 @@
# TASK: Agent Hub UI — Agent Monitoring & Management Dashboard
**Priority:** High (Parallel to Phase 3)
**Estimated Time:** 3-4 weeks
**Dependencies:** Phase 2 complete
---
## Goal
Створити централізований UI для моніторингу та управління агентами в DAARION:
- **Agent Gallery** — список всіх агентів з їх статусами
- **Agent Cabinet** — детальна інформація про агента
- **Real-time monitoring** — активність, метрики, логи
- **Agent Configuration** — налаштування, інструкції, tools
- **Conversational Interface** — прямий чат з агентом
---
## Architecture Overview
```
Agent Hub UI (React)
├─ Agent Gallery (список агентів)
├─ Agent Cabinet (детальний dashboard)
├─ Agent Chat (direct conversation)
├─ Agent Metrics (real-time stats)
└─ Agent Configuration (edit settings)
Backend:
├─ agents-service (агенти + blueprints)
├─ agent-runtime (execution stats)
├─ messaging-service (direct chat)
└─ NATS (real-time events)
```
---
## Deliverables
### 1. Frontend Components (src/features/agent-hub/)
**Structure:**
```
src/features/agent-hub/
components/
AgentGallery.tsx # Grid of agent cards
AgentCard.tsx # Single agent preview
AgentCabinet.tsx # Full agent dashboard
AgentMetrics.tsx # Real-time metrics panel
AgentLogs.tsx # Activity log stream
AgentConfiguration.tsx # Edit agent settings
AgentChat.tsx # Direct chat interface
AgentToolsPanel.tsx # List of agent tools
hooks/
useAgents.ts # Fetch agents list
useAgentDetails.ts # Fetch single agent
useAgentMetrics.ts # Real-time metrics
useAgentLogs.ts # Activity stream
useDirectChat.ts # Direct chat with agent
types/
agent.ts # TypeScript types
AgentHubPage.tsx # Main hub page
```
---
### 2. Backend: agents-service (NEW)
**Port:** 7010
**Purpose:** Agent blueprints, configuration, metadata
**Structure:**
```
services/agents-service/
main.py
models.py
blueprints.py
config.yaml
requirements.txt
Dockerfile
README.md
```
**API:**
#### GET /api/agents
List all agents:
```json
{
"agents": [
{
"id": "agent:sofia",
"name": "Sofia-Prime",
"kind": "assistant",
"status": "active",
"avatar": "https://...",
"description": "Project manager & task organizer",
"capabilities": ["task_mgmt", "summarization"],
"microdao_id": "microdao:daarion",
"created_at": "2025-01-01T00:00:00Z",
"last_active": "2025-11-24T12:00:00Z"
}
]
}
```
#### GET /api/agents/{agent_id}
Get agent details:
```json
{
"id": "agent:sofia",
"name": "Sofia-Prime",
"kind": "assistant",
"status": "active",
"avatar": "https://...",
"description": "...",
"instructions": "You are Sofia, a helpful assistant...",
"model": "gpt-4.1-mini",
"tools": ["projects.list", "task.create", "followup.create"],
"capabilities": {...},
"metrics": {
"total_messages": 1234,
"total_invocations": 567,
"avg_response_time_ms": 2345,
"success_rate": 0.98
},
"config": {
"temperature": 0.7,
"max_tokens": 1000,
"quiet_hours": "22:00-08:00"
}
}
```
#### PUT /api/agents/{agent_id}
Update agent configuration:
```json
{
"instructions": "Updated instructions...",
"model": "gpt-4",
"tools": ["projects.list", "task.create"],
"config": {
"temperature": 0.8,
"max_tokens": 2000
}
}
```
#### GET /api/agents/{agent_id}/metrics/realtime
Real-time metrics (WebSocket or SSE):
```json
{
"agent_id": "agent:sofia",
"timestamp": "2025-11-24T12:34:56Z",
"active_conversations": 3,
"messages_last_hour": 15,
"avg_response_time_ms": 2100,
"current_load": "normal"
}
```
#### GET /api/agents/{agent_id}/logs?limit=50
Activity logs:
```json
{
"logs": [
{
"id": "log-123",
"timestamp": "2025-11-24T12:34:56Z",
"event_type": "message.sent",
"channel_id": "channel-uuid",
"content_preview": "Створила задачу 'Phase 3 testing'",
"metadata": {
"latency_ms": 2345,
"tokens_used": 567
}
}
]
}
```
---
### 3. UI Components Detail
#### AgentGallery.tsx
- Grid layout (3-4 columns)
- Agent cards with:
- Avatar
- Name + status badge
- Short description
- Last active time
- Quick stats (messages, response time)
- Filter by:
- Status (active, paused, error)
- Kind (assistant, coordinator, specialist)
- MicroDAO
- Search by name
**Wireframe:**
```
┌─────────────────────────────────────────┐
│ Agent Hub [+ New] │
├─────────────────────────────────────────┤
│ [Filter: All] [Search: ...] │
├─────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ 👤 Sofia │ │ 👤 Alex │ │ 👤 Eva │
│ │ ● Active │ │ ● Active │ │ ⏸ Paused │
│ │ PM & Org │ │ Tech │ │ Research │
│ │ 234 msgs │ │ 156 msgs │ │ 89 msgs │
│ └──────────┘ └──────────┘ └──────────┘
```
#### AgentCabinet.tsx
- Header:
- Avatar, name, status
- Quick actions (pause, edit, chat)
- Tabs:
- **Overview**: Key stats + recent activity
- **Metrics**: Real-time charts
- **Logs**: Activity stream
- **Configuration**: Edit settings
- **Chat**: Direct conversation
**Wireframe:**
```
┌─────────────────────────────────────────┐
│ 👤 Sofia-Prime [Edit] [Chat]│
│ ● Active | Last seen: 2m ago │
├─────────────────────────────────────────┤
│ [Overview] [Metrics] [Logs] [Config] │
├─────────────────────────────────────────┤
│ 📊 Statistics (24h) │
│ Messages: 45 | Invocations: 23 │
│ Avg Response: 2.3s | Success: 98% │
│ │
│ 📝 Recent Activity │
│ • 12:34 - Replied in #general │
│ • 12:30 - Created task "Phase 3 test" │
│ • 12:25 - Summarized meeting notes │
└─────────────────────────────────────────┘
```
#### AgentChat.tsx
- Direct 1-on-1 chat with agent
- Similar to Messenger UI
- Shows agent thinking/tools usage
- Context: "Direct conversation (not in channel)"
---
### 4. Real-time Updates
**WebSocket endpoint:** `ws://localhost:7010/ws/agents/{agent_id}`
**Events:**
```json
{
"type": "agent.message.sent",
"agent_id": "agent:sofia",
"channel_id": "channel-uuid",
"timestamp": "2025-11-24T12:34:56Z",
"content_preview": "Created task..."
}
{
"type": "agent.metrics.update",
"agent_id": "agent:sofia",
"metrics": {
"active_conversations": 3,
"messages_last_hour": 15
}
}
{
"type": "agent.status.changed",
"agent_id": "agent:sofia",
"old_status": "active",
"new_status": "paused"
}
```
---
### 5. Integration Points
**With Phase 2:**
- Uses existing `messaging-service` for direct chat
- Displays logs from `agent-runtime` invocations
- Shows agent status from `agent-filter`
**With Phase 3:**
- Shows LLM usage (tokens, cost) from `llm-proxy`
- Displays memory stats from `memory-orchestrator`
- Lists available tools from `toolcore`
**With NATS:**
- Subscribes to `agent.*` events for real-time updates
- Publishes `agent.config.updated` on changes
---
### 6. Mock Data (Development)
**agents.mock.ts:**
```typescript
export const mockAgents: Agent[] = [
{
id: "agent:sofia",
name: "Sofia-Prime",
kind: "assistant",
status: "active",
avatar: "/avatars/sofia.png",
description: "Project manager & task organizer",
capabilities: ["task_mgmt", "summarization", "planning"],
microdao_id: "microdao:daarion",
created_at: "2025-01-01T00:00:00Z",
last_active: "2025-11-24T12:00:00Z",
metrics: {
total_messages: 1234,
total_invocations: 567,
avg_response_time_ms: 2345,
success_rate: 0.98
}
},
{
id: "agent:alex",
name: "Alex-Tech",
kind: "specialist",
status: "active",
avatar: "/avatars/alex.png",
description: "Technical specialist for development tasks",
capabilities: ["code_review", "debugging", "documentation"],
microdao_id: "microdao:daarion",
created_at: "2025-01-15T00:00:00Z",
last_active: "2025-11-24T11:45:00Z",
metrics: {
total_messages: 856,
total_invocations: 423,
avg_response_time_ms: 3100,
success_rate: 0.96
}
}
];
```
---
### 7. Tech Stack
**Frontend:**
- React 18 + TypeScript
- Tailwind CSS
- React Query (TanStack Query)
- React Router
- WebSocket (for real-time)
**Backend:**
- Python 3.11 + FastAPI
- PostgreSQL (agents DB)
- NATS (events)
- Docker
---
### 8. Database Schema
**agents table:**
```sql
CREATE TABLE agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL, -- assistant, coordinator, specialist
status TEXT NOT NULL DEFAULT 'active', -- active, paused, error
avatar TEXT,
description TEXT,
instructions TEXT NOT NULL,
model TEXT NOT NULL,
tools JSONB DEFAULT '[]',
capabilities JSONB DEFAULT '{}',
config JSONB DEFAULT '{}',
microdao_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_active TIMESTAMPTZ
);
CREATE INDEX idx_agents_microdao ON agents(microdao_id);
CREATE INDEX idx_agents_status ON agents(status);
```
**agent_metrics table:**
```sql
CREATE TABLE agent_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id TEXT NOT NULL REFERENCES agents(id),
timestamp TIMESTAMPTZ DEFAULT NOW(),
event_type TEXT NOT NULL, -- message.sent, invocation, error
channel_id TEXT,
latency_ms FLOAT,
tokens_used INT,
success BOOLEAN,
metadata JSONB
);
CREATE INDEX idx_metrics_agent_time ON agent_metrics(agent_id, timestamp DESC);
```
---
### 9. Routes
**Frontend routes:**
```
/agents → AgentGallery (list)
/agents/:agentId → AgentCabinet (dashboard)
/agents/:agentId/chat → AgentChat (direct conversation)
/agents/:agentId/config → AgentConfiguration (edit)
```
---
### 10. Acceptance Criteria
**Agent Gallery:**
- Shows all agents with status badges
- Filter by status, kind, microDAO
- Search by name
- Responsive grid layout
**Agent Cabinet:**
- Displays all agent details
- Shows real-time metrics
- Activity log stream
- Configuration editor
**Agent Chat:**
- Direct 1-on-1 conversation
- Shows agent thinking/tools
- Message history
**Real-time Updates:**
- Metrics auto-refresh (WebSocket)
- Activity log auto-updates
- Status changes reflected immediately
**Integration:**
- Works with Phase 2 (messaging, runtime)
- Works with Phase 3 (LLM, memory, tools)
- Uses NATS for events
---
### 11. Implementation Steps
#### Week 1: Backend Foundation
1. Create `agents-service` (FastAPI)
2. Database schema + migrations
3. Basic CRUD endpoints
4. Mock agent blueprints
#### Week 2: Frontend Components
1. AgentGallery + AgentCard
2. AgentCabinet (layout + tabs)
3. AgentMetrics panel
4. AgentLogs stream
#### Week 3: Real-time & Integration
1. WebSocket for real-time updates
2. Direct chat integration
3. Configuration editor
4. NATS events
#### Week 4: Polish & Testing
1. UI polish (animations, UX)
2. Error handling
3. E2E testing
4. Documentation
---
### 12. Future Enhancements (Phase 3.5+)
🔜 **Agent creation wizard**
- GUI for creating new agents
- Template selection
- Tool assignment
🔜 **Agent analytics**
- Charts (messages over time)
- Cost tracking
- Performance insights
🔜 **Agent marketplace**
- Browse community agents
- Install/deploy agents
- Agent templates
🔜 **Multi-agent orchestration**
- Agent chains
- Workflow builder
- Agent collaboration
---
## Quick Start (After Implementation)
```bash
# Start agents-service
docker-compose -f docker-compose.agents.yml up -d
# Frontend dev
cd node-network-app
npm run dev
# Open
open http://localhost:8899/agents
```
---
## Documentation
**After implementation, create:**
- `/docs/AGENT_HUB_SPEC.md` — Full specification
- `/docs/AGENT_SERVICE_API.md` — API documentation
- `/services/agents-service/README.md` — Service setup
---
**Status:** 📋 Spec Ready
**Version:** 1.0.0
**Last Updated:** 2025-11-24
**READY TO BUILD!** 🚀

View File

@@ -0,0 +1,864 @@
# TASK: PHASE 2 — AGENT INTEGRATION (agent_filter + Router + agent-runtime)
**Goal:**
Активувати повний ланцюг агентних відповідей у Messenger:
User → messaging-service → matrix-gateway → Matrix → NATS → agent_filter → DAGI Router → agent-runtime → LLM → messaging-service → Matrix → Frontend.
**Deliverables:**
- `services/agent-filter/` (rules + NATS + internal API)
- router extension (messaging.inbound → router.invoke.agent)
- `services/agent-runtime/` (LLM + memory + posting to channel)
- docker-compose integration
- documentation updates
---
## 1) SERVICE: agent-filter
**Create:** `services/agent-filter/`
**Files:**
- `main.py`
- `models.py`
- `rules.py`
- `config.yaml`
- `Dockerfile`
- `requirements.txt`
- `README.md`
### Specs:
#### models.py:
```python
from pydantic import BaseModel
from typing import Optional, Literal
from datetime import datetime
class MessageCreatedEvent(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
sender_id: str
sender_type: Literal["human", "agent"]
microdao_id: str
created_at: datetime
class FilterDecision(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
microdao_id: str
decision: Literal["allow", "deny", "modify"]
target_agent_id: Optional[str] = None
rewrite_prompt: Optional[str] = None
class ChannelContext(BaseModel):
microdao_id: str
visibility: Literal["public", "private", "microdao"]
allowed_agents: list[str] = []
disabled_agents: list[str] = []
class FilterContext(BaseModel):
channel: ChannelContext
sender_is_owner: bool = False
sender_is_admin: bool = False
sender_is_member: bool = True
local_time: Optional[datetime] = None
```
#### rules.py:
```python
from models import MessageCreatedEvent, FilterContext, FilterDecision
from datetime import datetime, time
import yaml
class FilterRules:
def __init__(self, config_path: str = "config.yaml"):
with open(config_path, 'r') as f:
self.config = yaml.safe_load(f)
self.quiet_hours_start = datetime.strptime(
self.config['rules']['quiet_hours']['start'],
"%H:%M"
).time()
self.quiet_hours_end = datetime.strptime(
self.config['rules']['quiet_hours']['end'],
"%H:%M"
).time()
self.default_agents = self.config['rules'].get('default_agents', {})
def is_quiet_hours(self, dt: datetime) -> bool:
current_time = dt.time()
if self.quiet_hours_start > self.quiet_hours_end:
# Overnight range (e.g., 23:00 - 07:00)
return current_time >= self.quiet_hours_start or current_time <= self.quiet_hours_end
else:
return self.quiet_hours_start <= current_time <= self.quiet_hours_end
def decide(self, event: MessageCreatedEvent, ctx: FilterContext) -> FilterDecision:
"""
Baseline rules v1:
- Block agent→agent loops
- Map channel → allowed_agents
- Apply quiet_hours
- Return FilterDecision with decision + target_agent_id
"""
base_decision = FilterDecision(
channel_id=event.channel_id,
message_id=event.message_id,
matrix_event_id=event.matrix_event_id,
microdao_id=event.microdao_id,
decision="deny"
)
# Rule 1: Block agent→agent loops
if event.sender_type == "agent":
return base_decision
# Rule 2: Check if agent is disabled
if ctx.channel.disabled_agents:
# For now, deny if any disabled agents exist
return base_decision
# Rule 3: Find target agent
target_agent_id = None
if ctx.channel.allowed_agents:
target_agent_id = ctx.channel.allowed_agents[0]
elif event.microdao_id in self.default_agents:
target_agent_id = self.default_agents[event.microdao_id]
if not target_agent_id:
return base_decision
# Rule 4: Check quiet hours
if ctx.local_time and self.is_quiet_hours(ctx.local_time):
return FilterDecision(
channel_id=event.channel_id,
message_id=event.message_id,
matrix_event_id=event.matrix_event_id,
microdao_id=event.microdao_id,
decision="modify",
target_agent_id=target_agent_id,
rewrite_prompt="Відповідай стисло і тільки якщо запит важливий. Не ініціюй розмову сам."
)
# Rule 5: Allow
return FilterDecision(
channel_id=event.channel_id,
message_id=event.message_id,
matrix_event_id=event.matrix_event_id,
microdao_id=event.microdao_id,
decision="allow",
target_agent_id=target_agent_id
)
```
#### main.py:
```python
from fastapi import FastAPI, HTTPException
from models import MessageCreatedEvent, FilterDecision, ChannelContext, FilterContext
from rules import FilterRules
import httpx
import asyncio
import json
from datetime import datetime, timezone
import os
app = FastAPI(title="DAARION Agent Filter", version="1.0.0")
# Configuration
MESSAGING_SERVICE_URL = os.getenv("MESSAGING_SERVICE_URL", "http://messaging-service:7004")
NATS_URL = os.getenv("NATS_URL", "nats://nats:4222")
# Rules engine
rules_engine = FilterRules("config.yaml")
# NATS setup (mocked for now, replace with actual NATS client)
# import nats
# nc = None
@app.on_event("startup")
async def startup_event():
print("Agent Filter starting up...")
# nc = await nats.connect(NATS_URL)
# await subscribe_to_messaging_events()
asyncio.create_task(mock_nats_listener())
async def mock_nats_listener():
"""Mock NATS listener for testing"""
print("Mock NATS listener started (replace with actual NATS subscription)")
# In production:
# sub = await nc.subscribe("messaging.message.created")
# async for msg in sub.messages:
# await handle_message_created(json.loads(msg.data.decode()))
async def handle_message_created(event_data: dict):
"""Process incoming message.created events"""
try:
event = MessageCreatedEvent(**event_data)
# Fetch channel context
ctx = await fetch_channel_context(event.channel_id)
# Apply rules
decision = rules_engine.decide(event, ctx)
# Publish decision to NATS
await publish_decision(decision)
print(f"Decision: {decision.decision} for channel {event.channel_id}")
except Exception as e:
print(f"Error processing message: {e}")
async def fetch_channel_context(channel_id: str) -> FilterContext:
"""Fetch channel context from messaging-service"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{MESSAGING_SERVICE_URL}/internal/messaging/channels/{channel_id}/context"
)
response.raise_for_status()
data = response.json()
channel_ctx = ChannelContext(**data)
return FilterContext(
channel=channel_ctx,
local_time=datetime.now(timezone.utc)
)
except httpx.HTTPStatusError as e:
print(f"HTTP error fetching context: {e}")
# Return default context
return FilterContext(
channel=ChannelContext(
microdao_id="microdao:daarion",
visibility="microdao"
),
local_time=datetime.now(timezone.utc)
)
except Exception as e:
print(f"Error fetching context: {e}")
return FilterContext(
channel=ChannelContext(
microdao_id="microdao:daarion",
visibility="microdao"
),
local_time=datetime.now(timezone.utc)
)
async def publish_decision(decision: FilterDecision):
"""Publish decision to NATS"""
# In production:
# await nc.publish("agent.filter.decision", decision.json().encode())
print(f"Publishing decision: {decision.json()}")
@app.get("/health")
async def health():
return {"status": "ok", "service": "agent-filter"}
@app.post("/internal/agent-filter/test", response_model=FilterDecision)
async def test_filter(event: MessageCreatedEvent):
"""Test endpoint for manual filtering"""
ctx = await fetch_channel_context(event.channel_id)
decision = rules_engine.decide(event, ctx)
return decision
```
#### config.yaml:
```yaml
nats:
servers: ["nats://nats:4222"]
messaging_subject: "messaging.message.created"
decision_subject: "agent.filter.decision"
rules:
quiet_hours:
start: "23:00"
end: "07:00"
default_agents:
"microdao:daarion": "agent:sofia"
"microdao:7": "agent:sofia"
```
#### requirements.txt:
```txt
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
httpx==0.25.1
python-nats==2.6.0
PyYAML==6.0.1
```
#### Dockerfile:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7005"]
```
#### README.md:
```markdown
# Agent Filter Service
Security & routing layer for DAARION agents in Messenger.
## Purpose
- Subscribe to NATS `messaging.message.created`
- Apply filtering rules (permissions, content, timing)
- Decide which agent should reply
- Publish to `agent.filter.decision`
## Rules
1. Block agent→agent loops
2. Map channels to allowed agents
3. Apply quiet hours (23:0007:00)
4. Check disabled agents
## Running Locally
```bash
pip install -r requirements.txt
uvicorn main:app --reload --port 7005
```
## Testing
```bash
curl -X POST http://localhost:7005/internal/agent-filter/test \
-H "Content-Type: application/json" \
-d '{
"channel_id": "test-channel",
"matrix_event_id": "$event123",
"sender_id": "user:1",
"sender_type": "human",
"microdao_id": "microdao:daarion",
"created_at": "2025-11-24T10:00:00Z"
}'
```
## NATS Events
- **Subscribes to:** `messaging.message.created`
- **Publishes to:** `agent.filter.decision`
```
---
## 2) DAGI ROUTER EXTENSION
**Extend existing router service:**
### Add subscription:
```python
# In router service (e.g., services/router/main.py)
from pydantic import BaseModel
from typing import Literal
import json
class FilterDecision(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
microdao_id: str
decision: Literal["allow", "deny", "modify"]
target_agent_id: Optional[str] = None
rewrite_prompt: Optional[str] = None
class AgentInvocation(BaseModel):
agent_id: str
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
payload: dict
async def handle_filter_decision(decision_data: dict):
"""Process agent.filter.decision events"""
decision = FilterDecision(**decision_data)
# Only process 'allow' decisions
if decision.decision != "allow":
print(f"Ignoring non-allow decision: {decision.decision}")
return
if not decision.target_agent_id:
print(f"No target agent specified, ignoring")
return
# Create AgentInvocation
invocation = AgentInvocation(
agent_id=decision.target_agent_id,
entrypoint="channel_message",
payload={
"channel_id": decision.channel_id,
"message_id": decision.message_id,
"matrix_event_id": decision.matrix_event_id,
"microdao_id": decision.microdao_id,
"rewrite_prompt": decision.rewrite_prompt
}
)
# Publish to NATS
await publish_agent_invocation(invocation)
print(f"Routed to {invocation.agent_id}")
async def publish_agent_invocation(invocation: AgentInvocation):
"""Publish to router.invoke.agent"""
# await nc.publish("router.invoke.agent", invocation.json().encode())
print(f"Publishing invocation: {invocation.json()}")
# Add to router startup
async def subscribe_to_filter_decisions():
"""Subscribe to agent.filter.decision"""
# sub = await nc.subscribe("agent.filter.decision")
# async for msg in sub.messages:
# await handle_filter_decision(json.loads(msg.data.decode()))
pass
```
### Add test endpoint:
```python
@app.post("/internal/router/test-messaging", response_model=AgentInvocation)
async def test_messaging_route(decision: FilterDecision):
"""Test endpoint for routing logic"""
if decision.decision != "allow" or not decision.target_agent_id:
raise HTTPException(status_code=400, detail="Decision not routable")
invocation = AgentInvocation(
agent_id=decision.target_agent_id,
entrypoint="channel_message",
payload={
"channel_id": decision.channel_id,
"message_id": decision.message_id,
"matrix_event_id": decision.matrix_event_id,
"microdao_id": decision.microdao_id,
"rewrite_prompt": decision.rewrite_prompt
}
)
return invocation
```
### Update config:
Create/update `router_config.yaml`:
```yaml
messaging_inbound:
enabled: true
source_subject: "agent.filter.decision"
target_subject: "router.invoke.agent"
```
---
## 3) SERVICE: agent-runtime
**Create:** `services/agent-runtime/`
**Files:**
- `main.py`
- `models.py`
- `llm_client.py`
- `messaging_client.py`
- `memory_client.py`
- `config.yaml`
- `Dockerfile`
- `requirements.txt`
- `README.md`
### Specs:
#### models.py:
```python
from pydantic import BaseModel
from typing import Literal, Optional
from datetime import datetime
class AgentInvocation(BaseModel):
agent_id: str
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
payload: dict
class AgentBlueprint(BaseModel):
id: str
name: str
model: str
instructions: str
capabilities: dict = {}
class ChannelMessage(BaseModel):
sender_id: str
sender_type: Literal["human", "agent"]
content: str
created_at: datetime
```
#### llm_client.py:
```python
import httpx
import os
LLM_PROXY_URL = os.getenv("LLM_PROXY_URL", "http://llm-proxy:7007")
async def generate_response(model: str, messages: list[dict]) -> str:
"""Call LLM Proxy to generate response"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{LLM_PROXY_URL}/internal/llm/proxy",
json={
"model": model,
"messages": messages
}
)
response.raise_for_status()
data = response.json()
return data.get("content", "")
except httpx.HTTPStatusError as e:
print(f"LLM Proxy error: {e}")
return "Вибачте, не можу відповісти зараз. (LLM error)"
except Exception as e:
print(f"Error calling LLM: {e}")
return "Вибачте, сталася помилка. (Connection error)"
```
#### messaging_client.py:
```python
import httpx
import os
from models import ChannelMessage
MESSAGING_SERVICE_URL = os.getenv("MESSAGING_SERVICE_URL", "http://messaging-service:7004")
async def get_channel_messages(channel_id: str, limit: int = 50) -> list[ChannelMessage]:
"""Fetch recent messages from channel"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{MESSAGING_SERVICE_URL}/internal/messaging/channels/{channel_id}/messages",
params={"limit": limit}
)
response.raise_for_status()
data = response.json()
return [ChannelMessage(**msg) for msg in data]
except Exception as e:
print(f"Error fetching messages: {e}")
return []
async def post_message(agent_id: str, channel_id: str, text: str) -> bool:
"""Post agent reply to channel"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{MESSAGING_SERVICE_URL}/internal/agents/{agent_id}/post-to-channel",
json={
"channel_id": channel_id,
"text": text
}
)
response.raise_for_status()
return True
except Exception as e:
print(f"Error posting message: {e}")
return False
```
#### memory_client.py:
```python
import httpx
import os
AGENT_MEMORY_URL = os.getenv("AGENT_MEMORY_URL", "http://agent-memory:7008")
async def query_memory(agent_id: str, microdao_id: str, query: str) -> list[dict]:
"""Query agent memory for relevant context"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{AGENT_MEMORY_URL}/internal/agent-memory/query",
json={
"agent_id": agent_id,
"microdao_id": microdao_id,
"query": query
}
)
response.raise_for_status()
data = response.json()
return data.get("results", [])
except Exception as e:
print(f"Error querying memory: {e}")
return []
```
#### main.py:
```python
from fastapi import FastAPI
from models import AgentInvocation, AgentBlueprint, ChannelMessage
from llm_client import generate_response
from messaging_client import get_channel_messages, post_message
from memory_client import query_memory
import asyncio
import json
app = FastAPI(title="DAARION Agent Runtime", version="1.0.0")
@app.on_event("startup")
async def startup_event():
print("Agent Runtime starting up...")
asyncio.create_task(mock_nats_listener())
async def mock_nats_listener():
"""Mock NATS listener (replace with actual subscription)"""
print("Mock NATS listener started")
# In production:
# sub = await nc.subscribe("router.invoke.agent")
# async for msg in sub.messages:
# await handle_invocation(json.loads(msg.data.decode()))
async def handle_invocation(invocation_data: dict):
"""Process agent invocation"""
try:
invocation = AgentInvocation(**invocation_data)
if invocation.entrypoint != "channel_message":
print(f"Ignoring non-channel_message invocation: {invocation.entrypoint}")
return
# Extract payload
channel_id = invocation.payload.get("channel_id")
microdao_id = invocation.payload.get("microdao_id")
rewrite_prompt = invocation.payload.get("rewrite_prompt")
# 1. Load agent blueprint (mock for now)
blueprint = await load_agent_blueprint(invocation.agent_id)
# 2. Load channel history
messages = await get_channel_messages(channel_id, limit=50)
# 3. Get last human message
last_human_msg = next(
(msg for msg in reversed(messages) if msg.sender_type == "human"),
None
)
if not last_human_msg:
print("No human message found, skipping")
return
# 4. Query memory
memory_results = await query_memory(
invocation.agent_id,
microdao_id,
last_human_msg.content
)
# 5. Build prompt
system_prompt = blueprint.instructions
if rewrite_prompt:
system_prompt += f"\n\nAdditional instructions: {rewrite_prompt}"
llm_messages = [
{"role": "system", "content": system_prompt}
]
# Add recent context
for msg in messages[-10:]:
role = "assistant" if msg.sender_type == "agent" else "user"
llm_messages.append({
"role": role,
"content": msg.content
})
# Add memory context
if memory_results:
memory_context = "\n\n".join([r.get("text", "") for r in memory_results[:3]])
llm_messages.insert(1, {
"role": "system",
"content": f"Relevant knowledge:\n{memory_context}"
})
# 6. Generate response
response_text = await generate_response(blueprint.model, llm_messages)
# 7. Post to channel
success = await post_message(invocation.agent_id, channel_id, response_text)
if success:
print(f"Agent {invocation.agent_id} replied successfully")
else:
print(f"Failed to post agent reply")
except Exception as e:
print(f"Error handling invocation: {e}")
async def load_agent_blueprint(agent_id: str) -> AgentBlueprint:
"""Load agent blueprint (mock for now)"""
# In production: GET /internal/agents/{agent_id}/blueprint
return AgentBlueprint(
id=agent_id,
name="Sofia-Prime",
model="gpt-4",
instructions="Ти Sofia, помічниця команди DAARION. Допомагай планувати, організовувати та підсумовувати роботу."
)
@app.get("/health")
async def health():
return {"status": "ok", "service": "agent-runtime"}
@app.post("/internal/agent-runtime/test-channel")
async def test_channel(invocation: AgentInvocation):
"""Test endpoint for manual invocation"""
await handle_invocation(invocation.dict())
return {"status": "processed"}
```
#### config.yaml:
```yaml
nats:
servers: ["nats://nats:4222"]
invocation_subject: "router.invoke.agent"
services:
messaging: "http://messaging-service:7004"
agent_memory: "http://agent-memory:7008"
llm_proxy: "http://llm-proxy:7007"
```
#### requirements.txt:
```txt
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
httpx==0.25.1
python-nats==2.6.0
PyYAML==6.0.1
```
#### Dockerfile:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7006"]
```
---
## 4) DOCKER + COMPOSE
**Create:** `docker-compose.agents.yml`
```yaml
version: '3.8'
services:
agent-filter:
build:
context: ./services/agent-filter
dockerfile: Dockerfile
restart: always
environment:
MESSAGING_SERVICE_URL: http://messaging-service:7004
NATS_URL: nats://nats:4222
ports:
- "7005:7005"
depends_on:
- nats
- messaging-service
networks:
- daarion
agent-runtime:
build:
context: ./services/agent-runtime
dockerfile: Dockerfile
restart: always
environment:
MESSAGING_SERVICE_URL: http://messaging-service:7004
AGENT_MEMORY_URL: http://agent-memory:7008
LLM_PROXY_URL: http://llm-proxy:7007
NATS_URL: nats://nats:4222
ports:
- "7006:7006"
depends_on:
- nats
- messaging-service
networks:
- daarion
networks:
daarion:
external: true
```
**Update existing `docker-compose.messenger.yml` to include agents:**
```yaml
# Add at the end
agent-filter:
extends:
file: docker-compose.agents.yml
service: agent-filter
agent-runtime:
extends:
file: docker-compose.agents.yml
service: agent-runtime
```
---
## 5) ACCEPTANCE CRITERIA
**Phase 2 Complete When:**
- ✅ Human writes message in Messenger
- ✅ messaging-service → matrix-gateway → Matrix works
- ✅ Matrix webhook triggers messaging.message.created
- ✅ agent_filter receives → outputs agent.filter.decision
- ✅ Router receives → emits router.invoke.agent
- ✅ agent-runtime receives → generates LLM answer
- ✅ agent-runtime posts reply → messaging-service → Matrix → Messenger UI
- ✅ Reply visible in Element + Messenger
- ✅ E2E latency < 5 seconds
---
## END OF TASK
**Implementation Order:**
1. agent-filter (1 week)
2. Router extension (3 days)
3. agent-runtime (2 weeks)
4. Docker integration (2 days)
5. Testing (3 days)
**Total Estimated Time:** 4 weeks

View File

@@ -0,0 +1,503 @@
# TASK: PHASE 3 — LLM PROXY + MEMORY ORCHESTRATOR + TOOLCORE
**Goal:**
Зробити агентів DAARION по-справжньому розумними та інструментальними:
- єдиний LLM Proxy з підтримкою кількох провайдерів і локальних моделей;
- Memory Orchestrator, який дає єдиний API для памʼяті агентів;
- Toolcore — реєстр інструментів і безпечне виконання tools для агентів.
**PHASE 3 = інфраструктура мислення й дії для Agent Runtime.**
**Existing (assume from Phase 12):**
- messaging-service, matrix-gateway, Messenger UI.
- agent-filter, DAGI Router (router.invoke.agent).
- agent-runtime (використовує /internal/llm/proxy та /internal/agent-memory/*, але поки можуть бути stubs).
- базова БД (users, microdaos, agents, channels, messages, etc.).
- NATS JetStream, PostgreSQL, docker-compose.
**Deliverables:**
1. services/llm-proxy/ — LLM Router + Providers + Logging.
2. services/memory-orchestrator/ — єдиний API для памʼяті агентів.
3. services/toolcore/ — registry + execution engine для tools.
4. Інтеграція з agent-runtime.
5. Оновлення docker-compose та docs.
---
## 1) SERVICE: LLM PROXY (services/llm-proxy)
**Create folder:**
```
services/llm-proxy/
main.py
models.py
router.py
providers/
__init__.py
openai_provider.py
deepseek_provider.py
local_provider.py
config.yaml
middlewares.py
logging_middleware.py
Dockerfile
requirements.txt
README.md
```
### 1.1 API
**Base URL (internal-only):**
- `POST /internal/llm/proxy`
**Request (v1):**
```json
{
"model": "gpt-4.1-mini",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." }
],
"metadata": {
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"channel_id": "..."
}
}
```
**Response:**
```json
{
"content": "текст відповіді",
"usage": {
"prompt_tokens": 123,
"completion_tokens": 45,
"total_tokens": 168
},
"provider": "openai",
"model_resolved": "gpt-4.1-mini"
}
```
**Optional (stub for later):**
- `/internal/llm/batch`
- `/internal/llm/stream` (можна залишити TODO)
### 1.2 Model routing (router.py)
Створити модуль:
```python
def route_model(logical_model: str) -> ProviderConfig:
# Читає зі структури в config.yaml
# Напр.:
# - gpt-4.1-mini → OpenAI (gpt-4.1-mini)
# - deepseek-r1 → DeepSeek API
# - dagi-local-8b → Local provider (Ollama / vllm)
```
**config.yaml (приклад):**
```yaml
providers:
openai:
base_url: "https://api.openai.com/v1"
api_key_env: "OPENAI_API_KEY"
deepseek:
base_url: "https://api.deepseek.com/v1"
api_key_env: "DEEPSEEK_API_KEY"
local:
base_url: "http://local-llm:8000"
models:
gpt-4.1-mini:
provider: "openai"
name: "gpt-4.1-mini"
deepseek-r1:
provider: "deepseek"
name: "deepseek-r1"
dagi-local-8b:
provider: "local"
name: "qwen2.5-8b-instruct"
```
### 1.3 Providers (providers/*.py)
Кожен провайдер:
```python
class BaseProvider(Protocol):
async def chat(self, messages: list[ChatMessage], model_name: str, **kwargs) -> LLMResponse: ...
```
**Файли:**
- `providers/openai_provider.py`
- `providers/deepseek_provider.py`
- `providers/local_provider.py`
Вони:
- отримують normalized messages,
- викликають відповідний API/endpoint,
- повертають LLMResponse (контент + usage).
**Local provider:**
- для Phase 3 можна зробити простий HTTP-запит до існуючого локального сервісу (Ollama/vLLM), або stub.
### 1.4 Logging & limits (middlewares.py, logging_middleware.py)
Добавити:
- простий rate limit per-agent (стабово, через in-memory або Redis stub).
- логування викликів:
- agent_id, microdao_id, model, latency, tokens.
- TODO hooks для Billing/Usage Engine (на майбутнє).
### 1.5 README.md
Описати:
- підтримувані моделі та мапінг,
- як додати нового провайдера,
- як викликати /internal/llm/proxy з інших сервісів (особливо agent-runtime).
---
## 2) SERVICE: MEMORY ORCHESTRATOR (services/memory-orchestrator)
**Goal:**
Єдиний API для роботи з памʼяттю:
- short-term (канальний контекст / event log),
- mid-term (agent memory / RAG embedding store),
- long-term (knowledge base, docs, roadmaps).
**Create folder:**
```
services/memory-orchestrator/
main.py
models.py
router.py
backends/
__init__.py
short_term_pg.py
vector_store_pg.py
kb_filesystem.py
embedding_client.py
config.yaml
Dockerfile
requirements.txt
README.md
```
### 2.1 API
**Base URL:**
- `POST /internal/agent-memory/query`
- `POST /internal/agent-memory/store`
- `POST /internal/agent-memory/summarize` (optional v1)
**Request /query:**
```json
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"channel_id": "optional",
"query": "коротко, які були останні зміни в цьому microDAO?",
"limit": 5
}
```
**Response:**
```json
{
"items": [
{
"id": "mem-123",
"kind": "conversation | kb | task | dao-event",
"score": 0.87,
"content": "Текст фрагменту памʼяті",
"meta": {
"source": "channel:...",
"timestamp": "..."
}
}
]
}
```
**Request /store:**
```json
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"channel_id": "optional",
"kind": "conversation | kb | task | dao-event",
"content": {
"user_message": "...",
"agent_reply": "..."
}
}
```
**Response:**
```json
{ "ok": true, "id": "mem-123" }
```
### 2.2 Backends
**backends/short_term_pg.py:**
- зберігає сирі events (або посилання на messages) в PostgreSQL, якщо треба окремо від messenger.
**backends/vector_store_pg.py:**
- використовує embeddings (через embedding_client.py) і зберігає в таблиці:
```sql
agent_memories (id, agent_id, microdao_id, embedding vector, content, meta)
```
- простий cosine similarity пошук.
**backends/kb_filesystem.py:**
- stub для long-term знань (roadmaps, docs), можна залишити TODO.
### 2.3 Embedding client (embedding_client.py)
- Простий клієнт до існуючої embedding-моделі (наприклад, BGE-M3 чи інша) через /internal/embedding/proxy або безпосередньо.
- Якщо embedding-сервіс не описаний — зробити stub + TODO.
### 2.4 Integration points
**agent-runtime:**
- замінити прямі виклики agent-memory на:
- `POST /internal/agent-memory/query`
- `POST /internal/agent-memory/store`
Надалі можна буде підʼєднати:
- microDAO knowledge,
- DAO events,
- roadmaps.
---
## 3) SERVICE: TOOLCORE (services/toolcore)
**Goal:**
Єдина точка для:
- реєстрації tools (інструментів агентів),
- перевірки дозволів,
- виконання tools та повернення результату агенту.
**Create folder:**
```
services/toolcore/
main.py
models.py
router.py
registry.py
executors/
__init__.py
http_executor.py
python_executor.py (optional)
config.yaml
Dockerfile
requirements.txt
README.md
```
### 3.1 Data model
**models.py:**
```python
class ToolDefinition(BaseModel):
id: str # "projects.list"
name: str
description: str
input_schema: dict # JSON Schema
output_schema: dict # JSON Schema
executor: Literal["http", "python"]
target: str # HTTP URL or python path
allowed_agents: list[str] | None
class ToolCallRequest(BaseModel):
tool_id: str
agent_id: str
microdao_id: str
args: dict
context: dict | None = None
class ToolCallResult(BaseModel):
ok: bool
result: dict | None = None
error: str | None = None
```
### 3.2 API
**HTTP (internal):**
- `GET /internal/tools`
→ список доступних tools
- `POST /internal/tools/call`
Body: ToolCallRequest
Behavior:
- перевірити, чи agent_id має право на tool_id (allowed_agents).
- знайти ToolDefinition.
- виконати через відповідний executor.
- повернути ToolCallResult.
### 3.3 Executors
**executors/http_executor.py:**
- викликає target як HTTP endpoint (POST з args + context).
- хендлінг помилок та таймаутів.
**executors/python_executor.py (optional):**
- allows direct call of internal python functions (у v1 можна залишити TODO або мінімальний stub).
### 3.4 Registry (registry.py)
Початок — з config.yaml (static registry):
**config.yaml:**
```yaml
tools:
- id: "projects.list"
name: "List projects"
description: "Повертає список проєктів microDAO."
input_schema:
type: "object"
properties:
microdao_id: { type: "string" }
required: ["microdao_id"]
output_schema:
type: "array"
items: { type: "object" }
executor: "http"
target: "http://projects-service:8000/internal/tools/projects.list"
allowed_agents: ["agent:sofia", "agent:pm", "agent:cto"]
```
**Registry API:**
- load from config.yaml at startup
- later можна зробити DB-backed registry.
### 3.5 NATS (optional для Phase 3)
Можна додати:
- subject: `tool.request`
- subject: `tool.response`
Але для Phase 3 достатньо HTTP-контракту, який викликає agent-runtime.
---
## 4) INTEGRATION: agent-runtime + LLM Proxy + Memory + Toolcore
**Update agent-runtime:**
### 4.1 Використовувати LLM Proxy
Замість прямого виклику будь-яких LLM:
- завжди викликати:
```http
POST /internal/llm/proxy
{
"model": "<logical model from agent blueprint>",
"messages": [...],
"metadata": {
"agent_id": ...,
"microdao_id": ...,
"channel_id": ...
}
}
```
### 4.2 Використовувати Memory Orchestrator
При channel_message:
- контекст:
`GET /internal/messaging/channels/{channelId}/messages?limit=50`
- памʼять:
`POST /internal/agent-memory/query`
- запис:
`POST /internal/agent-memory/store` після відповіді.
### 4.3 Виклик tools
Якщо в blueprint агента передбачені tools:
- парсити вихід LLM (JSON/structured) або через simple convention (наприклад, спеціальний формат "TOOL: projects.list {...}").
- для кожного tool call:
```http
POST /internal/tools/call
{
"tool_id": "...",
"agent_id": "...",
"microdao_id": "...",
"args": {...}
}
```
- включати результати tools у наступний LLM виклик як контекст.
- (На Phase 3 можна реалізувати один-два прості tools: projects.list, followups.create.)
---
## 5) DOCKER + DOCS
### 5.1 docker-compose
Додати new services:
- llm-proxy
- memory-orchestrator
- toolcore
Переконатися, що:
- вони в тій же мережі, що agent-runtime, messaging-service, NATS, DB;
- є healthcheck-и (GET /health).
### 5.2 Документація
Додати/оновити:
- `docs/LLM_PROXY_SPEC.md`
- `docs/MEMORY_ORCHESTRATOR_SPEC.md`
- `docs/TOOLCORE_SPEC.md`
- оновити `docs/INDEX.md` (додати посилання на ці три сервіси).
- в PHASE2_READY.md або окремому PHASE3_READY.md описати нову архітектуру agent pipeline.
---
## 6) ACCEPTANCE CRITERIA
### 1) LLM Proxy:
- ✅ /internal/llm/proxy приймає запити, повертає відповіді.
- ✅ Принаймні 2 логічні моделі працюють (наприклад, один remote, один local.stub).
- ✅ Логуються виклики per-agent.
### 2) Memory Orchestrator:
- ✅ /internal/agent-memory/query повертає релевантні фрагменти (можна з простим vector search або stub).
- ✅ /internal/agent-memory/store зберігає нові entries.
- ✅ agent-runtime успішно використовує його при відповіді.
### 3) Toolcore:
- ✅ /internal/tools повертає список інструментів.
- ✅ /internal/tools/call виконує хоча б один реальний tool (HTTP executor).
- ✅ agent-runtime може викликати хоча б один tool впродовж свого pipeline.
### 4) End-to-end:
- ✅ Агент у Messenger:
- читає контекст (messages + memory),
- викликає LLM через LLM Proxy,
- за потреби викликає tool через Toolcore,
- повертає відповідь у канал.
- ✅ Для одного демо-агента (наприклад, Sofia-Prime) цей ланцюг стабільно працює.
---
## END OF TASK
**Estimated Time:** 6-8 weeks
**Priority:** High
**Dependencies:** Phase 2 complete
**Version:** 1.0.0
**Date:** 2025-11-24

View File

@@ -0,0 +1,487 @@
# PHASE 3 ROADMAP — Core Agent Services
**After Phase 2 Agent Integration**
**Status:** 📋 Planning → ✅ SPEC READY
**Master Task:** [PHASE3_MASTER_TASK.md](PHASE3_MASTER_TASK.md) ⭐
**Summary:** [PHASE3_READY.md](../../PHASE3_READY.md)
**Priority:** High
**Estimated Time:** 6-8 weeks
**Dependencies:** Phase 2 complete
---
## 🎯 Goal
Replace Phase 2 stubs with production-ready services:
- Real LLM Proxy (multi-provider routing)
- Real Agent Memory (RAG + vector DB)
- Tool Registry (agent actions)
- Agent Blueprint Management (CRUD + versioning)
---
## 📦 Phase 3 Components
### 1. LLM Proxy Service (2 weeks)
**Purpose:** Centralized LLM gateway with routing, rate limiting, cost tracking
**Features:**
- Multi-provider support (OpenAI, Anthropic, DeepSeek, Local)
- Model selection & routing
- Rate limiting per agent/microDAO
- Cost tracking & billing
- Streaming support
- Error handling & retries
- Prompt sanitization
**API:**
```http
POST /internal/llm/proxy
{
"model": "gpt-4",
"messages": [...],
"stream": false,
"max_tokens": 1000,
"agent_id": "agent:sofia",
"microdao_id": "microdao:daarion"
}
GET /internal/llm/models
List available models
GET /internal/llm/usage?agent_id=agent:sofia&period=30d
Usage statistics
```
**Tech Stack:**
- FastAPI
- httpx for provider calls
- Redis for rate limiting
- PostgreSQL for usage tracking
**Files:**
```
services/llm-proxy/
├── main.py
├── providers/
│ ├── openai.py
│ ├── anthropic.py
│ ├── deepseek.py
│ └── local.py
├── routing.py
├── rate_limiter.py
├── cost_tracker.py
├── models.py
└── config.yaml
```
---
### 2. Agent Memory Service (2 weeks)
**Purpose:** Persistent memory + RAG for agents
**Features:**
- Short-term memory (recent context)
- Mid-term memory (session/task memory)
- Long-term memory (knowledge base)
- Vector search (RAG)
- Memory indexing (from channel history)
- Memory pruning (for cost/performance)
- Per-agent & per-microDAO isolation
**API:**
```http
POST /internal/agent-memory/query
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:daarion",
"query": "What did we discuss about Phase 2?",
"k": 5,
"memory_types": ["mid_term", "long_term"]
}
Top-k relevant memories
POST /internal/agent-memory/store
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:daarion",
"memory_type": "mid_term",
"content": {
"user_message": "...",
"agent_reply": "...",
"context": {...}
}
}
Store new memory
GET /internal/agent-memory/agents/{agent_id}/stats
Memory usage stats
```
**Tech Stack:**
- FastAPI
- PostgreSQL (structured memory)
- Qdrant/Weaviate/ChromaDB (vector DB for RAG)
- LangChain/LlamaIndex (RAG helpers)
**Files:**
```
services/agent-memory/
├── main.py
├── vector_store.py
├── memory_manager.py
├── rag_engine.py
├── indexer.py
├── models.py
└── config.yaml
```
---
### 3. Tool Registry Service (1.5 weeks)
**Purpose:** Centralized tool definitions & execution for agents
**Features:**
- Tool catalog (list all available tools)
- Tool execution (secure sandbox)
- Tool permissions (agent → tool mapping)
- Tool versioning
- Execution logs & auditing
**Tools (initial set):**
- `create_task(channel_id, title, description)`
- `create_followup(user_id, message_id, reminder_text, due_date)`
- `search_docs(query)`
- `create_project(microdao_id, name, description)`
- `summarize_channel(channel_id, period)`
- `send_notification(user_id, text)`
**API:**
```http
GET /internal/tools/catalog
List all tools
POST /internal/tools/execute
{
"tool_name": "create_task",
"agent_id": "agent:sofia",
"microdao_id": "microdao:daarion",
"parameters": {
"channel_id": "...",
"title": "Review Phase 2",
"description": "..."
}
}
Execute tool, return result
GET /internal/tools/agents/{agent_id}/permissions
List tools agent can use
```
**Tech Stack:**
- FastAPI
- Dynamic tool loading (plugins)
- Sandboxed execution (Docker/gVisor)
- PostgreSQL (tool definitions, permissions, logs)
**Files:**
```
services/tool-registry/
├── main.py
├── catalog.py
├── executor.py
├── sandbox.py
├── permissions.py
├── tools/
│ ├── task_tools.py
│ ├── project_tools.py
│ ├── notification_tools.py
│ └── ...
└── config.yaml
```
---
### 4. Agent Blueprint Service (1 week)
**Purpose:** CRUD + versioning for agent definitions
**Features:**
- Create/Read/Update/Delete agent blueprints
- Blueprint versioning
- Blueprint templates (archetypes)
- Blueprint validation
- Blueprint inheritance
**API:**
```http
GET /internal/agents/blueprints
List all blueprints
POST /internal/agents/blueprints
{
"code": "sofia_prime_v2",
"name": "Sofia Prime v2",
"model": "gpt-4.1",
"instructions": "...",
"capabilities": {...},
"tools": ["create_task", "summarize_channel"]
}
Create blueprint
GET /internal/agents/blueprints/{blueprint_id}
Get blueprint
GET /internal/agents/{agent_id}/blueprint
Get blueprint for specific agent instance
PUT /internal/agents/blueprints/{blueprint_id}
Update blueprint (creates new version)
```
**Tech Stack:**
- FastAPI
- PostgreSQL (blueprints, versions)
- YAML/JSON schema validation
**Files:**
```
services/agents-service/
├── main.py
├── blueprints/
│ ├── crud.py
│ ├── versioning.py
│ ├── validation.py
│ └── templates.py
├── models.py
└── config.yaml
```
---
### 5. Integration Updates (1 week)
**Update agent-runtime to use real services:**
```python
# Before (Phase 2):
blueprint = await load_agent_blueprint(agent_id) # Mock
memory = await query_memory(...) # Stub
llm_response = await generate_response(...) # Stub
# After (Phase 3):
blueprint = await agents_service.get_blueprint(agent_id) # Real
memory = await memory_service.query(...) # Real RAG
llm_response = await llm_proxy.generate(...) # Real multi-provider
# NEW: Tool usage
if llm_suggests_tool_use:
tool_result = await tool_registry.execute(tool_name, parameters)
# Add tool result to context, call LLM again
```
---
## 📅 Timeline
### Week 1-2: LLM Proxy
- Week 1: Core routing + OpenAI provider
- Week 2: Multi-provider + rate limiting + cost tracking
### Week 3-4: Agent Memory
- Week 3: Vector store setup + basic RAG
- Week 4: Memory management + indexing
### Week 5-6: Tool Registry
- Week 5: Catalog + basic tools (task, followup)
- Week 6: Executor + permissions + sandboxing
### Week 7: Agent Blueprint Service
- CRUD + versioning + validation
### Week 8: Integration & Testing
- Update agent-runtime
- E2E testing
- Performance optimization
- Documentation
---
## 🧪 Testing Strategy
### LLM Proxy Testing:
- Unit: Each provider (OpenAI, Anthropic, etc.)
- Integration: Rate limiting, cost tracking
- Load: 100 concurrent requests
- Failover: Provider unavailable scenarios
### Agent Memory Testing:
- RAG accuracy: Retrieve relevant memories
- Memory indexing: Auto-index from channels
- Vector search performance: < 500ms
- Memory pruning: Clean old memories
### Tool Registry Testing:
- Tool execution: All tools work
- Permissions: Agent cannot use unauthorized tools
- Sandboxing: Tools cannot escape sandbox
- Audit logs: All executions logged
### E2E Testing:
- User asks agent to create task → Task created
- User asks agent to summarize → Summary posted
- Agent uses memory correctly in replies
- Multiple providers work (switch between OpenAI/DeepSeek)
---
## 🎯 Acceptance Criteria
### Phase 3 Complete When:
- ✅ LLM Proxy supports 3+ providers
- ✅ Agent Memory RAG works (< 500ms queries)
- ✅ Tool Registry has 5+ working tools
- ✅ Agent Blueprint CRUD works
- ✅ agent-runtime integrated with all services
- ✅ E2E: User → Agent (with tool use) → Result
- ✅ Cost tracking shows LLM usage per agent
- ✅ Memory usage shows per agent/microDAO
- ✅ All services pass health checks
- ✅ Documentation complete
---
## 📊 Success Metrics
| Metric | Target |
|--------|--------|
| LLM response time | < 2s (non-streaming) |
| Memory query time | < 500ms |
| Tool execution time | < 3s |
| E2E agent reply | < 5s (with tool use) |
| LLM cost per request | < $0.05 |
| System uptime | > 99.5% |
---
## 🔗 Dependencies
### External Services:
- OpenAI API (for GPT-4)
- Anthropic API (for Claude, optional)
- DeepSeek API (optional)
- Qdrant/Weaviate (for vector DB)
### Internal Services:
- PostgreSQL (for all structured data)
- Redis (for rate limiting, caching)
- NATS (for events)
---
## 💡 Optional Enhancements (Phase 3.5)
### LLM Proxy:
- Streaming SSE support
- Local model support (Ollama, vLLM)
- Prompt caching
- A/B testing for prompts
### Agent Memory:
- Hierarchical memory (microDAO → team → agent)
- Memory sharing between agents
- Memory snapshots (save/restore agent state)
- Memory analytics dashboard
### Tool Registry:
- Tool marketplace (community tools)
- Tool composition (chain tools)
- Visual tool builder
- Tool usage analytics
---
## 🚀 Quick Start (After Phase 2)
### To prepare for Phase 3:
```bash
# 1. Review Phase 3 roadmap
cat docs/tasks/PHASE3_ROADMAP.md
# 2. Set up external services
# - Get OpenAI API key
# - Set up Qdrant (Docker or cloud)
# - Set up Redis
# 3. Start with LLM Proxy
mkdir -p services/llm-proxy
cd services/llm-proxy
# Follow PHASE3_LLM_PROXY_TASK.md (to be created)
```
---
## 📝 Task Files (To Be Created)
After Phase 2 complete, create detailed tasks:
1. **TASK_PHASE3_LLM_PROXY.md** (2 weeks)
2. **TASK_PHASE3_AGENT_MEMORY.md** (2 weeks)
3. **TASK_PHASE3_TOOL_REGISTRY.md** (1.5 weeks)
4. **TASK_PHASE3_BLUEPRINT_SERVICE.md** (1 week)
5. **TASK_PHASE3_INTEGRATION.md** (1 week)
---
## 🎓 Architecture Evolution
### Phase 1 (Complete):
```
User → Frontend → messaging-service → Matrix → Frontend
```
### Phase 2 (Current):
```
User → Messenger → agent_filter → Router → agent-runtime (stub) → Reply
```
### Phase 3 (Target):
```
User → Messenger
agent_filter → Router → agent-runtime
├─ LLM Proxy → [OpenAI | Anthropic | DeepSeek]
├─ Agent Memory → [Vector DB | PostgreSQL]
├─ Tool Registry → [Task | Project | Notification tools]
└─ Agent Blueprint → [Definitions | Versions]
Reply with tool results
```
---
## ✅ Current Status
- ✅ Phase 1: Messenger Core (Complete)
- 📋 Phase 2: Agent Integration (In Progress)
- 📋 Phase 3: Core Services (This Roadmap)
- 🔜 Phase 4: Advanced Features (TBD)
---
**Ready for Phase 3?**
First complete Phase 2, then return to this roadmap for detailed implementation tasks.
---
**Version:** 1.0.0
**Date:** 2025-11-24
**Status:** Planning

251
docs/tasks/README.md Normal file
View File

@@ -0,0 +1,251 @@
# DAARION Cursor Tasks
**Готові таски для Cursor AI / інженерів**
Ця директорія містить структуровані задачі для імплементації функцій DAARION.
---
## 📋 Available Tasks
### 🔥 MASTER TASK: PHASE2_MASTER_TASK.md
**Status:** 🚀 READY TO COPY TO CURSOR
**Priority:** Critical
**Estimated Time:** 4 weeks
**The complete, copy-paste-ready task for Cursor AI!**
Full implementation with code examples:
- agent_filter (complete Python code)
- Router extension (complete code)
- agent-runtime (complete code)
- Docker integration
**How to use:**
```bash
# Copy entire file and paste into Cursor
cat docs/tasks/PHASE2_MASTER_TASK.md | pbcopy
```
---
### 1. TASK_PHASE2_AGENT_INTEGRATION.md
**Status:** 📋 Ready to implement
**Priority:** High
**Estimated Time:** 4 weeks
**Goal:**
Повна інтеграція агентів у Messenger:
- `agent_filter` service — Security & routing layer
- DAGI Router extension — Message routing
- `agent-runtime-service` — LLM + Memory + Posting
**Deliverables:**
- 3 нові сервіси (agent-filter, router extension, agent-runtime)
- NATS event integration (actual publishing)
- Full flow: Human → agent_filter → Router → Agent Runtime → Reply
**Dependencies:**
- Messenger Module (✅ Complete)
- NATS JetStream (✅ Running)
- Matrix Synapse (✅ Running)
**How to use:**
```bash
# Copy entire task into Cursor
cat docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
# Or work step-by-step:
# 1) Implement agent_filter
# 2) Extend DAGI Router
# 3) Implement agent-runtime
# 4) Docker-compose integration
```
---
### 2. TASK_AGENT_HUB_MVP.md
**Status:** 📋 Ready to implement
**Priority:** High
**Estimated Time:** 2 weeks
**Goal:**
Створити Agent Hub — головний інтерфейс для роботи з агентами:
- 3-колонковий layout (Agents | Chat | Context)
- Direct channels з агентами
- Reuse Messenger components
- Context panel (projects, capabilities)
**Deliverables:**
- Frontend: `/hub` route з 6 компонентами
- Backend: Agent Hub API (4 endpoints)
- Navigation links (Onboarding → Hub, City → Hub)
**Dependencies:**
- Messenger Module (✅ Complete)
- TASK_PHASE2_AGENT_INTEGRATION (⚠️ Recommended but not blocking for UI)
**How to use:**
```bash
# Copy entire task into Cursor
cat docs/tasks/TASK_AGENT_HUB_MVP.md
# Or implement incrementally:
# 1) Backend API (agents-service)
# 2) Frontend structure
# 3) Messenger integration
# 4) Context panel
```
---
## 🔄 Task Workflow
### Step 1: Choose Task
Select based on priority and dependencies.
### Step 2: Review
- Read task completely
- Check dependencies
- Review acceptance criteria
### Step 3: Implement
- Copy task to Cursor
- Follow structure step-by-step
- Test each component
### Step 4: Validate
- Run acceptance criteria tests
- Update documentation
- Mark task as complete
---
## 📚 Related Documentation
### Core Docs
- [MESSAGING_ARCHITECTURE.md](../MESSAGING_ARCHITECTURE.md) — Complete technical spec
- [MESSENGER_COMPLETE_SPECIFICATION.md](../MESSENGER_COMPLETE_SPECIFICATION.md) — Master navigation
- [messaging-erd.dbml](../messaging-erd.dbml) — Database ERD
### Implementation Guides
- [MESSENGER_MODULE_COMPLETE.md](../MESSENGER_MODULE_COMPLETE.md) — Phase 1 summary
- [MESSENGER_TESTING_GUIDE.md](../MESSENGER_TESTING_GUIDE.md) — Testing scenarios
---
## 🎯 Roadmap
```
✅ Phase 1: Messenger Core (Complete)
- Database schema
- messaging-service
- matrix-gateway spec
- Frontend UI
- WebSocket real-time
📋 Phase 2: Agent Integration (Next - TASK_PHASE2)
- agent_filter service
- DAGI Router extension
- agent-runtime-service
- NATS events
📋 Phase 2.5: Agent Hub (Parallel - TASK_AGENT_HUB)
- Agent Hub UI
- Direct channels
- Context panel
- Navigation
🔜 Phase 3: Advanced Features
- Multi-agent coordination
- Scheduled messages
- Voice messages
- Analytics
```
---
## 💡 Tips for Using Tasks
### For Cursor AI:
1. Copy entire task file
2. Paste as single prompt
3. Let Cursor work through incrementally
4. Review generated code
5. Test acceptance criteria
### For Human Developers:
1. Read task thoroughly
2. Break into sub-tasks if needed
3. Implement step-by-step
4. Cross-reference with architecture docs
5. Write tests
### For Team Leads:
1. Assign tasks based on expertise
2. Track progress via acceptance criteria
3. Review deliverables
4. Update roadmap
---
## 🚀 Quick Start
### To start Phase 2:
```bash
# 1. Read task
cat docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
# 2. Start with agent_filter
cd services
mkdir agent-filter
cd agent-filter
# Follow task instructions...
# 3. Test
docker-compose -f docker-compose.agents.yml up -d
```
### To start Agent Hub:
```bash
# 1. Read task
cat docs/tasks/TASK_AGENT_HUB_MVP.md
# 2. Create feature structure
mkdir -p src/features/agent-hub/{components,hooks,api}
# Follow task instructions...
# 3. Test
npm run dev
# Navigate to http://localhost:8899/hub
```
---
## 📊 Task Status
| Task | Status | Progress | ETA |
|------|--------|----------|-----|
| Messenger Core (Phase 1) | ✅ Complete | 100% | Done |
| **Phase 2: Agent Integration** | 🚀 **READY** | 0% | 4 weeks |
| Agent Hub MVP | 📋 Ready | 0% | 2 weeks |
| Phase 3: Core Services | 📋 Roadmap | 0% | 6-8 weeks |
| Projects Module | 🔜 Planned | 0% | TBD |
| Follow-ups Module | 🔜 Planned | 0% | TBD |
---
## 🤝 Contributing
When adding new tasks:
1. Use existing task format
2. Include all sections (Goal, Dependencies, Deliverables, Acceptance Criteria)
3. Add to this README
4. Link to related docs
5. Update roadmap
---
**Last Updated:** 2025-11-24
**Maintainer:** DAARION Platform Team

View File

@@ -0,0 +1,276 @@
# TASK: Implement Agent Hub (Team Assistant) using existing Messenger as core
**Goal:**
Створити головний "Agent Hub" інтерфейс (Team Assistant), який:
- показує список microDAO / команд / агентів,
- дозволяє вибрати агента й говорити з ним (на основі Messenger),
- показує контекст (проєкти, задачі, квести, стан агента),
- стане `/home` для користувача.
**Constraints:**
- Використати існуючий Messenger (channels, messages, WS) як ядро для чату.
- Не дублювати логіку відправки/отримання повідомлень.
- Agent Hub = надбудова над Messenger + Agents + Projects (stub).
---
## 1) Frontend structure
**Create feature:**
```
src/features/agent-hub/
AgentHubPage.tsx
components/
AgentSidebar.tsx
AgentList.tsx
AgentListItem.tsx
AgentSummaryCard.tsx
AgentContextPanel.tsx
AgentStatusBadge.tsx
hooks/
useAgents.ts
useAgentHubState.ts
api/
getAgents.ts
getAgentSummary.ts
getAgentContext.ts
```
**Route:**
- Додати `/hub` або `/home`:
```tsx
<Route path="/hub" element={<AgentHubPage />} />
```
### Layout (3-колонковий)
```
┌──────────────────┬────────────────────────────┬───────────────────┐
│ Left: │ Center: │ Right: │
│ Agents sidebar │ Chat (Messenger embed) │ Context panel │
└──────────────────┴────────────────────────────┴───────────────────┘
```
#### Left (AgentSidebar):
- список:
- "My microDAO"
- "My agents"
- "System agents"
- кожен агент: name, kind, microDAO badge, status badge.
- фільтр/пошук.
#### Center:
- Вбудований Messenger:
- Reuse MessengerPage components:
- ChannelHeader
- MessageList
- MessageComposer
- Але:
- працюємо з "direct" або "agent-specific" channel.
- При виборі агента:
- якщо ще немає direct channel з цим агентом:
- викликати backend:
```http
POST /api/messaging/channels
{
"name": "DM with Sofia-Prime",
"type": "direct",
"agent_id": "<agent_id>"
}
```
- запамʼятати channel_id.
- якщо є → просто підключити Messenger до цього channel_id.
#### Right (AgentContextPanel):
- картка обраного агента:
- імʼя, архетип, модель, microDAO.
- статус: active/idle, останній reply.
- блок "Active projects" (stub):
- отримати з `/api/agent-hub/agents/{id}/context`
- список 35 проєктів/тасок (можна поки мок).
- блок "Capabilities":
- список інструментів агента (з blueprint):
- "Can manage tasks"
- "Can summarise channels"
- "Can create follow-ups"
---
## 2) Backend: Agent Hub API
**New service or extend existing agents-service:**
`services/agents-service/` (якщо вже є, розширити)
### Endpoints:
#### GET /api/agent-hub/agents
→ список агентів для поточного користувача:
```json
[
{
"id": "agent:sofia",
"name": "Sofia-Prime",
"kind": "assistant",
"microdao_id": "microdao:7",
"status": "online",
"model": "gpt-4.1",
"avatar_url": null
}
]
```
#### GET /api/agent-hub/agents/{agentId}/summary
→ коротке резюме:
```json
{
"id": "agent:sofia",
"name": "Sofia-Prime",
"kind": "assistant",
"microdao_id": "microdao:7",
"specialization": "Team Assistant / PM",
"description": "Допомагає планувати, підсумовувати, слідкує за задачами.",
"last_activity_at": "...",
"stats": {
"messages_last_24h": 42,
"channels": 5
}
}
```
#### GET /api/agent-hub/agents/{agentId}/context
→ контекст:
```json
{
"projects": [
{ "id": "...", "name": "Messenger v1", "status": "active" },
{ "id": "...", "name": "City Dashboard", "status": "active" }
],
"followups": [
{ "id": "...", "title": "Перевірити NATS інтеграцію", "due_at": "..." }
]
}
```
#### POST /api/agent-hub/agents/{agentId}/ensure-direct-channel
Body: `{}`
Behavior:
- знайти чи існує direct channel між user та agent:
```sql
SELECT FROM channels WHERE kind='direct' AND user_id=... AND agent_id=...
```
- якщо не існує:
- створити в messaging-service:
```http
POST /internal/messaging/channels
{
"name": "DM with Sofia-Prime",
"kind": "direct",
"microdao_id": "<current microdao>",
"participants": ["user:...", "agent:sofia"]
}
```
- повернути channel_id.
- якщо існує → повернути channel_id.
---
## 3) Frontend wiring
### useAgents.ts:
- `GET /api/agent-hub/agents`
- зберігати список агентів + loading/error.
### useAgentHubState.ts:
- стан:
- `selectedAgentId`
- `selectedChannelId`
- методи:
- `selectAgent(agentId)`:
- викликати `/api/agent-hub/agents/{agentId}/ensure-direct-channel`
- зберегти channelId
- `selectChannel(channelId)`
### AgentHubPage.tsx:
- Layout на 3 колонки.
- Зліва: AgentSidebar (list, onSelect → selectAgent).
- Центр:
- якщо selectedChannelId:
- рендер Messenger core:
```tsx
<MessengerChannelView channelId={selectedChannelId} />
```
(створити обгортку над існуючими компонентами Messenger, яка приймає channelId як проп).
- Праворуч: AgentContextPanel (summary + context для selectedAgent).
---
## 4) Reuse Messenger components
**Створити lightweight обгортку:**
`src/features/messenger/components/MessengerChannelView.tsx`
Яка:
- приймає `channelId` як проп,
- внутрішньо використовує:
- `useMessages(channelId)`
- `useMessagingWebSocket(channelId)`
- `MessageList`
- `MessageComposer`
- `ChannelHeader` (можна переоприділити title під імʼя агента).
Це дозволяє:
- зберегти один стек логіки для Messenger,
- використовувати його і в `/messenger`, і в `/hub`.
---
## 5) Навігація
Додати кнопку/посилання:
- з Onboarding (PortalScene) → редірект у `/hub` замість `/city` або як додаткову опцію.
- з `/city-v2` HUD: кнопка "Agent Hub" → `/hub`.
- з `/messenger`: кнопка "Open Agent Hub" → `/hub`.
---
## 6) Документація
Додати:
- `docs/AGENT_HUB_MVP.md`:
- опис ролі Agent Hub,
- API endpoints,
- UX flow (вибір агента → відкриття каналу → контекст справа),
- як це повʼязано з agent-runtime (Phase 2).
---
## Acceptance Criteria
- ✅ Route `/hub` відкривається без помилок.
-В AgentSidebar видно список агентів для поточного користувача.
- ✅ При виборі агента створюється (або знаходиться) direct channel, відкривається чат (Messenger components).
- ✅ Праворуч показується хоча б stub-контекст (projects/followups з мокових даних).
- ✅ Агент, підʼєднаний через Phase 2 (agent_filter + router + runtime), може відповідати в direct channel з Agent Hub.
---
**Version:** 1.0.0
**Date:** 2025-11-24
**Priority:** High
**Estimated Time:** 2 weeks
**Dependencies:** TASK_PHASE2_AGENT_INTEGRATION.md (recommended but not blocking for UI)

View File

@@ -0,0 +1,124 @@
# TASK_MATRIX_PREPARE_ONLY.md
Matrix & Element — PREPARATION PHASE (no deploy)
## 0. Ціль
Підготувати Cursor до майбутнього впровадження повного Matrix stack, але **нічого не запускаючи**.
Результат: структури, конфіги, документація, дизайн-схема — без інсталяції.
---
## 1. Структура майбутнього каталогу
```
infra/matrix/
synapse/
homeserver.yaml
workers.yaml
log.config
postgres/
init.sql
turn/
turnserver.conf
element-web/
config.json
gateway/
matrix.conf
README_MATRIX.md
```
---
## 2. Фази майбутнього впровадження
### Phase 1 — Base Synapse
- homeserver.yaml
- Postgres schema
- registration tokens
- media repo
- rate limits
- .well-known/matrix/client
- workers mode architecture
### Phase 2 — Element Web
- element-web build
- config.json
- routing через `/element/`
- custom branding
### Phase 3 — Federation
- DNS SRV
- federation listener
- multi-node mesh
- key sharing
- presence federation
### Phase 4 — Agent Bridge
- agent_matrix_bridge.py
- NATS ↔ Matrix events
- mapping rooms → channels
- agent replies → matrix events
### Phase 5 — DAARION City Mesh
- районні сервери
- глобальні канали Marketplace / Governance
---
## 3. Документація, яку треба створити (але не виконувати)
### README_MATRIX.md
- Архітектура
- Фази 15
- Валідація
- Security model
- Federation design
### config.json (Element)
- базові поля:
```json
{
"default_server_config": {},
"show_labs_settings": true,
"default_country_code": "UA"
}
```
### homeserver.yaml (шаблон)
- server_name
- signing keys
- registration
- modules (MSC3861, MSC3981)
- federation settings
---
## 4. Acceptance Criteria
Cursor повинен:
- створити всі шаблонні файли
- створити README_MATRIX.md
- підготувати структуру директорій
- НЕ запускати Synapse
- НЕ змінювати docker-compose
- НЕ виконувати деплой
- НЕ генерувати SSL/DNS
---
## 5. Команда до Cursor
**"Створи структуру та конфіги згідно TASK_MATRIX_PREPARE_ONLY.md, але не виконуй ніякого деплою."**

View File

@@ -0,0 +1,421 @@
# TASK: PHASE 2 — Agent Integration (agent_filter + DAGI Router + Agent Runtime)
**Goal:**
Зробити Messenger повноцінно агентним:
- новий сервіс agent_filter, який вирішує, коли й який агент відповідає;
- розширити DAGI Router, щоб маршрутизувати події з Messenger до агентів;
- реалізувати agent-runtime-service, який читає історію каналів, викликає LLM і постить відповіді назад у Messenger.
**Existing:**
- Messenger Module (Matrix-aware, Full Stack) вже реалізований:
- messaging-service (FastAPI, 9 endpoints + WS)
- matrix-gateway API spec (services/matrix-gateway/API_SPEC.md)
- DB schema (channels, messages, channel_members, message_reactions, channel_events)
- frontend /messenger (ChannelList, MessageList, MessageComposer, WS)
- NATS, Synapse, matrix-gateway, messaging-service у docker-compose.messenger.yml
- Документація:
- docs/MESSENGER_MODULE_COMPLETE.md
- docs/MESSAGING_ARCHITECTURE.md
- docs/messaging-erd.dbml
- docs/MESSENGER_TESTING_GUIDE.md
**PHASE 2 складається з 3 підзадач:**
---
## 1) Сервіс agent_filter
**Create new service:** `services/agent-filter/`
**Files:**
- `services/agent-filter/main.py`
- `services/agent-filter/models.py`
- `services/agent-filter/rules.py`
- `services/agent-filter/config.yaml`
- `services/agent-filter/requirements.txt`
- `services/agent-filter/Dockerfile`
- `services/agent-filter/README.md`
**Tech:**
- Python + FastAPI
- NATS JetStream client (python-nats)
- Config з YAML
### 1.1 Models (models.py)
Define Pydantic models:
```python
from pydantic import BaseModel
from typing import Optional, Literal
from datetime import datetime
class MessageCreatedEvent(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
sender_id: str
sender_type: Literal["human", "agent"]
microdao_id: str
created_at: datetime
class FilterDecision(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
microdao_id: str
decision: Literal["allow", "deny", "modify"]
target_agent_id: Optional[str] = None
rewrite_prompt: Optional[str] = None
class ChannelContext(BaseModel):
microdao_id: str
visibility: Literal["public", "private", "microdao"]
allowed_agents: list[str] = []
disabled_agents: list[str] = []
class FilterContext(BaseModel):
channel: ChannelContext
sender_is_owner: bool = False
sender_is_admin: bool = False
sender_is_member: bool = True
local_time: Optional[datetime] = None
```
### 1.2 Rules (rules.py)
Implement:
```python
def decide(event: MessageCreatedEvent, ctx: FilterContext) -> FilterDecision:
"""
Baseline rules v1:
- Якщо event.sender_type == "agent" → decision = "deny" (щоб не було loop).
- Якщо channel.visibility == "microdao" і є default assistant агента для microDAO:
- target_agent_id = цей агент (поки можна жорстко прописати в config або заглушка).
- Якщо час у quiet_hours (23:0007:00 з config.yaml):
- decision = "modify"
- rewrite_prompt = "Відповідай стисло і тільки якщо запит важливий. Не ініціюй розмову сам."
- Якщо агент заборонений у цьому каналі (agent_id у disabled_agents) → decision = "deny".
- Якщо немає жодного кандидата → decision = "deny".
"""
pass
```
**config.yaml:**
```yaml
nats:
servers: ["nats://nats:4222"]
messaging_subject: "messaging.message.created"
decision_subject: "agent.filter.decision"
rules:
quiet_hours:
start: "23:00"
end: "07:00"
default_agents:
"microdao:daarion": "agent:sofia"
```
### 1.3 main.py
- Підняти FastAPI:
- `GET /health``{ "status": "ok" }`
- `POST /internal/agent-filter/test` → приймає MessageCreatedEvent, викликає rules.decide(...), повертає FilterDecision.
- На startup:
- підʼєднатися до NATS
- підписатися на subject `messaging.message.created`
**Алгоритм обробки:**
1. Deserialize payload у MessageCreatedEvent.
2. Підібрати ChannelContext:
- `GET /internal/messaging/channels/{channel_id}/context` (потрібно додати цей endpoint у messaging-service, якщо ще нема).
- Очікуваний response:
```json
{
"microdao_id": "...",
"visibility": "microdao",
"allowed_agents": ["agent:sofia"],
"disabled_agents": []
}
```
3. Побудувати FilterContext.
4. Викликати rules.decide(event, ctx).
5. Опублікувати FilterDecision у NATS:
- subject: `agent.filter.decision`
- payload: `decision.json()`
### 1.4 Dockerfile + README
- Dockerfile подібний до messaging-service.
- README.md:
- як запускати локально,
- як тестувати `/internal/agent-filter/test`,
- приклад NATS payload.
---
## 2) Розширення DAGI Router під messaging.inbound
**Goal:**
- DAGI Router має слухати `agent.filter.decision` і на основі allow-рішень створювати AgentInvocation і штовхати в `router.invoke.agent`.
### 2.1 NATS subscription
У `services/router/` (або де реалізований DAGI Router):
- Підписка на subject: `agent.filter.decision`.
**Очікуваний payload:** FilterDecision (див. вище).
**Алгоритм:**
- Якщо `decision != "allow"` → ігноруємо.
- Якщо `decision == "allow"` і `target_agent_id` не заданий → логування + ігнор.
- Інакше: побудувати AgentInvocation:
```json
{
"agent_id": "<target_agent_id>",
"entrypoint": "channel_message",
"payload": {
"channel_id": "<channel_id>",
"message_id": "<message_id>",
"matrix_event_id": "<matrix_event_id>",
"microdao_id": "<microdao_id>",
"rewrite_prompt": "<rewrite_prompt>"
}
}
```
Опублікувати у NATS:
- subject: `router.invoke.agent`
- payload: AgentInvocation JSON.
### 2.2 Моделі
Додати в Router:
```python
from pydantic import BaseModel
from typing import Literal
class AgentInvocation(BaseModel):
agent_id: str
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
payload: dict
```
### 2.3 Конфіг
Файл `router_config.yaml` (або аналог):
```yaml
messaging_inbound:
enabled: true
source_subject: "agent.filter.decision"
target_subject: "router.invoke.agent"
```
### 2.4 HTTP debug endpoint
Додати в Router:
**POST /internal/router/test-messaging**
Body: FilterDecision
Behavior:
- прогнати той самий код, який обробляє NATS event,
- повернути AgentInvocation JSON без публікації у NATS.
---
## 3) Agent Runtime integration з Messenger
**Goal:**
- Реалізувати agent-runtime-service, який:
- читає контекст каналу (останні повідомлення),
- читає памʼять агента,
- викликає LLM через LLM Proxy,
- постить відповідь у канал через messaging-service.
**Create service:** `services/agent-runtime/`
**Files:**
- `services/agent-runtime/main.py`
- `services/agent-runtime/models.py`
- `services/agent-runtime/llm_client.py`
- `services/agent-runtime/messaging_client.py`
- `services/agent-runtime/memory_client.py`
- `services/agent-runtime/config.yaml`
- `services/agent-runtime/requirements.txt`
- `services/agent-runtime/Dockerfile`
- `services/agent-runtime/README.md`
### 3.1 Models (models.py)
```python
from pydantic import BaseModel
from typing import Literal
from datetime import datetime
class AgentInvocation(BaseModel):
agent_id: str
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
payload: dict
class ChannelContextMessage(BaseModel):
sender_id: str
sender_type: Literal["human", "agent"]
content: str
created_at: datetime
```
### 3.2 NATS subscription
**main.py:**
- Підʼєднатись до NATS.
- Підписатися на `router.invoke.agent`.
**Алгоритм:**
1. Deserialize AgentInvocation.
2. Якщо `entrypoint != "channel_message"` → поки що ігноруємо (або лог).
3. Витягти:
- `agent_id`
- `channel_id`, `message_id`, `matrix_event_id`, `microdao_id`, `rewrite_prompt` з payload.
4. Завантажити blueprint агента:
```http
GET /internal/agents/{agent_id}/blueprint
```
Очікуваний response:
```json
{
"id": "...",
"name": "Sofia-Prime",
"model": "gpt-4.1",
"instructions": "System prompt...",
"capabilities": {...}
}
```
5. Завантажити історію каналу:
```http
GET /internal/messaging/channels/{channel_id}/messages?limit=50
```
→ вернути список повідомлень у форматі ChannelContextMessage.
6. Витягти останнє human-повідомлення як user input.
7. Запитати памʼять:
```http
POST /internal/agent-memory/query
{
"agent_id": "<agent_id>",
"microdao_id": "<microdao_id>",
"channel_id": "<channel_id>",
"query": "<останній текст користувача>"
}
```
Очікуваний response: список релевантних фрагментів knowledge base.
8. Побудувати промпт для LLM (llm_client.py):
- system: інструкції з blueprint +, якщо є, rewrite_prompt
- context: останні N повідомлень (з імʼям, роллю, часом)
- memory: релевантні фрагменти
- user: останній текст користувача
9. Викликати LLM через LLM Proxy:
```http
POST /internal/llm/proxy
{
"model": "<з blueprint>",
"messages": [ {"role": "...", "content": "..."}, ... ]
}
```
Очікуваний response:
```json
{ "content": "<текст відповіді>" }
```
10. Надіслати відповідь у канал:
```http
POST /internal/agents/{agentId}/post-to-channel
{
"channel_id": "<channel_id>",
"text": "<llm response>"
}
```
Цей endpoint вже повинен існувати у messaging-service (як внутрішній).
11. (optional v1) Записати в памʼять:
```http
POST /internal/agent-memory/store
{
"agent_id": "<agent_id>",
"microdao_id": "<microdao_id>",
"channel_id": "<channel_id>",
"content": {
"user_message": "...",
"agent_reply": "..."
}
}
```
### 3.3 HTTP debug endpoint
**main.py:**
**POST /internal/agent-runtime/test-channel**
Body: AgentInvocation
Behavior:
- викликає ту саму логіку, що NATS handler,
- але замість реального POST до `/internal/agents/{agentId}/post-to-channel` просто повертає згенерований текст і зібраний prompt (обережно, без секретів у логах).
### 3.4 Docker + README
- Dockerfile за шаблоном інших сервісів.
- README.md:
- як запускати локально,
- як тестувати через `/internal/agent-runtime/test-channel`,
- як дивитись NATS events.
---
## 4) Інтеграція в docker-compose та документацію
### 4.1 docker-compose
- Додати `agent-filter`, `router` (якщо ще не доданий), `agent-runtime` у загальний docker-compose (або створити окремий `docker-compose.agents.yml`).
- Забезпечити доступ до:
- NATS
- messaging-service
- agent-memory-service (якщо вже існує) / stub
- agents-service (blueprints) / stub
- llm-proxy-service / stub
### 4.2 Документація
Оновити/додати:
- `docs/MESSAGING_ARCHITECTURE.md` — помітити, що PHASE 2 реалізовано.
- `docs/MESSENGER_COMPLETE_SPECIFICATION.md` — додати розділ "Agent Integration (PHASE 2)" з посиланнями на нові сервіси.
- При потребі: окремий `docs/AGENT_INTEGRATION_PHASE2.md` з коротким описом flow.
---
## Acceptance Criteria
- ✅ Human → пише в канал → agent_filter приймає event → DAGI Router відправляє AgentInvocation → Agent Runtime читає історію й памʼять → агент відповідає в той самий канал → повідомлення відображається у /messenger і в Element.
- ✅ Є мінімум один робочий агент (наприклад, Sofia-Prime), який стабільно відповідає в одному каналі microDAO.
-Всі сервіси стартують через docker-compose, health-checkи зелені.
---
**Version:** 1.0.0
**Date:** 2025-11-24
**Priority:** High
**Estimated Time:** 4 weeks

View File

@@ -0,0 +1,416 @@
# TASK_PHASE7_BACKEND_COMPLETION.md
## PHASE 7 — microDAO Console Backend Completion
### Goal
Доробити бекенд для **microdao-service** до production-ready стану:
- повний CRUD для microDAO;
- учасники (members) з ролями;
- проста казна (treasury) з балансами;
- налаштування (settings);
- PDP + Auth перевірки;
- базові NATS-події;
- інтеграція з існуючим фронтендом microDAO Console (MVP вже є).
---
## 0. Вихідні умови (вважати, що вже є)
З попереднього Phase 7 (MVP) вже створено:
- `migrations/008_create_microdao_core.sql` (схема БД);
- `services/microdao-service/main.py` (FastAPI-скелет, health endpoint);
- `services/microdao-service/models.py` (базові Pydantic-схеми);
- `services/microdao-service/requirements.txt`, `Dockerfile`;
- фронтенд:
- `src/api/microdao.ts` (чернетка);
- `src/features/microdao/MicrodaoListPage.tsx`;
- `src/features/microdao/MicrodaoConsolePage.tsx` (MVP з tabs);
- інфраструктура:
- `docker-compose.phase7.yml`;
- `scripts/start-phase7.sh`, `scripts/stop-phase7.sh`.
Цей таск ДОПОВНЮЄ вже створене, НЕ переписує з нуля.
---
## 1. Database: верифікація та дрібний тюнінг
1. Відкрити `migrations/008_create_microdao_core.sql` і переконатися, що там є таблиці:
```sql
microdaos (
id uuid primary key,
external_id text unique not null,
slug text unique not null,
name text not null,
description text,
owner_user_id uuid not null references users(id),
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
microdao_members (
id uuid primary key,
microdao_id uuid not null references microdaos(id),
user_id uuid not null references users(id),
role text not null, -- 'owner' | 'admin' | 'member' | 'guest'
joined_at timestamptz not null default now()
);
microdao_treasury (
id uuid primary key,
microdao_id uuid not null references microdaos(id),
token_symbol text not null,
balance numeric(30, 8) not null default 0,
updated_at timestamptz not null default now()
);
microdao_settings (
id uuid primary key,
microdao_id uuid not null references microdaos(id),
key text not null,
value jsonb
);
```
2. Додати індекси, якщо їх ще немає:
```sql
create index if not exists idx_microdao_members_user_id
on microdao_members(user_id);
create index if not exists idx_microdao_members_microdao_id_role
on microdao_members(microdao_id, role);
create index if not exists idx_microdao_treasury_microdao_id
on microdao_treasury(microdao_id);
```
3. Переконатися, що міграція **застосована** до dev-БД.
---
## 2. Repository layer для microDAO
Створити/оновити `services/microdao-service/repository_microdao.py`:
### 2.1. Вважати, що вже є спільний модуль для БД
Подивитися, як це зроблено в `agents-service` / `messaging-service` (наприклад, `database.py` або `db.py` з `async_session` або `Pool`):
* використовувати **той самий підхід** (SQLAlchemy / asyncpg), НЕ вводити новий.
### 2.2. Оголосити інтерфейс
У `repository_microdao.py` реалізувати функції (асинхронні, якщо так прийнято):
```python
# Псевдо-інтерфейс, реалізувати згідно з існуючим стилем проєкту
async def create_microdao(db, *, owner_user_id: uuid.UUID, slug: str, name: str, description: str | None) -> MicrodaoRead: ...
async def update_microdao(db, *, microdao_id: uuid.UUID, data: MicrodaoUpdate) -> MicrodaoRead | None: ...
async def delete_microdao(db, *, microdao_id: uuid.UUID) -> None: ...
async def get_microdao_by_slug(db, slug: str) -> MicrodaoRead | None: ...
async def get_microdao_by_id(db, microdao_id: uuid.UUID) -> MicrodaoRead | None: ...
async def list_microdaos_for_user(db, user_id: uuid.UUID) -> list[MicrodaoRead]: ...
```
### 2.3. Members
```python
async def list_members(db, microdao_id: uuid.UUID) -> list[MicrodaoMember]: ...
async def add_member(db, microdao_id: uuid.UUID, user_id: uuid.UUID, role: str) -> MicrodaoMember: ...
async def remove_member(db, member_id: uuid.UUID) -> None: ...
```
Правила:
* при створенні microDAO — власник автоматично додається в `microdao_members` з `role='owner'`;
* при видаленні microDAO (`delete_microdao`) — або `is_active=false`, або м'яке видалення (краще `is_active=false`).
### 2.4. Treasury
```python
async def get_treasury_items(db, microdao_id: uuid.UUID) -> list[TreasuryItem]: ...
async def apply_treasury_delta(db, microdao_id: uuid.UUID, token_symbol: str, delta: Decimal) -> TreasuryItem: ...
```
* `delta` може бути додатним/від'ємним;
* гарантувати, що `balance` не йде в мінус без крайньої потреби (можна кидати помилку при `balance+delta < 0`).
### 2.5. Settings
```python
async def get_settings(db, microdao_id: uuid.UUID) -> dict[str, Any]: ...
async def upsert_setting(db, microdao_id: uuid.UUID, key: str, value: Any) -> None: ...
```
* повернути `dict[key] = value` для фронтенду.
---
## 3. Pydantic models — models.py
Оновити `services/microdao-service/models.py`:
```python
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel
class MicrodaoBase(BaseModel):
slug: str
name: str
description: str | None = None
class MicrodaoCreate(MicrodaoBase):
pass
class MicrodaoUpdate(BaseModel):
name: str | None = None
description: str | None = None
is_active: bool | None = None
class MicrodaoRead(MicrodaoBase):
id: str
external_id: str
owner_user_id: str
is_active: bool
created_at: datetime
updated_at: datetime
class MicrodaoMember(BaseModel):
id: str
user_id: str
role: str
joined_at: datetime
class TreasuryItem(BaseModel):
token_symbol: str
balance: Decimal
class MicrodaoSettings(BaseModel):
values: dict[str, object]
```
За потреби вирівняти з уже існуючими типами в проєкті.
---
## 4. Routes: REST API для microDAO
Створити/оновити `services/microdao-service/routes_microdao.py`:
### 4.1. Auth + PDP клієнти
Створити `auth_client.py`, `pdp_client.py` (або використати спільні з інших сервісів, якщо вони вже є).
Мінімум:
```python
async def get_actor_identity(request) -> ActorIdentity: ...
async def check_permission(actor, action: str, resource: dict) -> None:
# кинути HTTPException(403) якщо deny
```
### 4.2. Endpoints
```python
from fastapi import APIRouter, Depends, HTTPException
from .models import MicrodaoCreate, MicrodaoUpdate, MicrodaoRead, MicrodaoMember, TreasuryItem
from . import repository_microdao as repo
router = APIRouter(prefix="/microdao", tags=["microdao"])
```
#### GET `/microdao`
Повертає всі microDAO, де actor є member:
* `actor = get_actor_identity()`
* `repo.list_microdaos_for_user(db, actor.user_id)`
#### POST `/microdao`
Створює новий microDAO:
* PDP: `action="MICRODAO_CREATE"`
* `owner_user_id = actor.user_id`
* виклик `repo.create_microdao(...)`
* автоматично створити запис в `microdao_members` з `role='owner'`.
#### GET `/microdao/{slug}`
* знайти microDAO по `slug`;
* PDP: `action="MICRODAO_READ"`, `resource={"microdao_id": id}`;
* повернути `MicrodaoRead`.
#### PUT `/microdao/{slug}`
* PDP: `action="MICRODAO_MANAGE"`;
* дозволити тільки owner/admin;
* оновити `name/description/is_active`.
#### DELETE `/microdao/{slug}`
* PDP: `action="MICRODAO_MANAGE"`;
* `is_active=false` (soft delete).
---
## 5. Routes: Members / Treasury / Settings
### 5.1. Members
У `routes_members.py` (або в тому ж `routes_microdao.py`, якщо ти тримаєш все разом):
```python
GET /microdao/{slug}/members
POST /microdao/{slug}/members
DELETE /microdao/{slug}/members/{member_id}
```
Правила:
* тільки owner/admin можуть:
* додавати членів;
* видаляти членів;
* змінювати роль (якщо імплементуєш PATCH).
* простий body для POST:
* `user_id: str`
* `role: str`
### 5.2. Treasury
```python
GET /microdao/{slug}/treasury
POST /microdao/{slug}/treasury # delta operation
```
* PDP: `READ_TREASURY` для GET, `MANAGE_TREASURY` для POST.
### 5.3. Settings
```python
GET /microdao/{slug}/settings
POST /microdao/{slug}/settings # { key, value }
```
---
## 6. NATS Events
У `main.py` microdao-service або в окремому модулі:
* Підключитися до NATS (використати той самий клієнт, що в інших сервісах).
* Функція helper:
```python
async def publish_event(subject: str, payload: dict) -> None: ...
```
Викликати:
* при `create_microdao`:
* subject: `microdao.event.created`
* при `update_microdao`:
* `microdao.event.updated`
* при додаванні/видаленні члена:
* `microdao.event.member_added`
* `microdao.event.member_removed`
* при оновленні treasury:
* `microdao.event.treasury_updated`
Payload мінімальний:
```json
{
"microdao_id": "...",
"slug": "daarion-city",
"actor_id": "user:...",
"ts": "2025-11-24T12:00:00Z",
"data": { ... }
}
```
---
## 7. Інтеграція в main.py
Оновити `services/microdao-service/main.py`:
* створити `app = FastAPI(...)`
* підключити `router`:
```python
from .routes_microdao import router as microdao_router
app.include_router(microdao_router)
# за потреби: members_router, treasury_router
```
* додати `/health` endpoint (якщо ще не зроблено).
---
## 8. Frontend: використати реальний бекенд
Оновити `src/api/microdao.ts`:
* `getMyMicrodaos() → GET /microdao`
* `getMicrodao(slug) → GET /microdao/{slug}`
* `createMicrodao(payload) → POST /microdao`
* `getMicrodaoMembers(slug) → GET /microdao/{slug}/members`
* `getMicrodaoTreasury(slug) → GET /microdao/{slug}/treasury`
* `getMicrodaoSettings(slug) → GET /microdao/{slug}/settings`
Потім оновити:
* `MicrodaoListPage.tsx`:
* щоб брав дані з `getMyMicrodaos()`;
* `MicrodaoConsolePage.tsx`:
* Overview → `getMicrodao(slug)`;
* Members tab → `getMicrodaoMembers(slug)`;
* Treasury tab → `getMicrodaoTreasury(slug)`.
---
## 9. Docker / Scripts
Оновити (якщо потрібно):
* `docker-compose.phase7.yml`:
* переконатися, що `microdao-service` піднятий і залежить від Postgres та auth/pdp;
* `scripts/start-phase7.sh`:
* додати команду застосування міграції `008` (як це робиться для інших);
* `scripts/stop-phase7.sh`:
* зупинити microdao-service і пов'язані сервіси.
---
## 10. Acceptance Criteria
Вважати завдання виконаним, якщо:
* [ ] `/microdao` повертає список microDAO, де actor є member;
* [ ] `/microdao` (POST) створює новий microDAO і додає owner в members;
* [ ] `/microdao/{slug}` повертає деталі microDAO;
* [ ] `/microdao/{slug}/members` повертає список учасників;
* [ ] `/microdao/{slug}/treasury` повертає список токенів;
* [ ] PDP блокує доступ до чужих microDAO (403);
* [ ] MicrodaoListPage показує **реальні** microDAO із БД;
* [ ] MicrodaoConsolePage показує **реальні** Overview/Members/Treasury без mock-даних;
* [ ] всі тести/линт проходять успішно.
END OF TASK

View File

@@ -0,0 +1,614 @@
# TASK_PHASE8_DAO_DASHBOARD.md
## PHASE 8 — DAO Dashboard (Governance + Treasury + Voting)
### Goal
Завершити **DAO-рівень governance** поверх вже готового microDAO Console:
- створити **dao-service** (бекенд) з повним CRUD;
- додати **governance models** (simple / quadratic / delegated);
- реалізувати **proposals + votes + treasury**;
- інтегрувати з **PDP/Auth** (Phase 4);
- зробити **DAO Dashboard UI** (frontend);
- підключити **NATS-події** для живого оновлення.
Фінальний результат:
DAO Dashboard, який показує стан DAO (учасники, казна, пропозиції, голосування) і дозволяє керувати governance.
---
## 0. Вихідні умови
Вважати, що в репозиторії вже є:
- **Phase 17 завершені** (Messenger, Agents, LLM, Security, Passkey, Agent Hub, microDAO Console);
- База даних (PostgreSQL) з таблицями `users`, `microdaos`, `microdao_members`, `microdao_treasury`, `microdao_settings`;
- **auth-service**, **pdp-service**, **usage-engine**, **messaging-service**, **agents-service**, **microdao-service**;
- Frontend (React/TS), з:
- `MicrodaoListPage.tsx`, `MicrodaoConsolePage.tsx`;
- auth/pdp інтеграцією;
- Agent Hub UI.
Цей таск додає **новий шар DAO** поверх існуючих microDAO.
---
## 1. Database: DAO Core Schema
Створити нову міграцію:
`migrations/009_create_dao_core.sql`
### 1.1. Таблиці
```sql
-- 1) DAO (верхній рівень governance)
create table if not exists dao (
id uuid primary key default gen_random_uuid(),
slug text not null unique,
name text not null,
description text,
microdao_id uuid not null references microdaos(id),
owner_user_id uuid not null references users(id),
governance_model text not null default 'simple', -- 'simple' | 'quadratic' | 'delegated'
voting_period_seconds integer not null default 604800, -- 7 днів
quorum_percent integer not null default 20, -- 20%
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_dao_microdao_id on dao(microdao_id);
create index if not exists idx_dao_owner_user_id on dao(owner_user_id);
-- 2) DAO Members (над microdao_members)
create table if not exists dao_members (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
user_id uuid not null references users(id),
role text not null, -- 'owner' | 'admin' | 'member' | 'guest'
joined_at timestamptz not null default now()
);
create index if not exists idx_dao_members_user_id on dao_members(user_id);
create index if not exists idx_dao_members_dao_id_role on dao_members(dao_id, role);
-- 3) DAO Treasury (агрегований шар над microdao_treasury, але на рівні DAO)
create table if not exists dao_treasury (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
token_symbol text not null,
contract_address text,
balance numeric(30, 8) not null default 0,
updated_at timestamptz not null default now()
);
create unique index if not exists uq_dao_treasury_token
on dao_treasury(dao_id, token_symbol);
-- 4) DAO Proposals
create table if not exists dao_proposals (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
slug text not null,
title text not null,
description text,
created_by_user_id uuid not null references users(id),
created_at timestamptz not null default now(),
start_at timestamptz,
end_at timestamptz,
status text not null default 'draft', -- 'draft' | 'active' | 'passed' | 'rejected' | 'executed'
governance_model_override text, -- optional override
quorum_percent_override integer
);
create unique index if not exists uq_dao_proposals_slug
on dao_proposals(dao_id, slug);
-- 5) DAO Votes
create table if not exists dao_votes (
id uuid primary key default gen_random_uuid(),
proposal_id uuid not null references dao_proposals(id) on delete cascade,
voter_user_id uuid not null references users(id),
vote_value text not null, -- 'yes' | 'no' | 'abstain'
weight numeric(30, 8) not null, -- actual weight after applying model
raw_power numeric(30, 8), -- до обробки
created_at timestamptz not null default now()
);
create index if not exists idx_dao_votes_proposal_id on dao_votes(proposal_id);
create unique index if not exists uq_dao_votes_proposal_voter
on dao_votes(proposal_id, voter_user_id);
-- 6) DAO Roles (додатковий шар, якщо потрібні нестандартні ролі)
create table if not exists dao_roles (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
code text not null,
name text not null,
description text
);
create unique index if not exists uq_dao_roles_code
on dao_roles(dao_id, code);
-- 7) DAO Role Assignments
create table if not exists dao_role_assignments (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
user_id uuid not null references users(id),
role_code text not null,
assigned_at timestamptz not null default now()
);
create index if not exists idx_dao_role_assignments_user
on dao_role_assignments(user_id);
-- 8) DAO Audit Log
create table if not exists dao_audit_log (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
actor_user_id uuid references users(id),
event_type text not null,
event_payload jsonb,
created_at timestamptz not null default now()
);
create index if not exists idx_dao_audit_log_dao_id
on dao_audit_log(dao_id);
```
Перевірити, що міграція застосовується без помилок.
---
## 2. Backend: `dao-service` (FastAPI)
Створити новий сервіс:
`services/dao-service/`:
* `main.py`
* `models.py`
* `repository_dao.py`
* `repository_proposals.py`
* `repository_votes.py`
* `governance_engine.py`
* `nats_events.py`
* `auth_client.py` / `pdp_client.py` (як thin-обгортки над існуючими)
* `requirements.txt`
* `Dockerfile`
* `README.md`
### 2.1. models.py
Оголосити Pydantic-схеми (адаптувати до стилю проєкту):
```python
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel
class DaoBase(BaseModel):
slug: str
name: str
description: str | None = None
class DaoCreate(DaoBase):
governance_model: str | None = None
voting_period_seconds: int | None = None
quorum_percent: int | None = None
class DaoUpdate(BaseModel):
name: str | None = None
description: str | None = None
governance_model: str | None = None
voting_period_seconds: int | None = None
quorum_percent: int | None = None
is_active: bool | None = None
class DaoRead(DaoBase):
id: str
microdao_id: str
owner_user_id: str
governance_model: str
voting_period_seconds: int
quorum_percent: int
is_active: bool
created_at: datetime
updated_at: datetime
class DaoMember(BaseModel):
id: str
user_id: str
role: str
joined_at: datetime
class DaoTreasuryItem(BaseModel):
token_symbol: str
contract_address: str | None = None
balance: Decimal
class ProposalBase(BaseModel):
slug: str
title: str
description: str | None = None
class ProposalCreate(ProposalBase):
start_at: datetime | None = None
end_at: datetime | None = None
class ProposalRead(ProposalBase):
id: str
dao_id: str
created_by_user_id: str
created_at: datetime
start_at: datetime | None
end_at: datetime | None
status: str
governance_model_override: str | None
quorum_percent_override: int | None
class VoteCreate(BaseModel):
vote_value: str # 'yes' | 'no' | 'abstain'
class VoteRead(BaseModel):
id: str
proposal_id: str
voter_user_id: str
vote_value: str
weight: Decimal
raw_power: Decimal | None
created_at: datetime
class DaoOverview(BaseModel):
dao: DaoRead
members_count: int
active_proposals_count: int
treasury_items: list[DaoTreasuryItem]
```
---
## 3. Repository layer
### 3.1. `repository_dao.py`
Реалізувати:
```python
async def list_dao_for_user(db, user_id: uuid.UUID) -> list[DaoRead]: ...
async def get_dao_by_slug(db, slug: str) -> DaoRead | None: ...
async def create_dao(db, *, microdao_id, owner_user_id, data: DaoCreate) -> DaoRead: ...
async def update_dao(db, *, dao_id, data: DaoUpdate) -> DaoRead | None: ...
async def soft_delete_dao(db, *, dao_id) -> None: ...
async def list_members(db, dao_id) -> list[DaoMember]: ...
async def add_member(db, dao_id, user_id, role) -> DaoMember: ...
async def remove_member(db, member_id) -> None: ...
async def get_treasury_items(db, dao_id) -> list[DaoTreasuryItem]: ...
```
### 3.2. `repository_proposals.py`
```python
async def list_proposals(db, dao_id: uuid.UUID) -> list[ProposalRead]: ...
async def get_proposal(db, proposal_id: uuid.UUID) -> ProposalRead | None: ...
async def get_proposal_by_slug(db, dao_id, slug) -> ProposalRead | None: ...
async def create_proposal(db, dao_id, created_by_user_id, data: ProposalCreate) -> ProposalRead: ...
async def update_proposal_status(db, proposal_id, status: str) -> ProposalRead | None: ...
```
### 3.3. `repository_votes.py`
```python
async def list_votes_for_proposal(db, proposal_id) -> list[VoteRead]: ...
async def create_or_update_vote(db, proposal_id, voter_user_id, vote_value, weight, raw_power) -> VoteRead: ...
```
---
## 4. Governance Engine
Файл: `services/dao-service/governance_engine.py`
Реалізувати три моделі:
```python
async def calculate_voting_power_simple(actor, dao: DaoRead) -> Decimal:
# 1 user = 1 голос
return Decimal(1)
async def calculate_voting_power_quadratic(actor, dao: DaoRead, base_power: Decimal) -> Decimal:
# weight = sqrt(base_power)
# base_power може бути, наприклад, кількістю токенів з treasury або окремої таблиці
from decimal import Decimal, getcontext
getcontext().prec = 28
return base_power.sqrt()
async def calculate_voting_power_delegated(actor, dao: DaoRead, delegation_graph) -> Decimal:
# з урахуванням делегацій
...
```
Також функцію **обчислення результату**:
```python
async def evaluate_proposal_outcome(
dao: DaoRead,
proposal: ProposalRead,
votes: list[VoteRead]
) -> dict:
# рахуємо total weight, yes/no/abstain, quorum, чи passed
...
```
---
## 5. NATS Events
Файл: `services/dao-service/nats_events.py`
Функція:
```python
async def publish_event(subject: str, payload: dict) -> None: ...
```
Викликати з backend:
* при створенні DAO:
`dao.event.created`
* при оновленні DAO:
`dao.event.updated`
* при оновленні treasury:
`dao.event.treasury_updated`
* при створенні пропозиції:
`dao.event.proposal_created`
* при активації/закритті пропозиції:
`dao.event.proposal_status_changed`
* при голосуванні:
`dao.event.vote_cast`
---
## 6. Routes (FastAPI)
У `services/dao-service/main.py` створити FastAPI app з router'ами:
### 6.1. Auth/PDP
Створити `auth_client.py`, `pdp_client.py` (як у інших сервісах):
```python
async def get_actor_identity(request) -> ActorIdentity: ...
async def check_permission(actor, action: str, resource: dict) -> None:
# кинути HTTPException(403) якщо deny
```
### 6.2. DAO Routes
Файл: `routes_dao.py`:
```python
router = APIRouter(prefix="/dao", tags=["dao"])
```
Endpoints:
1. `GET /dao`
* повертає DAO, де actor є членом:
* `list_dao_for_user(db, actor.user_id)`
2. `POST /dao`
* PDP: `DAO_CREATE`
* body: `DaoCreate`
* потрібно вказати `microdao_id` (як поле або через контекст)
* створюємо DAO, додаємо owner у `dao_members` з роллю `owner`.
3. `GET /dao/{slug}`
* PDP: `DAO_READ`
* повертає `DaoRead` + агреговану інформацію (можна через `DaoOverview`).
4. `PUT /dao/{slug}`
* PDP: `DAO_MANAGE`
* оновити `name/description/governance_model/...`.
5. `DELETE /dao/{slug}`
* PDP: `DAO_MANAGE`
* `is_active=false`.
### 6.3. Members Routes
`GET /dao/{slug}/members`
`POST /dao/{slug}/members`
`DELETE /dao/{slug}/members/{memberId}`
### 6.4. Treasury Routes
`GET /dao/{slug}/treasury`
`POST /dao/{slug}/treasury` (delta-операція: `token_symbol`, `delta`)
### 6.5. Proposals & Votes
`GET /dao/{slug}/proposals`
`POST /dao/{slug}/proposals`
`GET /dao/{slug}/proposals/{proposalSlug}`
`POST /dao/{slug}/proposals/{proposalSlug}/activate`
`POST /dao/{slug}/proposals/{proposalSlug}/close`
`GET /dao/{slug}/proposals/{proposalSlug}/votes`
`POST /dao/{slug}/proposals/{proposalSlug}/votes` (create/update vote)
---
## 7. Frontend: DAO Dashboard
У фронтенді створити структуру:
`src/api/dao.ts`
`src/features/dao/DaoListPage.tsx`
`src/features/dao/DaoDashboardPage.tsx`
`src/features/dao/components/...`
### 7.1. API Client
У `src/api/dao.ts`:
```ts
export async function getMyDaos(): Promise<DaoSummary[]> { ... }
export async function getDao(slug: string): Promise<DaoOverview> { ... }
export async function createDao(payload: DaoCreatePayload): Promise<DaoRead> { ... }
export async function getDaoMembers(slug: string): Promise<DaoMember[]> { ... }
export async function getDaoTreasury(slug: string): Promise<DaoTreasuryItem[]> { ... }
export async function getDaoProposals(slug: string): Promise<DaoProposal[]> { ... }
export async function createDaoProposal(slug: string, payload: ProposalCreatePayload): Promise<DaoProposal> { ... }
export async function getDaoProposal(slug: string, proposalSlug: string): Promise<DaoProposalDetail> { ... }
export async function castVote(slug: string, proposalSlug: string, payload: VotePayload): Promise<VoteRead> { ... }
```
Типи — у цьому ж файлі або в `types/dao.ts`.
### 7.2. Сторінки
#### `/dao`
`DaoListPage.tsx`:
* список DAO картками:
* назва
* опис
* governance модель
* кількість учасників, активних пропозицій
* кнопка "Створити DAO" (dialog → createDao)
#### `/dao/:slug`
`DaoDashboardPage.tsx`:
Tabs:
* Overview:
* загальна інформація
* короткі stats (members, proposals, treasury)
* Proposals:
* список пропозицій
* кнопка "Створити пропозицію"
* статуси, дедлайни
* Proposal detail:
* окрема панель (може бути drawer/side-panel або окрема сторінка)
* кнопка Vote (Yes/No/Abstain)
* показ quorum, результатів
* Treasury:
* список токенів, баланси
* простий графік
* Members:
* таблиця учасників
* роль
* Activity:
* стрічка подій DAO (з `dao_audit_log` або NATS)
---
## 8. WebSocket / Live Updates (MVP)
Опційно для цієї фази (якщо є час):
* у `dao-service`:
```python
@router.websocket("/ws/dao-events")
async def dao_events_ws(ws: WebSocket):
# підписка на NATS dao.event.* і пуш у клієнт
...
```
* на фронті `useDaoEvents(slug)`:
* фільтрувати тільки події конкретного DAO;
* оновлювати списки proposals/treasury.
Якщо часу мало — можна залишити це на наступну фазу, але місце під WS варто зарезервувати.
---
## 9. Інтеграція з microDAO Console
У `MicrodaoConsolePage.tsx` додати:
* секцію `Governance`:
* якщо для даного microDAO існує DAO → кнопка:
* "Відкрити DAO Dashboard"
* `navigate('/dao/' + daoSlug)`
* якщо не існує DAO → кнопка:
* "Створити DAO Governance"
* виклик `createDao({ microdaoId, slug, name, ... })`
(можна автогенерувати slug з microDAO slug).
---
## 10. Docker / Scripts
Оновити:
* `docker-compose.phase8.yml` (або доповнити існуючий compose):
* `dao-service` (порт, наприклад, 7016)
* `scripts/start-phase8.sh`:
* застосування `009` міграції;
* запуск `dao-service` разом з іншими сервісами.
---
## 11. Acceptance Criteria
Вважати Phase 8 виконаною, якщо:
* [ ] `009_create_dao_core.sql` застосовується без помилок;
* [ ] Запущено `dao-service` з `/health` endpoint;
* [ ] `GET /dao` повертає DAO, де actor є членом;
* [ ] `POST /dao` створює DAO, додає owner у members і публікує `dao.event.created`;
* [ ] `GET /dao/{slug}` повертає overview DAO (включно з members_count, active_proposals_count, treasury_items);
* [ ] `POST /dao/{slug}/proposals` створює пропозицію, `dao.event.proposal_created` публікується;
* [ ] `POST /dao/{slug}/proposals/{proposalSlug}/votes` створює/оновлює голос;
* [ ] у фронтенді `/dao` показує реальні DAO з БД;
* [ ] у фронтенді `/dao/:slug` показує Overview/Proposals/Treasury/Members з реальних endpoint'ів (без mock);
* [ ] PDP блокує доступ до DAO, де actor не є членом (403).
END OF TASK

View File

@@ -0,0 +1,448 @@
# TASK PHASE 9 — LIVING MAP (FULL STACK SERVICE)
Version: 1.0
Status: READY FOR IMPLEMENTATION
Scope: Backend + WebSocket + NATS + Minimal Frontend Hook
## 1. Context
DAARION уже має:
- core сервіси:
- `messaging-service` (Matrix-aware Messenger)
- `agent-runtime`, `agent-filter`, `dagi-router`
- `llm-proxy`, `memory-orchestrator`, `toolcore`
- `auth-service`, `pdp-service`, `usage-engine`
- `agents-service` (Agent Hub + lifecycle)
- `microdao-service` (microDAO Console)
- `dao-service` (DAO Dashboard)
- `city-service`, `space-service`
- інфраструктуру:
- PostgreSQL
- NATS JetStream
- docker-compose для різних фаз
- WebSocket-потоки для окремих модулів
Потрібен **єдиний "Living Map" шар**, який агрегує стан всієї мережі:
- City (microDAO, метрики)
- Space (DAO-планети, ноди)
- Nodes (ресурси, алерти)
- Agents (статус, використання)
- DAO (голосування, proposals)
- Messaging (активність каналів)
і видає це:
- через `GET /living-map/snapshot` (full-state)
- через `GET /living-map/history` (event log)
- через `GET /living-map/entities` (каталог сутностей)
- через `WS /living-map/stream` (живий потік подій).
Цей таск — **backend + API + NATS + WS + базовий frontend hook**.
---
## 2. Goals
1. Створити сервіс `living-map-service` (FastAPI), порт `7017`.
2. Зібрати стан мережі з існуючих сервісів **в один snapshot**.
3. Підписатися на ключові NATS-сабджекти й зберігати історію в `living_map_history`.
4. Віддавати:
- `snapshot` (HTTP)
- `history` (HTTP)
- `real-time stream` (WS).
5. Додати мінімальну фронтенд-обгортку `useLivingMapFull` (React hook) для інтеграції з 2D/3D UI.
---
## 3. Architecture Overview
### 3.1. New service: `living-map-service`
- Stack: Python 3 + FastAPI + uvicorn
- Port: `7017`
- Responsibilities:
- Агрегувати дані з:
- `city-service` (city snapshot)
- `space-service` (planets/nodes/events)
- `agents-service` (agents, metrics, events)
- `microdao-service` (microDAOs)
- `dao-service` (dao, proposals, votes)
- `usage-engine` (LLM/tool usage summary)
- Нормалізувати в **єдину структуру сцени**:
- `scene.layers.city`
- `scene.layers.space`
- `scene.layers.nodes`
- `scene.layers.agents`
- `scene.meta` (timestamps, version)
- Підписуватись на NATS-subject'и й створювати event log.
- Видавати snapshot/history + WebSocket stream.
### 3.2. Data sources (HTTP)
Очікувані існуючі/доступні ендпоінти (можна створити прості адаптери, якщо їх ще нема):
- `city-service`:
- `GET http://city-service:7001/api/city/snapshot`
- `space-service`:
- `GET http://space-service:7002/api/space/scene` або окремо planets/nodes/events
- `agents-service`:
- `GET http://agents-service:7014/agents` (list)
- `GET http://agents-service:7014/agents/metrics` (summary)
- `microdao-service`:
- `GET http://microdao-service:7015/microdaos` (list)
- `dao-service`:
- `GET http://dao-service:7016/dao` (list)
- `GET http://dao-service:7016/dao/proposals/summary`
- `usage-engine`:
- `GET http://usage-engine:7013/internal/usage/summary?period_hours=24`
Якщо чогось немає — зробити простий adapter/placeholder з mock даними всередині `living-map-service`.
### 3.3. NATS Subjects
Потрібно підписатися на:
- `city.event.*`
- `dao.event.*`
- `microdao.event.*`
- `node.metrics.*`
- `agent.event.*`
- `usage.llm.*`
- `usage.agent.*`
- `messaging.message.created`
(Якщо частина сабджектів ще не існує — підготувати consumer з graceful handling та вимкненими/placeholder subscriptions.)
---
## 4. API Specification
### 4.1. `GET /living-map/health`
Простий health-check.
```json
{
"status": "ok",
"service": "living-map-service",
"version": "1.0.0",
"time": "2025-11-24T12:34:56Z"
}
```
### 4.2. `GET /living-map/snapshot`
Агрегований стан усієї мережі.
Response (спрощений приклад):
```json
{
"generated_at": "2025-11-24T12:34:56Z",
"layers": {
"city": {
"microdaos_total": 12,
"active_users": 57,
"active_agents": 34,
"health": "green",
"items": [
{
"id": "microdao:7",
"slug": "daarion-city",
"name": "DAARION City",
"status": "active",
"agents": 9,
"nodes": 3
}
]
},
"space": {
"planets": [
{
"id": "dao:daarion-core",
"name": "DAARION CORE",
"type": "dao",
"orbits": ["node:gpu-1", "node:gpu-2"],
"status": "active"
}
],
"nodes": [
{
"id": "node:gpu-1",
"name": "NODE1",
"cpu": 0.42,
"gpu": 0.77,
"memory": 0.63,
"alerts": []
}
]
},
"nodes": {
"items": [
{
"id": "node:gpu-1",
"microdao_id": "microdao:7",
"status": "online",
"metrics": {
"cpu": 0.42,
"gpu": 0.77,
"ram": 0.63,
"net_in": 12345,
"net_out": 9876
}
}
]
},
"agents": {
"items": [
{
"id": "agent:sofia",
"name": "Sofia",
"kind": "system",
"microdao_id": "microdao:7",
"status": "online",
"usage": {
"llm_calls_24h": 123,
"tokens_24h": 45678
}
}
]
}
},
"meta": {
"source_services": [
"city-service",
"space-service",
"agents-service",
"microdao-service",
"dao-service",
"usage-engine"
]
}
}
```
### 4.3. `GET /living-map/entities`
Плоский список сутностей з мінімальними даними для побудови legend/списків.
Query params:
* `type` (optional): `city|space|node|agent|dao|microdao|channel`
* `limit` (optional, default 100)
Response:
```json
{
"items": [
{
"id": "microdao:7",
"type": "microdao",
"label": "DAARION City",
"status": "active",
"layer": "city"
},
{
"id": "dao:daarion-core",
"type": "dao",
"label": "DAARION CORE DAO",
"status": "active",
"layer": "space"
}
]
}
```
### 4.4. `GET /living-map/entities/{id}`
Детальний опис сутності (проксі до відповідного сервісу з нормалізацією).
Response (узагальнений):
```json
{
"id": "agent:sofia",
"type": "agent",
"layer": "agents",
"data": {
"...": "raw or normalized fields"
}
}
```
### 4.5. `GET /living-map/history`
Історія подій Living Map.
Query params:
* `since` (optional, ISO datetime)
* `limit` (optional, default 200)
Response:
```json
{
"items": [
{
"id": "evt-uuid",
"timestamp": "2025-11-24T12:30:00Z",
"event_type": "node.metrics.update",
"payload": {
"node_id": "node:gpu-1",
"cpu": 0.8,
"gpu": 0.95
}
}
]
}
```
### 4.6. `WS /living-map/stream`
WebSocket-потік:
* Типи повідомлень:
* `snapshot` — повний стан (на підключення та за потреби)
* `event` — одинична подія
* Формат:
```json
{
"kind": "event",
"event_type": "agent.event.status",
"timestamp": "2025-11-24T12:34:56Z",
"payload": { ... }
}
```
---
## 5. Database Schema
Створити міграцію, наприклад: `migrations/010_create_living_map_tables.sql`.
```sql
CREATE TABLE IF NOT EXISTS living_map_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp timestamptz NOT NULL DEFAULT now(),
event_type TEXT NOT NULL,
payload JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_living_map_history_timestamp
ON living_map_history (timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_living_map_history_event_type
ON living_map_history (event_type);
```
---
## 6. Implementation Plan
### 6.1. Files to create
* `services/living-map-service/main.py`
* `services/living-map-service/models.py`
* `services/living-map-service/repository_history.py`
* `services/living-map-service/adapters/city_client.py`
* `services/living-map-service/adapters/space_client.py`
* `services/living-map-service/adapters/agents_client.py`
* `services/living-map-service/adapters/microdao_client.py`
* `services/living-map-service/adapters/dao_client.py`
* `services/living-map-service/adapters/usage_client.py`
* `services/living-map-service/nats_subscriber.py`
* `services/living-map-service/ws_stream.py`
* `services/living-map-service/Dockerfile`
* `services/living-map-service/requirements.txt`
* `docker-compose.phase9.yml` (або оновити загальний)
* `scripts/start-phase9.sh`
* `scripts/stop-phase9.sh`
* `migrations/010_create_living_map_tables.sql`
* Frontend:
* `src/features/livingMap/hooks/useLivingMapFull.ts`
### 6.2. Minimal frontend hook
`useLivingMapFull.ts`:
* `GET /living-map/snapshot` → зберігає state
* Підключає WS `/living-map/stream`
* При `kind=event` оновлює локальний state (immutable update)
* Повертає:
* `snapshot`
* `events`
* `isLoading`
* `error`
* `connectionStatus`
---
## 7. Security / Auth
* Всі HTTP-ендпоінти:
* Перевірка JWT/Session через `auth-service` (ActorContext).
* Опціонально, `GET /living-map/snapshot` може мати:
* `public_mode` (спрощений, анонімний)
* або вимагати auth (рекомендовано).
* WS:
* `Authorization: Bearer <token>` у заголовках.
* NATS:
* лише internal subjects, використовувати існуючий NATS connection з параметрами як в інших сервісах.
---
## 8. TODO Checklist
Backend:
* [ ] Створити міграцію `010_create_living_map_tables.sql`
* [ ] Створити `living-map-service` структуру
* [ ] Реалізувати адаптери до інших сервісів (з timeout/retry)
* [ ] Реалізувати `GET /living-map/health`
* [ ] Реалізувати `GET /living-map/snapshot`
* [ ] Реалізувати `GET /living-map/entities`
* [ ] Реалізувати `GET /living-map/entities/{id}`
* [ ] Реалізувати `GET /living-map/history`
* [ ] Реалізувати `WS /living-map/stream`
* [ ] Реалізувати `nats_subscriber.py` для key subjects
* [ ] Інтегрувати Auth/PDP (як у інших сервісах)
* [ ] Додати Dockerfile + requirements.txt
* [ ] Оновити docker-compose/script'и
* [ ] Додати базові unit tests (snapshot builder)
Frontend:
* [ ] Створити `useLivingMapFull.ts`
* [ ] Протестувати запит snapshot + WS stream (можна через тимчасовий debug-компонент)
---
## 9. Acceptance Criteria
1. `docker-compose -f docker-compose.phase9.yml up -d` піднімає `living-map-service` без помилок.
2. `GET /living-map/health` повертає `status=ok`.
3. `GET /living-map/snapshot` повертає валідний JSON з `layers.city`, `layers.space`, `layers.nodes`, `layers.agents`.
4. `GET /living-map/history` повертає список подій, які приходять з NATS.
5. `WS /living-map/stream` надсилає:
* при підключенні: `kind="snapshot"`
* далі: `kind="event"` при нових подіях.
6. `useLivingMapFull` успішно підключається до API+WS і оновлює локальний state без TypeScript помилок.
7. Уся нова логіка проходить лінтер та тести.
---
END OF TASK

View File

@@ -0,0 +1,387 @@
# TASK PHASE 9 — LIVING MAP (LITE 2D UI)
Version: 1.0
Status: READY FOR IMPLEMENTATION
Scope: Frontend-Only 2D Interactive Map (React + Canvas)
## 1. Context
Існує або буде реалізовано `living-map-service` (Phase 9 FULL):
- `GET /living-map/snapshot`
- `WS /living-map/stream`
Цей таск — чисто **UI/Frontend**, який:
- візуалізує стан мережі DAARION у вигляді 2D карти,
- дає змогу перемикатися між шарами (City / Space / Nodes / Agents),
- показує базові стани (online/offline, load, alerts),
- реагує на живі події (WS).
Цей 2D UI має працювати **без 3D/Three.js**, тільки React + Canvas.
---
## 2. Goals
1. Створити 2D "Living Map" сторінку `/living-map`.
2. Зробити Canvas-рендеринг 4 шарів:
- City layer (microDAO як "райони міста")
- Space layer (DAO-планети, орбіти нод)
- Nodes layer (ноди, їх завантаженість)
- Agents layer (агенти як точки/іконки)
3. Підключити `useLivingMapFull` (з FULL таску) або окремий `useLivingMapLite`.
4. Забезпечити:
- панель шарів (Layer switcher),
- клік по сутності → панель деталей справа,
- zoom/pan базового рівня.
---
## 3. UI Structure
### 3.1. Routes
У `src/App.tsx`:
- Додати route:
- `/living-map``LivingMapPage`.
### 3.2. Files (Frontend)
Створити:
```text
src/features/livingMap/
├── LivingMapPage.tsx
├── hooks/useLivingMapLite.ts # або reuse useLivingMapFull
├── components/LivingMapCanvas.tsx
├── components/LayerSwitcher.tsx
├── components/EntityDetailsPanel.tsx
├── mini-engine/canvasRenderer.ts
└── mini-engine/layoutEngine.ts
```
---
## 4. Data Contract (UI Level)
Очікуваний формат snapshot (узгоджений з FULL таском):
```ts
type LivingMapSnapshot = {
generated_at: string;
layers: {
city: {
items: Array<{
id: string;
slug: string;
name: string;
status: "active" | "inactive" | "warning";
agents: number;
nodes: number;
}>;
};
space: {
planets: Array<{
id: string;
name: string;
type: "dao" | "platform" | "other";
status: "active" | "inactive" | "warning";
orbits: string[];
}>;
nodes: Array<{
id: string;
name: string;
status: "online" | "offline" | "warning";
cpu: number;
gpu: number;
}>;
};
nodes: {
items: Array<{
id: string;
microdao_id: string | null;
status: "online" | "offline" | "warning";
metrics: {
cpu: number;
gpu: number;
ram: number;
};
}>;
};
agents: {
items: Array<{
id: string;
name: string;
kind: string;
microdao_id: string | null;
status: "online" | "offline" | "idle";
usage: {
llm_calls_24h: number;
tokens_24h: number;
};
}>;
};
};
};
```
Якщо backend ще не повністю готовий — у hook'у передбачити fallback з mock-даними.
---
## 5. Hook: `useLivingMapLite`
Мета: інкапсулювати логіку:
* HTTP-запит snapshot
* WebSocket-підписка
* merge подій у локальний state
### 5.1. API
```ts
type UseLivingMapLiteResult = {
snapshot: LivingMapSnapshot | null;
isLoading: boolean;
error: string | null;
connectionStatus: "connecting" | "open" | "closed" | "error";
selectedLayer: "city" | "space" | "nodes" | "agents";
setSelectedLayer: (layer: "city" | "space" | "nodes" | "agents") => void;
selectedEntityId: string | null;
setSelectedEntityId: (id: string | null) => void;
};
```
### 5.2. Поведінка
* При mount:
* `GET /living-map/snapshot`
* після успіху — зберегти в `snapshot`
* відкрити WS `/living-map/stream`
* На WS повідомлення:
* якщо `kind="event"`:
* оновлювати відповідні `layers.*` immutable-способом
* При помилках:
* виставити `error`
* обережний reconnect (наприклад, через 510 сек).
---
## 6. Canvas Rendering
### 6.1. `LivingMapCanvas.tsx`
Компонент:
```tsx
interface LivingMapCanvasProps {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
onSelectEntity: (id: string | null) => void;
}
export function LivingMapCanvas(props: LivingMapCanvasProps) {
// створює <canvas>, підключає canvasRenderer
}
```
* Використати `useRef<HTMLCanvasElement>` + `useEffect`.
* Передавати в `canvasRenderer`:
* `snapshot`
* `selectedLayer`
* `selectedEntityId`
* `onSelectEntity`
* внутрішній state zoom/pan (можна зберігати тут або в hook'у).
### 6.2. `mini-engine/canvasRenderer.ts`
Експортувати функцію:
```ts
export function createLivingMapRenderer(opts: {
canvas: HTMLCanvasElement;
getState: () => {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
zoom: number;
offsetX: number;
offsetY: number;
};
onSelectEntity: (id: string | null) => void;
}) {
// 1) ініціалізація контексту
// 2) підписка на mouse events
// 3) основний render loop (requestAnimationFrame)
}
```
Проста логіка:
* Layer `"city"`:
* Рендерити прямокутники/кластери для кожного microDAO.
* Layer `"space"`:
* Кола/"орбіти" для DAO-планет, ноди — точки на орбітах.
* Layer `"nodes"`:
* Квадрати/іконки нод, колір залежить від `status` + bar для `cpu/gpu`.
* Layer `"agents"`:
* Маленькі точки/іконки, колір за статусом, розмір за `usage.tokens_24h`.
### 6.3. `mini-engine/layoutEngine.ts`
Нехай вміщає функції:
```ts
export function layoutCityLayer(/* items */) { /* x,y,w,h для кожного microDAO */ }
export function layoutSpaceLayer(/* planets, nodes */) { /* координати */ }
export function layoutNodesLayer(/* nodes */) { /* grid/cluster layout */ }
export function layoutAgentsLayer(/* agents */) { /* grid / spiral / random seeded */ }
```
Координати зберігати в локальному мапінгу (наприклад, `Map<entityId, {x,y,w,h}>`).
---
## 7. UI Components
### 7.1. `LayerSwitcher.tsx`
Простий компонент:
```tsx
interface LayerSwitcherProps {
value: "city" | "space" | "nodes" | "agents";
onChange: (v: "city" | "space" | "nodes" | "agents") => void;
}
export function LayerSwitcher(props: LayerSwitcherProps) {
// 4 кнопки / pills / segmented control
}
```
### 7.2. `EntityDetailsPanel.tsx`
Показує деталі обраної сутності:
```tsx
interface EntityDetailsPanelProps {
snapshot: LivingMapSnapshot | null;
selectedLayer: "city" | "space" | "nodes" | "agents";
selectedEntityId: string | null;
}
export function EntityDetailsPanel(props: EntityDetailsPanelProps) {
// шукає entity у відповідному layer
// показує name, type, status, basic metrics
// опційно: кнопки "Open Agent Hub", "Open microDAO Console", "Open DAO"
}
```
---
## 8. `LivingMapPage.tsx`
Складає все разом:
* Layout:
* Ліворуч — Canvas (70% ширини)
* Праворуч — панель з:
* LayerSwitcher
* Connection status (WS)
* EntityDetailsPanel
* Підключає `useLivingMapLite`.
Псевдокод:
```tsx
export function LivingMapPage() {
const {
snapshot,
isLoading,
error,
connectionStatus,
selectedLayer,
setSelectedLayer,
selectedEntityId,
setSelectedEntityId,
} = useLivingMapLite();
return (
<div className="flex h-full">
<div className="flex-1">
<LivingMapCanvas
snapshot={snapshot}
selectedLayer={selectedLayer}
selectedEntityId={selectedEntityId}
onSelectEntity={setSelectedEntityId}
/>
</div>
<div className="w-96 border-l flex flex-col">
<LayerSwitcher value={selectedLayer} onChange={setSelectedLayer} />
{/* status + errors */}
<EntityDetailsPanel
snapshot={snapshot}
selectedLayer={selectedLayer}
selectedEntityId={selectedEntityId}
/>
</div>
</div>
);
}
```
---
## 9. TODO Checklist
* [ ] Додати route `/living-map` в `App.tsx`.
* [ ] Створити папку `src/features/livingMap/`.
* [ ] Реалізувати `useLivingMapLite` (або обгорнути `useLivingMapFull`).
* [ ] Створити `LivingMapPage.tsx`.
* [ ] Створити `LivingMapCanvas.tsx`.
* [ ] Реалізувати `canvasRenderer.ts` з базовим рендером:
* [ ] city layer
* [ ] space layer
* [ ] nodes layer
* [ ] agents layer
* [ ] Реалізувати `layoutEngine.ts`.
* [ ] Додати `LayerSwitcher.tsx` (простий UI).
* [ ] Додати `EntityDetailsPanel.tsx`.
* [ ] Підключити WebSocket stream (якщо backend вже готовий).
* [ ] Додати fallback на mock-дані, якщо API недоступний.
* [ ] Переконатись, що немає TypeScript/lint помилок.
---
## 10. Acceptance Criteria
1. Route `/living-map` доступний у UI.
2. При відкритті сторінки:
* робиться запит `GET /living-map/snapshot` (або використовується mock),
* на Canvas зʼявляються базові форми (місто/космос/ноди/агенти).
3. LayerSwitcher перемикає режим рендерингу між `city`, `space`, `nodes`, `agents`.
4. Клік по елементу на Canvas змінює `selectedEntityId` і панель деталей показує правильні дані.
5. WebSocket (якщо активний) змінює стан (наприклад, статус ноди, агента) без перезавантаження сторінки.
6. FPS достатній (без явних лагів на базовому обсязі даних).
7. Код компілюється без TypeScript та ESLint помилок.
---
END OF TASK

View File

@@ -0,0 +1,140 @@
# TASK_PHASE_AGENTS_CORE.md
DAARION — AGENTS CORE IMPLEMENTATION
## 0. Ціль
Реалізувати ядро агентної системи:
- agent registry
- agent filtering
- agent.invoke → agent.reply
- NATS subjects
- quota & rate-limits
- agent-runs logging
- agent console API
- agent presence
Все на основі Data Model & Event Catalog.
---
## 1. Структура
```
services/
agents/
agent_filter.py
agent_router.py
agent_executor.py
models.py
schemas.py
routes.py
nats/
subjects.py
publisher.py
subscriber.py
```
---
## 2. NATS Subjects
### Publish:
- `integration.matrix.message` (stub)
- `agents.invoke`
- `agents.reply`
- `agents.error`
- `agents.telemetry`
- `agents.runs.created`
- `agents.runs.finished`
### Subscribe:
- `message.created`
- `task.created`
- `event.user.action`
---
## 3. agent_filter.py
Функції:
```python
def filter_message(message):
# - detect spam
# - detect commands
# - route to agent if needed
# - block restricted content
pass
```
Вихід:
- "allow"
- "deny"
- "agent:<agentId>"
---
## 4. agent_router.py
Логіка:
```python
def route(agent_id, payload):
# publish to NATS -> agents.invoke
pass
```
---
## 5. agent_executor.py
Stub реалізація:
- виклик локальної LLM (gRPC/HTTP)
- timeout
- tokens count
- save run in DB
---
## 6. API endpoints
### POST `/agents/{id}/invoke`
### GET `/agents/{id}/runs`
### GET `/agents/{id}`
---
## 7. Quotas
limits:
- tokens/minute
- runs/day
- users/day
---
## 8. Acceptance Criteria
- агент отримує запит
- агент відповідає через WS
- runs логуються
- quotas працюють
- agent_filter перехоплює підозрілі повідомлення
- NATS події працюють
---
## 9. Команда до Cursor
**"Реалізувати Agents Core згідно TASK_PHASE_AGENTS_CORE.md.
Створити всі файли й інтеграцію з NATS."**

View File

@@ -0,0 +1,497 @@
# TASK_PHASE_CITY_BACKEND_FINISHER.md
DAARION CITY — Backend Completion for Phase 3 (MVP)
Цей таск **закриває City Backend** до рівня, коли MVP можна деплоїти на сервер (daarion.space) і реально користуватись:
- Public Rooms (міські кімнати)
- Presence System (онлайн-статуси)
- Second Me (персональний агент MVP)
- City Home інтеграція (дані для дашборду міста)
Фронтенд уже реалізований (CityRoomsPage, SecondMePage, PresenceBar тощо),
цей таск — про **backend-реалізацію API + WS + Redis + DB + інтеграцію з Agents Core**.
---
## 0. База / припущення
1. Primary DB: **PostgreSQL** (той самий, що й для microdao).
2. Cache / presence: **Redis** (ok додати новий контейнер або використовувати існуючий).
3. Message bus: **NATS JetStream** (вже є для Agents Core).
4. HTTP API gateway: уже налаштований (`/api/...`, `/ws/...`), ти додаєш нові маршрути.
5. Існує **Agents Core** з endpoints `/agents/{id}/invoke` і NATS-темами `agents.invoke` / `agents.reply`.
---
## 1. Структура Backend-модулів City
Створити (або доповнити, якщо частково вже є):
```text
services/
city-service/
__init__.py
models.py
schemas.py
routes_city.py
ws_city.py
presence.py
feed.py
rooms.py
repo.py
secondme-service/
__init__.py
models.py
schemas.py
routes_secondme.py
service_secondme.py
common/
redis_client.py # якщо ще немає
```
І підключити:
* `routes_city.py` і `routes_secondme.py` до основного `main.py` (або відповідного API-aggregator service).
* `ws_city.py` — до WebSocket router'а (`/ws/...`).
---
## 2. PostgreSQL: нові таблиці
### 2.1 Таблиця `city_rooms`
```sql
create table city_rooms (
id text primary key, -- room_id, напр. "room_city_general"
slug text not null unique, -- "general", "science"
name text not null, -- "General", "Science"
description text null,
is_default boolean not null default false,
created_at timestamptz not null default now(),
created_by text null -- user_id (u_*)
);
create index ix_city_rooms_slug on city_rooms(slug);
```
### 2.2 Таблиця `city_room_messages`
```sql
create table city_room_messages (
id text primary key, -- ksuid/ulid, префікс m_city_
room_id text not null references city_rooms(id) on delete cascade,
author_user_id text null, -- u_*
author_agent_id text null, -- ag_*
body text not null,
created_at timestamptz not null default now()
);
create index ix_city_room_messages_room_time on city_room_messages(room_id, created_at desc);
```
### 2.3 Таблиця `city_feed_events`
```sql
create table city_feed_events (
id text primary key, -- evt_city_*
kind text not null, -- 'room_message','agent_reply','system'
room_id text null references city_rooms(id) on delete set null,
user_id text null,
agent_id text null,
payload jsonb not null,
created_at timestamptz not null default now()
);
create index ix_city_feed_time on city_feed_events(created_at desc);
```
### 2.4 Таблиця `secondme_sessions` (історія Second Me)
```sql
create table secondme_sessions (
id text primary key, -- smsess_*
user_id text not null, -- u_*
created_at timestamptz not null default now()
);
create table secondme_messages (
id text primary key, -- smmsg_*
session_id text not null references secondme_sessions(id) on delete cascade,
user_id text not null,
role text not null check (role in ('user','assistant')),
content text not null,
created_at timestamptz not null default now()
);
create index ix_secondme_messages_session_time on secondme_messages(session_id, created_at desc);
```
Для MVP: можна використовувати **одну активну session per user** (останню).
---
## 3. Redis: Presence System
Використати Redis як KV-store для онлайн-присутності:
* key: `presence:user:{user_id}` → value: `"online"`
* TTL: 40 секунд
* WS heartbeat кожні 20 секунд оновлює TTL
### Redis-клієнт
`common/redis_client.py`:
```python
import aioredis
from typing import Optional
_redis = None
async def get_redis() -> aioredis.Redis:
global _redis
if _redis is None:
_redis = await aioredis.from_url(
os.getenv("REDIS_URL", "redis://redis:6379/0"),
encoding="utf-8",
decode_responses=True,
)
return _redis
```
---
## 4. HTTP API — City Rooms / Feed
### 4.1 Маршрути (routes_city.py)
Base prefix: **`/city`**.
#### GET `/city/rooms`
* Повертає список всіх кімнат.
* Query params: (optional) `limit`, `offset`.
* Response:
```json
[
{
"id": "room_city_general",
"slug": "general",
"name": "General",
"description": "Головна кімната міста",
"members_online": 42,
"last_event": "2025-11-23T10:15:00Z"
}
]
```
`members_online` рахувати через Redis:
* keys: `presence:user:*` → map users → rooms (див. нижче в Presence).
Для MVP можна:
* рахувати `members_online` приблизно: число унікальних `presence:user:*` (спрощено),
* або додати key `presence:room:{room_id}` (більш точно).
#### POST `/city/rooms`
Body:
```json
{
"name": "Science",
"slug": "science",
"description": "Наукова кімната"
}
```
* Генерує `id = room_city_{slug}`.
* Створює запис у `city_rooms`.
* Віддає створену кімнату.
#### GET `/city/rooms/{room_id}`
Returns:
* room meta
* останні 50 повідомлень
* приблизний `members_online`
```json
{
"room": {
"id": "room_city_general",
"name": "General",
"description": "Головна кімната міста"
},
"messages": [
{
"id": "m_city_...",
"author_user_id": "u_123",
"author_agent_id": null,
"body": "Привіт місто!",
"created_at": "..."
}
],
"members_online": 12
}
```
#### POST `/city/rooms/{room_id}/messages`
* Body:
```json
{
"body": "Текст повідомлення"
}
```
* Запис у `city_room_messages`.
* Запис у `city_feed_events` з kind = `"room_message"`.
* Публікація WS event (див. WS нижче).
* Повертає створене повідомлення.
#### GET `/city/feed`
* Повертає останні N (наприклад, 20) подій:
```json
[
{
"id": "evt_city_...",
"kind": "room_message",
"room_id": "room_city_general",
"user_id": "u_123",
"payload": {"body": "Текст...", "snippet": "..."},
"created_at": "..."
}
]
```
---
## 5. WebSocket — City Rooms + Presence
### 5.1 City Rooms WS (`ws_city.py`)
Шлях (через already existing WS server):
```
/ws/city/rooms/{room_id}
```
Події (JSON):
* Вхідні від клієнта:
```json
{ "event": "room.join", "room_id": "room_city_general" }
{ "event": "room.leave", "room_id": "room_city_general" }
{ "event": "room.message.send", "room_id": "...", "body": "..." }
```
* Вихідні до клієнтів:
```json
{ "event": "room.message", "room_id": "...", "message": { ... } }
{ "event": "room.join", "room_id": "...", "user_id": "u_123" }
{ "event": "room.leave", "room_id": "...", "user_id": "u_123" }
{ "event": "room.presence", "room_id": "...", "user_id": "u_123", "status": "online" }
```
При `room.message.send`:
1. зберегти в DB (`city_room_messages`)
2. зберегти в `city_feed_events`
3. розіслати WS подію `room.message` усім підписникам
### 5.2 Presence WS (`/ws/city/presence`)
* Вхідні:
```json
{ "event": "presence.heartbeat", "user_id": "u_123" }
```
Обробка:
1. `SETEX presence:user:u_123 "online" 40`
2. broadcast (опційно) `presence.update`:
```json
{ "event": "presence.update", "user_id": "u_123", "status": "online" }
```
3. Періодично (background task, наприклад кожні 30 сек):
* сканувати `presence:user:*`,
* якщо TTL минув (Redis сам видаляє keys), ws-клієнтам можна розіслати `presence.update` зі статусом `"offline"` (або робити lazy-оновлення при наступних запитах).
---
## 6. Backend для Second Me
### 6.1 Service logic (`secondme-service/service_secondme.py`)
Функції:
```python
async def get_or_create_session(user_id: str) -> SecondMeSession:
# бере останню сесію або створює нову
async def get_last_messages(user_id: str, limit: int = 5) -> list[SecondMeMessage]:
# повертає останні 5 повідомлень з secondme_messages
async def invoke_second_me(user_id: str, prompt: str) -> SecondMeMessage:
# 1. get_or_create_session
# 2. зберегти user-повідомлення
# 3. зібрати короткий контекст (останні N повідомлень)
# 4. викликати Agents Core:
# POST /agents/{second_me_agent_id}/invoke
# або NATS publish agents.invoke
# 5. зберегти assistant-відповідь
# 6. повернути відповідь
```
`second_me_agent_id` поки можна:
* або hardcode (один глобальний Second Me agent),
* або зберігати у таблиці `users` / `agents` як поле.
Для MVP — допустимо hardcode у конфігу.
### 6.2 API (`routes_secondme.py`)
#### POST `/secondme/invoke`
Body:
```json
{ "prompt": "..." }
```
З HTTP-контексту брати `user_id` (із JWT).
Викликати `invoke_second_me`, повернути:
```json
{
"reply": "Текст відповіді",
"history": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
}
```
#### GET `/secondme/history`
Query: (optional) `limit`, default 5.
Повернути:
```json
[
{"role": "user", "content": "...", "created_at": "..."},
{"role": "assistant", "content": "...", "created_at": "..."}
]
```
---
## 7. Інтеграція з Agents Core
Для Second Me використати стандартний шлях:
* або **HTTP-level**:
```text
POST /agents/{second_me_agent_id}/invoke
{
"input": "...prompt+context...",
"context": { "user_id": "...", "kind": "secondme" }
}
```
* або **NATS** (якщо вже зручно):
```json
{
"event": "agents.invoke",
"agent_id": "ag_secondme",
"payload": {
"input": "...",
"user_id": "u_123",
"source": "secondme"
}
}
```
Для MVP допустимо використати HTTP-виклик до Agents Core service.
---
## 8. Конфігурація (ENV)
Додати змінні:
```env
REDIS_URL=redis://redis:6379/0
SECONDME_AGENT_ID=ag_secondme_global
CITY_DEFAULT_ROOMS=general,welcome,builders
```
При старті `city-service`:
* якщо `CITY_DEFAULT_ROOMS` порожній → створити дефолтні кімнати:
* `room_city_general` ("General")
* `room_city_welcome` ("Welcome")
* `room_city_builders` ("Builders")
---
## 9. Acceptance Criteria
### 9.1 Public Rooms
* `GET /city/rooms` повертає список кімнат.
* `POST /city/rooms` створює кімнату, видно в фронтенді.
* `GET /city/rooms/{room_id}` повертає кімнату та останні повідомлення.
* `POST /city/rooms/{room_id}/messages`:
* зберігає повідомлення в DB,
* відправляє WS-івент `room.message`,
* додає запис у `city_feed_events`.
### 9.2 Presence
* при підключенні фронтенду до `/ws/city/presence` і надсиланні `presence.heartbeat`:
* Redis має ключ `presence:user:<user_id>` із TTL ~40с;
* інші клієнти отримують `presence.update` (online).
* при припиненні heartbeat ключ зникає → статус offline (або lazily оновлюється).
### 9.3 Second Me
* `POST /secondme/invoke`:
* робить виклик до Agents Core,
* повертає текст відповіді,
* історія зберігається у `secondme_messages`.
* `GET /secondme/history` повертає останні N записів.
### 9.4 City Home
* `GET /city/feed` повертає події (room messages мінімум).
* Frontend City Home (вже реалізований) отримує всі необхідні дані через API.
---
## 10. Команда до Cursor
**"Реалізувати backend для City MVP згідно TASK_PHASE_CITY_BACKEND_FINISHER.md.
Створити city-service (rooms, feed, presence) та secondme-service.
Інтегрувати з Redis (presence) та Agents Core (Second Me).
Не змінювати існуючий фронтенд, тільки підключити API."**

View File

@@ -0,0 +1,281 @@
# TASK_PHASE_CITY_FINISHER.md
DAARION CITY — FINISHER (Phase 3 Completion)
Цей таск завершує Phase 3 (City MVP).
Ти маєш додати 3 ключові компоненти:
1. Public Rooms (API + WS + UI)
2. Full Presence System (WS heartbeats + user statuses)
3. Second Me (MVP stub: простий агент із короткою пам'яттю)
---
# 0. Цілі
Після виконання цього таску DAARION City має:
✔ Глобальні публічні кімнати міста
✔ Живий статус онлайн-користувачів
✔ Прості персональні агенти Second Me
✔ Готовність до подальшої інтеграції Matrix (без самого Matrix)
---
# 1. PUBLIC ROOMS (Міські кімнати)
## 1.1 Backend API
Створити endpoints в `services/city-service`:
### GET `/city/rooms`
Повертає список всіх кімнат:
```json
[
{
"id": "city_general",
"name": "General",
"members_online": 42,
"last_event": "...",
"description": "Головна кімната міста"
}
]
```
### POST `/city/rooms`
Створення нової публічної кімнати:
```json
{
"name": "Science",
"description": "Наукова спільнота"
}
```
### GET `/city/rooms/{id}`
Повертає:
- метадані кімнати
- останні 50 повідомлень
- онлайн-учасників
### POST `/city/rooms/{id}/messages`
Додати повідомлення в кімнату.
---
## 1.2 WebSocket
WS канал:
```
/ws/city/rooms/{roomId}
```
Події:
```
city.room.message
city.room.join
city.room.leave
city.room.presence
```
---
## 1.3 Frontend UI
Додати папку:
```
src/features/city/rooms/
```
Компоненти:
- `CityRoomsPage.tsx`
- `CityRoomView.tsx`
- `CityRoomMessageList.tsx`
- `CityRoomInput.tsx`
Функції:
- Лістинг кімнат
- Стан онлайн-учасників
- Чат у реальному часі
- Тайпінг індикатор (optional)
- Автоскрол останніх повідомлень
---
# 2. PRESENCE SYSTEM (Повноцінна система присутності)
## 2.1 Backend
WS канал:
```
/ws/city/presence
```
Кожні 20 секунд фронтенд надсилає heartbeat:
```json
{
"event": "presence.heartbeat",
"user_id": "u_123"
}
```
Backend:
- оновлює Redis key: `presence:u_123 = online`
- TTL = 40 секунд
- публікує у WS:
```json
{
"event": "presence.update",
"user_id": "...",
"status": "online|offline"
}
```
---
## 2.2 Frontend
Додати:
```
src/lib/presence.ts
```
Функції:
- підключення до WS `/ws/city/presence`
- кожні 20 секунд надсилати heartbeat
- локальна мапа `userId → status`
- глобальний presence store (Zustand)
---
## 2.3 Presence UI
Використати:
```
src/features/city/presence/PresenceBar.tsx
```
- список активних юзерів
- аватари
- статуси (online/offline)
- індикатор "active now"
---
# 3. SECOND ME (MVP)
Мінімальний персональний агент:
- коротка пам'ять (останні 5 interaction logs)
- LLM-виклик через agents core (`/agents/{id}/invoke`)
- UI з 1 полем вводу
- контекст: userId + останні 5 фраз
---
## 3.1 Backend API
Створити сервіс:
```
services/secondme-service/
```
Endpoints:
### POST `/secondme/invoke`
```json
{
"prompt": "Порадь ідею для міського району",
"user_id": "u_123"
}
```
Backend:
1. знаходить SecondMe agent user'а
2. дає короткий контекст
3. викликає `/agents/{id}/invoke`
4. зберігає відповідь
5. повертає JSON з відповіддю
### GET `/secondme/history?user_id=...`
Повертає 5 останніх interaction entries.
---
## 3.2 Frontend UI
Додати:
```
src/features/secondme/SecondMePage.tsx
src/features/secondme/SecondMeChat.tsx
```
Функції:
- поле вводу prompt
- вивід LLM-відповіді
- горизонтальна стрічка історії
- кнопка "clear history"
---
# 4. РОЗШИРЕННЯ CITY HOME
На головній сторінці міста показати:
- кількість online мешканців (із presence)
- останні 3 події Feed
- 3 популярні кімнати
- швидкий доступ до Second Me (CTA)
---
# 5. Acceptance Criteria
## Public Rooms
✔ Створення, лістинг, перегляд
✔ Чат працює через WS
✔ Online members оновлюються в реальному часі
## Presence System
✔ Heartbeats працюють
✔ Redis TTL працює
✔ PresenceBar показує онлайн-юзерів
✔ Оновлення статусів через WS
## Second Me MVP
✔ prompt → відповідь
✔ історія зберігається
✔ UI працює на окремій сторінці
## City Home
✔ всі нові елементи працюють
✔ загальний досвід живого "міста"
---
# 6. Команда для Cursor
**"Завершити Phase 3 згідно TASK_PHASE_CITY_FINISHER.md.
Додати Public Rooms, Presence System, Second Me.
Працювати в існуючих сервісах і фронтенді."**

View File

@@ -0,0 +1,120 @@
# TASK_PHASE_CITY_MVP.md
DAARION CITY — MVP
## 0. Ціль
Створити перший живий MVP DAARION City:
- City Home
- Public Rooms (райони)
- City Feed
- Presence System
- Second Me (stub)
- Living Map (2D JSON)
---
## 1. Структура фронту
```
app/city/
layout.tsx
page.tsx
feed/
rooms/
presence/
second-me/
map/
```
---
## 2. API endpoints (створити або доопрацювати)
### Public Rooms:
- GET `/city/rooms`
- POST `/city/rooms`
- GET `/city/rooms/{id}`
### City Feed:
- GET `/city/feed`
- POST `/city/feed`
### Presence:
- WS `/ws/city/presence`
### Second Me:
- POST `/secondme/invoke`
- GET `/secondme/profile`
### Living Map:
- GET `/city/map`
- POST `/city/map/update`
---
## 3. Функціонал
### 3.1 City Home
- списки районів
- live online count
- city metrics (stub)
### 3.2 Public Rooms
- як канали microDAO, але загальноміські
- WS-чат
- метрики присутності
### 3.3 City Feed
- пости + коментарі (спрощено)
- push notifications (stub)
### 3.4 Presence
- WS presence heartbeat
- avatars grid
### 3.5 Second Me (stub)
- одне поле prompt → простий LLM call
- Зберігати 5 останніх контекстів
### 3.6 Living Map
- JSON-шари:
```json
{
"nodes": [],
"blocks": [],
"agents": [],
"events": []
}
```
---
## 4. Acceptance Criteria
- City Home працює
- Public Rooms доступні
- Feed оновлюється
- Presence працює
- Second Me відповідає
- Map видно
---
## 5. Команда до Cursor
**"Створити DAARION City MVP згідно TASK_PHASE_CITY_MVP.md"**

View File

@@ -0,0 +1,202 @@
# TASK_PHASE_FRONTEND_MVP.md
DAARION / microDAO — FRONTEND MVP (Next.js 15 + App Router + Tailwind)
## 0. Ціль
Створити повністю робочий Frontend MVP, який працює через наш єдиний gateway:
- `https://app.<domain>/api/...`
- `wss://app.<domain>/ws/...`
Фронтенд повинен реалізувати **всі базові флоу microDAO**:
Auth → Teams → Channels → Chat → Follow-ups → Projects → Agents Console → Settings.
Архітектура:
✔ Next.js 15 (App Router)
✔ React Server Components
✔ TailwindCSS
✔ Zustand або Jotai для локального стану
✔ WebSocket transport
✔ API інтерфейс через fetch/axios
✔ SSR для public pages
✔ Protected routes через middleware
---
## 1. Структура проекту
```
frontend/
app/
layout.tsx
page.tsx
auth/
login/
callback/
dashboard/
layout.tsx
teams/
channels/
chat/
projects/
followups/
agents/
settings/
components/
lib/
hooks/
config/
services/
types/
public/
styles/
middleware.ts
next.config.js
package.json
tsconfig.json
```
---
## 2. Екрани, які треба створити
### 2.1 Auth
- `/auth/login` — ввід email → POST `/auth/login-email`
- `/auth/callback?code=` → POST `/auth/exchange`
Зберігання JWT у **httpOnly cookie**.
### 2.2 Teams
- Листинг спільнот
- Створення нової Team
### 2.3 Channels
- Листинг каналів команди
- Створення каналу
- Перемикання public/confidential
### 2.4 Chat
- Чат у реальному часі
- WS підключення: `wss://app.domain/ws/channels/{channelId}`
- Звичайні повідомлення (text)
- Вкладення (stub)
- Thread view (MVP, простий)
### 2.5 Follow-ups
- Листинг
- Create
- Assign / Complete
### 2.6 Projects & Tasks
- Kanban board
- Перетягування задач (drag & drop)
- Фільтри по labels/status
- Docs (editable textarea MVP)
- Meetings (простий scheduler)
### 2.7 Agents Console
- Список агентів
- Agent Chat
- Відображення tokens/latency
- Minimal: call `/agents/{id}/invoke`
### 2.8 Settings
- User settings
- Notifications toggle
- Language toggle
---
## 3. API клієнт
Створити:
```typescript
lib/api.ts
```
Реалізувати:
- `api.get()`
- `api.post()`
- `api.patch()`
- `api.delete()`
Через `fetch` з:
```typescript
credentials: 'include'
```
Обробка:
- 401 → redirect to login
- 429 throttling
- 5xx error toast
---
## 4. WebSocket клієнт
Файл:
```typescript
lib/ws.ts
```
Підтримка:
- auto-reconnect
- heartbeat
- rooms/channels stream
- agent updates stream
---
## 5. UI Kit
Створити мінімальний UI Kit:
```
components/ui/
Button.tsx
Input.tsx
Card.tsx
Avatar.tsx
Badge.tsx
Modal.tsx
```
---
## 6. Acceptance Criteria
1. Авторизація працює від початку до кінця
2. Канали відображаються
3. Чат відправляє/отримує в реальному часі
4. Follow-ups створюються
5. Kanban працює
6. Agent Console відповідає
7. UI без критичних помилок
8. SSR без помилок
9. Mobile-friendly (min 360px)
---
## 7. Команда до Cursor:
**"Створи фронтенд згідно TASK_PHASE_FRONTEND_MVP.md.
Next.js 15 + App Router. Tailwind. JWT у cookie. WS.
Підключення через gateway."**

View File

@@ -0,0 +1,299 @@
# TASK_PHASE_MATRIX_PREPARE.md
DAARION — PHASE 4: MATRIX PREPARE (Без деплою)
Цей таск **підготовчий**:
ми створюємо **повну структуру й конфіги для Matrix + Element Web + TURN**,
але **нічого не деплоїмо і не запускаємо**.
---
## 0. Жорсткі обмеження (дуже важливо)
Cursor **НЕ МАЄ ПРАВА** в цій фазі:
- запускати Synapse, Element, TURN
- змінювати існуючі docker-compose файли
- додавати Matrix у прод-інфру
- змінювати gateway, порти, SSL
Це **тільки підготовка файлів** для майбутньої **PHASE MATRIX FULL**.
---
## 1. Структура каталогів
Створити:
```text
infra/
matrix/
synapse/
homeserver.yaml
workers.yaml
log.config
postgres/
init.sql
turn/
turnserver.conf
element-web/
config.json
gateway/
matrix.conf
schemas/
events/
matrix_event.schema.json
README_MATRIX.md
```
---
## 2. Synapse — шаблони конфігів
### 2.1 `synapse/homeserver.yaml`
Шаблон (без реальних ключів і паролів):
* `server_name: "matrix.daarion.space"` (placeholder)
* `public_baseurl: "https://matrix.daarion.space/"` (placeholder)
* `database:` → Postgres (без реальних паролів)
* `listeners:`:
* порт 8008 (http)
* `registration:`:
* `enable_registration: false`
* `registration_shared_secret: "<TO_BE_FILLED>"` (placeholder comment)
* `media_store_path: "/data/media"` (placeholder)
* `federation_domain_whitelist` (коментар, не обов'язково)
* `modules:`:
* залишити секцію порожньою або з коментарями для майбутніх MSC
* `log_config: /config/log.config`
**Важливо:** у файлі мають бути **коментарі**, що це шаблон і реальні значення будуть заповнені в PHASE MATRIX FULL.
### 2.2 `synapse/workers.yaml`
Шаблон worker-конфіга:
```yaml
worker_app: generic_worker
worker_name: worker1
worker_listeners:
- type: http
port: 9101
resources:
- names: [client, federation]
compress: false
worker_daemonize: false
worker_log_config: /config/log.config
worker_replication_host: synapse
worker_replication_port: 9093
worker_replication_http_port: 9093
```
* коментарі, як це буде використовуватись у майбутньому.
### 2.3 `synapse/log.config`
Мінімальний logging конфіг (YAML), з рівнем `INFO` та коментарями.
---
## 3. Postgres schema init
`postgres/init.sql` — SQL-шаблон:
* створити БД `synapse`
* створити роль `synapse` з паролем `'CHANGE_ME'` (коментар, що змінити)
* дати права на БД
```sql
create database synapse;
create user synapse with encrypted password 'CHANGE_ME';
grant all privileges on database synapse to synapse;
```
Коментар: у PHASE MATRIX FULL буде або використовуватись офіційний мигратор Synapse, або helm/compose, а це — лише стартова заготівка.
---
## 4. TURN/STUN конфіг
`turn/turnserver.conf` — шаблон для coturn:
* `listening-port=3478`
* `fingerprint`
* `lt-cred-mech`
* `realm=daarion.space`
* `user=matrix:CHANGE_ME`
* `total-quota=100`
* `bps-capacity=0`
З великими коментарями:
* про використання real secrets
* про те, що TLS/DTLS налаштовуватиметься в PHASE MATRIX FULL
---
## 5. Element Web конфіг
`element-web/config.json` — базовий шаблон:
```json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.daarion.space",
"server_name": "daarion.space"
}
},
"brand": "DAARION Matrix",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"bug_report_endpoint_url": null,
"features": {},
"show_labs_settings": true,
"defaultCountryCode": "UA",
"disable_custom_urls": true,
"disable_3pid_login": true
}
```
З коментарями (у README):
* як і де це буде використовуватись
* що сторінка `/element/` буде віддавати цей SPA
---
## 6. Gateway конфіг (Nginx) — шаблон
`gateway/matrix.conf`:
```nginx
# Шаблон. Не підключати до прод nginx поки що.
server {
listen 80;
server_name matrix.daarion.space;
location /_matrix/ {
proxy_pass http://matrix-homeserver:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /element/ {
root /var/www/element;
try_files $uri /index.html;
}
}
```
Коментар великими літерами:
**"ЦЕ ШАБЛОН. НЕ АКТИВУВАТИ, ПОКИ НЕ БУДЕ PHASE MATRIX FULL."**
---
## 7. Schemas — майбутній Bridge
`schemas/events/matrix_event.schema.json`:
JSON Schema для подій, які:
* Matrix → DAARION (через майбутній bridge)
* DAARION → Matrix
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Matrix Event (Bridge)",
"type": "object",
"properties": {
"event_id": { "type": "string" },
"room_id": { "type": "string" },
"sender": { "type": "string" },
"type": { "type": "string" },
"origin_server_ts": { "type": "integer" },
"content": { "type": "object" }
},
"required": ["event_id", "room_id", "sender", "type", "content"]
}
```
---
## 8. README_MATRIX.md
Документ повинен описати:
### 8.1. Ролі
* Synapse — Matrix homeserver
* Element Web — клієнт
* TURN — медіа (voice/video, на майбутнє)
* Gateway — HTTP reverse proxy
* Bridge (майбутній) — DAARION City / Agents ↔ Matrix
### 8.2. Фази
Описати **5 фаз**:
1. **Phase 4 — Prepare (цей таск)**
* файли, структури, без деплою.
2. **Phase 5 — Base Synapse Deploy**
* розгортання Synapse + Postgres + TLS.
3. **Phase 6 — Element Web + Gateway**
* деплой статичного SPA, `/element/` route, `/_matrix/`.
4. **Phase 7 — Federation + Multi-node**
* кілька нод DAARION Matrix, federation keys, SRV-записи.
5. **Phase 8 — DAARION Bridge**
* агентний міст: public rooms ↔ Matrix rooms, presence, events.
### 8.3. Безпека
* зберігання ключів
* обмеження доступу
* E2EE keys (відповідальність клієнта)
* логування та privacy
---
## 9. Acceptance Criteria
1. Директорія `infra/matrix/` існує з усіма піддиректоріями й файлами.
2. Усі конфіги (`homeserver.yaml`, `workers.yaml`, `log.config`, `config.json`, `turnserver.conf`, `matrix.conf`) — є, містять валідний YAML/JSON/NGINX-синтаксис.
3. `postgres/init.sql` — є, описує базову схему БД (без реальних паролів).
4. `schemas/events/matrix_event.schema.json` — валідний JSON Schema.
5. `README_MATRIX.md` — повністю описує архітектуру, фази, безпеку та майбутні кроки.
6. **ЖОДНОГО** нового сервісу не додано в docker-compose.
7. **ЖОДЕН** Matrix-сервіс не запускається в цій фазі.
---
## 10. Команда для Cursor
**"Підготувати структуру та конфіги для Matrix згідно TASK_PHASE_MATRIX_PREPARE.md.
НЕ змінювати існуючі docker-compose, НЕ запускати Synapse/Element/Turn,
тільки створити файли та README."**

View File

@@ -0,0 +1,299 @@
# TASK_PHASE_MVP_DEPLOY.md
DAARION MVP — Production Deploy на домен daarion.space
Цей таск готує **повний прод-деплой DAARION MVP** на домен:
- `https://daarion.space` (landing / marketing або redirect)
- `https://app.daarion.space` (MVP-продукт: microDAO + City + Agents)
MVP уже реалізовано (Frontend, Agents Core, City Backend, Second Me).
Завдання цієї фази — **перенести готовий стек на реальний сервер**.
---
## 0. Передумови
1. **PHASE 13** завершені (як у звітах Cursor).
2. Є **готові docker-файли**:
- `docker-compose.all.yml` (23 services, port 80)
- `infra/all-in-one-gateway/docker-compose.yml` (порт 8080, dev)
- скрипти `scripts/start-all.sh`, `scripts/stop-all.sh`
3. Є **PostgreSQL** та **Redis** (або будуть розгорнуті в цьому таску).
4. Є **VPS / dedicated server** з Linux (Ubuntu 22.04 LTS або подібне).
5. Домен **daarion.space** керується тобою (DNS-записи можна змінювати).
---
## 1. Цільова архітектура прод-деплою
### 1.1. Сервер
- 1× VPS:
- CPU: 4+ cores
- RAM: 16+ GB
- Disk: NVMe 256+ GB
- OS: Ubuntu 22.04 LTS
- Docker + Docker Compose встановлені.
### 1.2. Сервіси
Запускаються через **`docker-compose.all.yml`**:
- gateway / nginx (порт 80 всередині)
- frontend (Next.js build + nginx/static)
- microdao-api
- agents-service
- city-service
- secondme-service
- Postgres
- Redis
- NATS
- Prometheus + Grafana
- інші core-сервіси, вже визначені в цьому compose
**Важливо:** prod-деплой бере за основу саме **docker-compose.all.yml**, а не dev-варіанти.
---
## 2. DNS-конфігурація
Налаштувати наступні DNS-записи:
1. `daarion.space` → A-запис на IP сервера.
2. `app.daarion.space` → A-запис на той самий IP сервера.
Опційно:
- `grafana.daarion.space` → для прямого доступу до Grafana (якщо потрібно).
- `api.daarion.space` → якщо хочеш окремий субдомен під API (на майбутнє).
---
## 3. SSL / HTTPS
Рекомендовано три варіанти (вибери один, реалізуй у таску):
### ВАРІАНТ A — Caddy (найпростіший)
- Запустити Caddy як окремий контейнер / сервіс:
- слухає 80/443
- робить HTTPS-термінацію
- проксить на внутрішній gateway (порт 80 контейнерів Docker)
Приклад `Caddyfile`:
```caddy
app.daarion.space {
reverse_proxy gateway-nginx:80
}
daarion.space {
redir https://app.daarion.space{uri}
}
```
### ВАРІАНТ B — Nginx + Certbot (класика)
* Зовнішній nginx на хості:
* `server_name app.daarion.space`
* reverse_proxy → `127.0.0.1:8080` (або 80, залежно від композу)
* SSL-сертифікати через certbot.
### ВАРІАНТ C — Traefik (якщо вже є в інфрі)
* Traefik як edge router, видає HTTPS, роутить по labels.
---
## 4. ENV та секрети
Створити **`env/` директорію** у репозиторії або на сервері (не в git):
```text
env/
app.env
db.env
redis.env
nats.env
agents.env
city.env
secondme.env
```
### 4.1. `app.env` (основні змінні)
```env
APP_ENV=production
APP_BASE_URL=https://app.daarion.space
DATABASE_URL=postgres://daarion:********@db:5432/daarion
REDIS_URL=redis://redis:6379/0
NATS_URL=nats://nats:4222
JWT_SECRET=***************
ENCRYPTION_KEY=*********** # якщо використовується
```
### 4.2. Специфічні
* `SECONDME_AGENT_ID=ag_secondme_global`
* `CITY_DEFAULT_ROOMS=general,welcome,builders`
* `TELEMETRY_ENDPOINT` (якщо є)
* `GF_SERVER_ROOT_URL` для Grafana (якщо публікуємо через `/grafana/`)
У `docker-compose.all.yml`:
* підключити `.env` файли як `env_file:` до відповідних сервісів.
---
## 5. Міграції бази даних
На сервері:
```bash
cd /opt/daarion # приклад шляху деплою
docker compose -f docker-compose.all.yml run --rm migrations-service
# або, якщо в тебе чистий psql:
psql -U postgres -d daarion -f migrations/010_create_city_backend.sql
# + попередні міграції 001..009
```
**Вимога в таску:**
Описати в README:
* порядок запуску **всіх** міграцій (001010)
* команду для re-run (idempotent)
---
## 6. Старт сервісів (docker-compose.all.yml)
На сервері:
```bash
cd /opt/daarion
cp env/app.env .env # якщо Docker Compose очікує .env
docker compose -f docker-compose.all.yml pull # якщо є образи в registry
docker compose -f docker-compose.all.yml up -d
```
або, якщо образи збираються локально:
```bash
docker compose -f docker-compose.all.yml build
docker compose -f docker-compose.all.yml up -d
```
**Завдання в таску:**
Оновити `scripts/start-all.sh` і `scripts/stop-all.sh`, щоб вони:
* працювали в prod (з використанням правильного compose-файлу)
* логували в `/var/log/daarion/` (при потребі)
---
## 7. Healthchecks та smoke-тести
Створити документ:
`docs/DEPLOY_SMOKETEST_CHECKLIST.md` з такими перевірками:
1. **API**
* `GET https://app.daarion.space/api/health` → 200
* `GET https://app.daarion.space/api/city/rooms` → список дефолтних кімнат
* `GET https://app.daarion.space/api/secondme/profile` (якщо user авторизований)
2. **Frontend**
* Відкрити `https://app.daarion.space` у браузері:
* сторінка логіну
* dashboard після входу
* Projects, Follow-ups, Settings
* City → Rooms, Presence, Second Me
3. **WS**
* WebSocket підключення:
* `/ws/channels/...`
* `/ws/city/rooms/{room_id}`
* `/ws/city/presence`
4. **Second Me**
* У UI написати prompt → отримати відповідь.
5. **Monitoring**
* Якщо Grafana/promo публікуються:
* `https://app.daarion.space/grafana/`
* `https://app.daarion.space/prometheus/` (optional)
---
## 8. Логи та моніторинг
Оновити `docs/DEPLOYMENT_OVERVIEW.md`:
* показати, де зберігаються логи:
* `docker logs` (для кожного сервісу)
* опційно — volume з `/var/log/...`
* описати:
* як дивитися Grafana dashboard
* як перевіряти NATS (jetstream, lag)
* як перевіряти Redis (presence keys)
---
## 9. Безпека (мінімум для MVP)
1. Вимкнути:
* прямий доступ до Postgres ззовні
* прямий доступ до Redis/NATS ззовні
2. Обмежити доступ до:
* Prometheus
* Grafana
(або за Basic Auth, або через окремий VPN)
3. У `.env` не зберігати секрети в git.
4. Оновити `PHASE_INFRA_READY.md` з прод-статусом.
---
## 10. Acceptance Criteria
1. **Сервер**: daarion.space резолвиться на IP VPS.
2. **HTTPS**: `https://app.daarion.space` відкривається без помилок сертифіката.
3. **MVP UX**:
* користувач може зареєструватися / залогінитись
* створити Team / Channel / Project / Follow-up
* зайти в City → Rooms, чат, presence
* викликати Second Me
4. **Сервіси живі**: усі контейнері в `docker ps` — в статусі `healthy` / `up`.
5. **Документація**:
* `DEPLOY_ON_SERVER.md` оновлено з урахуванням daarion.space
* `DEPLOY_SMOKETEST_CHECKLIST.md` існує і відповідає фактичному деплою.
---
## 11. Команда для Cursor
**"Підготувати повний production deploy для DAARION MVP згідно TASK_PHASE_MVP_DEPLOY.md.
Використовувати docker-compose.all.yml, домен daarion.space, субдомен app.daarion.space, HTTPS, міграції, Redis, NATS, City та Second Me."**