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:
542
docs/tasks/AGENT_HUB_UI_TASK.md
Normal file
542
docs/tasks/AGENT_HUB_UI_TASK.md
Normal 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!** 🚀
|
||||
|
||||
|
||||
|
||||
|
||||
864
docs/tasks/PHASE2_MASTER_TASK.md
Normal file
864
docs/tasks/PHASE2_MASTER_TASK.md
Normal 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:00–07: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
|
||||
|
||||
|
||||
|
||||
|
||||
503
docs/tasks/PHASE3_MASTER_TASK.md
Normal file
503
docs/tasks/PHASE3_MASTER_TASK.md
Normal 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 1–2):**
|
||||
- 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
|
||||
|
||||
|
||||
|
||||
|
||||
487
docs/tasks/PHASE3_ROADMAP.md
Normal file
487
docs/tasks/PHASE3_ROADMAP.md
Normal 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
251
docs/tasks/README.md
Normal 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
|
||||
|
||||
276
docs/tasks/TASK_AGENT_HUB_MVP.md
Normal file
276
docs/tasks/TASK_AGENT_HUB_MVP.md
Normal 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`
|
||||
- список 3–5 проєктів/тасок (можна поки мок).
|
||||
- блок "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)
|
||||
|
||||
|
||||
|
||||
|
||||
124
docs/tasks/TASK_MATRIX_PREPARE_ONLY.md
Normal file
124
docs/tasks/TASK_MATRIX_PREPARE_ONLY.md
Normal 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
|
||||
|
||||
- Архітектура
|
||||
- Фази 1–5
|
||||
- Валідація
|
||||
- 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, але не виконуй ніякого деплою."**
|
||||
|
||||
421
docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
Normal file
421
docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
Normal 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:00–07: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
|
||||
|
||||
|
||||
|
||||
|
||||
416
docs/tasks/TASK_PHASE7_BACKEND_COMPLETION.md
Normal file
416
docs/tasks/TASK_PHASE7_BACKEND_COMPLETION.md
Normal 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
|
||||
|
||||
614
docs/tasks/TASK_PHASE8_DAO_DASHBOARD.md
Normal file
614
docs/tasks/TASK_PHASE8_DAO_DASHBOARD.md
Normal 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 1–7 завершені** (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
|
||||
|
||||
448
docs/tasks/TASK_PHASE9_LIVING_MAP_FULL.md
Normal file
448
docs/tasks/TASK_PHASE9_LIVING_MAP_FULL.md
Normal 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
|
||||
|
||||
387
docs/tasks/TASK_PHASE9_LIVING_MAP_LITE_2D.md
Normal file
387
docs/tasks/TASK_PHASE9_LIVING_MAP_LITE_2D.md
Normal 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 (наприклад, через 5–10 сек).
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
140
docs/tasks/TASK_PHASE_AGENTS_CORE.md
Normal file
140
docs/tasks/TASK_PHASE_AGENTS_CORE.md
Normal 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."**
|
||||
|
||||
497
docs/tasks/TASK_PHASE_CITY_BACKEND_FINISHER.md
Normal file
497
docs/tasks/TASK_PHASE_CITY_BACKEND_FINISHER.md
Normal 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."**
|
||||
|
||||
281
docs/tasks/TASK_PHASE_CITY_FINISHER.md
Normal file
281
docs/tasks/TASK_PHASE_CITY_FINISHER.md
Normal 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.
|
||||
Працювати в існуючих сервісах і фронтенді."**
|
||||
|
||||
120
docs/tasks/TASK_PHASE_CITY_MVP.md
Normal file
120
docs/tasks/TASK_PHASE_CITY_MVP.md
Normal 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"**
|
||||
|
||||
202
docs/tasks/TASK_PHASE_FRONTEND_MVP.md
Normal file
202
docs/tasks/TASK_PHASE_FRONTEND_MVP.md
Normal 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."**
|
||||
|
||||
299
docs/tasks/TASK_PHASE_MATRIX_PREPARE.md
Normal file
299
docs/tasks/TASK_PHASE_MATRIX_PREPARE.md
Normal 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."**
|
||||
|
||||
299
docs/tasks/TASK_PHASE_MVP_DEPLOY.md
Normal file
299
docs/tasks/TASK_PHASE_MVP_DEPLOY.md
Normal 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 1–3** завершені (як у звітах 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:
|
||||
|
||||
* порядок запуску **всіх** міграцій (001–010)
|
||||
* команду для 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."**
|
||||
|
||||
Reference in New Issue
Block a user