mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
501 lines
13 KiB
Markdown
501 lines
13 KiB
Markdown
# Implementation Plan: TASK-008 - Resolve All TODOs
|
|
|
|
## Overview
|
|
|
|
This plan breaks down the TODO resolution into discrete, testable sub-tasks organized by priority and dependency order.
|
|
|
|
---
|
|
|
|
## Phase 1: Critical Path (P0) - ~2.5 hours
|
|
|
|
### 1.1 Health Check Implementation (30 min)
|
|
|
|
**File**: `roboco/api/routes/health.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Add database health check function
|
|
2. Add Redis health check function
|
|
3. Update `readiness_check()` endpoint to call both
|
|
4. Handle failures gracefully (return status per service)
|
|
|
|
**Changes**:
|
|
```python
|
|
# Add imports
|
|
from sqlalchemy import text
|
|
from roboco.db import get_async_session
|
|
import redis.asyncio as redis
|
|
from roboco.config import settings
|
|
|
|
# Add check functions
|
|
async def check_database() -> tuple[str, bool]:
|
|
try:
|
|
async with get_async_session() as session:
|
|
await session.execute(text("SELECT 1"))
|
|
return "ok", True
|
|
except Exception as e:
|
|
return str(e), False
|
|
|
|
async def check_redis() -> tuple[str, bool]:
|
|
try:
|
|
client = redis.from_url(settings.redis_url)
|
|
await client.ping()
|
|
await client.close()
|
|
return "ok", True
|
|
except Exception as e:
|
|
return str(e), False
|
|
|
|
# Update endpoint
|
|
@router.get("/ready")
|
|
async def readiness_check() -> ReadinessResponse:
|
|
db_status, db_ok = await check_database()
|
|
redis_status, redis_ok = await check_redis()
|
|
overall = "ok" if (db_ok and redis_ok) else "degraded"
|
|
return ReadinessResponse(
|
|
status=overall,
|
|
database=db_status,
|
|
redis=redis_status,
|
|
)
|
|
```
|
|
|
|
**Tests**: Call `/ready` with DB up/down, Redis up/down
|
|
|
|
---
|
|
|
|
### 1.2 Base Agent LLM Integration (2 hours)
|
|
|
|
**File**: `roboco/agents/base.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Add Anthropic client initialization
|
|
2. Implement `think()` method with actual LLM call
|
|
3. Implement `think_and_stream()` with streaming response
|
|
4. Implement `send_message()` with HTTP API call
|
|
5. Implement `stream_reasoning()` with WebSocket broadcast
|
|
|
|
**Changes**:
|
|
|
|
```python
|
|
# Add to imports
|
|
from anthropic import AsyncAnthropic
|
|
import httpx
|
|
from roboco.config import settings
|
|
from roboco.api.websocket import broadcast_agent_chunk
|
|
|
|
# Add to Agent.__init__
|
|
self._llm_client: AsyncAnthropic | None = None
|
|
|
|
# Add property
|
|
@property
|
|
def llm_client(self) -> AsyncAnthropic:
|
|
if self._llm_client is None:
|
|
self._llm_client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
|
return self._llm_client
|
|
|
|
# Implement think()
|
|
async def think(self, prompt: str, context: dict[str, Any] | None = None) -> str:
|
|
self.log.debug("Thinking", prompt_length=len(prompt))
|
|
|
|
messages = [{"role": "user", "content": prompt}]
|
|
|
|
response = await self.llm_client.messages.create(
|
|
model=self.config.model,
|
|
max_tokens=self.config.max_tokens,
|
|
system=self.config.system_prompt,
|
|
messages=messages,
|
|
)
|
|
|
|
return response.content[0].text
|
|
|
|
# Implement think_and_stream()
|
|
async def think_and_stream(
|
|
self,
|
|
prompt: str,
|
|
context: dict[str, Any] | None = None,
|
|
) -> str:
|
|
self.log.debug("Thinking (streaming)", prompt_length=len(prompt))
|
|
|
|
messages = [{"role": "user", "content": prompt}]
|
|
full_response = ""
|
|
|
|
async with self.llm_client.messages.stream(
|
|
model=self.config.model,
|
|
max_tokens=self.config.max_tokens,
|
|
system=self.config.system_prompt,
|
|
messages=messages,
|
|
) as stream:
|
|
async for text in stream.text_stream:
|
|
full_response += text
|
|
await self.stream_reasoning(text)
|
|
|
|
return full_response
|
|
|
|
# Implement send_message()
|
|
async def send_message(
|
|
self,
|
|
channel_id: UUID,
|
|
content: str,
|
|
message_type: str = "dialogue",
|
|
) -> None:
|
|
self.state.messages_sent += 1
|
|
self.state.last_activity = datetime.now(UTC)
|
|
|
|
url = f"http://{settings.host}:{settings.port}/api/v1/messages"
|
|
async with httpx.AsyncClient() as client:
|
|
await client.post(url, json={
|
|
"channel_id": str(channel_id),
|
|
"agent_id": str(self.id),
|
|
"content": content,
|
|
"message_type": message_type,
|
|
})
|
|
|
|
self.log.debug("Message sent", channel_id=str(channel_id))
|
|
|
|
# Implement stream_reasoning()
|
|
async def stream_reasoning(self, content: str) -> None:
|
|
await broadcast_agent_chunk(self.id, content)
|
|
self.log.debug("Streamed reasoning", content_length=len(content))
|
|
```
|
|
|
|
**Tests**: Unit test with mocked Anthropic client
|
|
|
|
---
|
|
|
|
## Phase 2: Agent Functionality (P1) - ~5.5 hours
|
|
|
|
### 2.1 Agent API Helper (30 min)
|
|
|
|
**File**: `roboco/agents/base.py` (add helper method)
|
|
|
|
```python
|
|
async def _api_call(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
**kwargs: Any,
|
|
) -> dict[str, Any]:
|
|
"""Make API call to RoboCo services."""
|
|
url = f"http://{settings.host}:{settings.port}/api/v1{path}"
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.request(method, url, **kwargs)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 Developer Agent (1.5 hours)
|
|
|
|
**File**: `roboco/agents/developer.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Implement `_find_paused_task()` - GET `/tasks?status=paused&assigned_to={id}`
|
|
2. Implement `_find_assigned_task()` - GET `/tasks?status=pending&assigned_to={id}`
|
|
3. Implement `_get_task_title()` - GET `/tasks/{id}`
|
|
4. Implement `_read_task_requirements()` - GET `/tasks/{id}` (description + criteria)
|
|
5. Implement `_update_task_status()` - PUT `/tasks/{id}`
|
|
6. Implement `_check_qa_approved()` - GET `/tasks/{id}` check status
|
|
7. Implement `_check_docs_complete()` - GET `/tasks/{id}/handoffs`
|
|
|
|
**Pattern**:
|
|
```python
|
|
async def _find_paused_task(self) -> UUID | None:
|
|
result = await self._api_call(
|
|
"GET",
|
|
"/tasks",
|
|
params={"status": "paused", "assigned_to": str(self.id)}
|
|
)
|
|
tasks = result.get("items", [])
|
|
return UUID(tasks[0]["id"]) if tasks else None
|
|
```
|
|
|
|
---
|
|
|
|
### 2.3 QA Agent (1 hour)
|
|
|
|
**File**: `roboco/agents/qa.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Implement `_find_awaiting_qa()` - GET `/tasks?status=awaiting_qa&team={team}`
|
|
2. Implement `_get_task_title()` - same as developer
|
|
3. Implement `_read_task_requirements()` - same as developer
|
|
4. Implement `_read_dev_notes()` - GET `/tasks/{id}` (dev_notes field)
|
|
5. Implement `_get_task_commits()` - GET `/tasks/{id}` (commits field)
|
|
6. Implement `_update_task_status()` - same as developer
|
|
|
|
---
|
|
|
|
### 2.4 Documenter Agent (1 hour)
|
|
|
|
**File**: `roboco/agents/documenter.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Implement all query methods (same pattern as QA)
|
|
2. Implement `_phase_publish()` with file writing:
|
|
|
|
```python
|
|
import aiofiles
|
|
|
|
async def _phase_publish(self, ctx: DocContext) -> None:
|
|
self.log.info("PUBLISH phase", task_id=str(ctx.task_id))
|
|
|
|
for doc_spec in ctx.documents_needed:
|
|
if doc_spec.content:
|
|
path = Path(doc_spec.path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
async with aiofiles.open(path, 'w') as f:
|
|
await f.write(doc_spec.content)
|
|
self.log.info("Published", path=doc_spec.path)
|
|
|
|
await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED)
|
|
```
|
|
|
|
---
|
|
|
|
### 2.5 PM Agents (1 hour)
|
|
|
|
**File**: `roboco/agents/pm.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Implement task counting methods using `/tasks` with filters
|
|
2. Implement agent counting using `/agents` endpoint
|
|
3. Implement `_get_pending_questions()` using `/messages?type=dialogue`
|
|
4. Implement `_check_task_progress()` using `/tasks/{id}`
|
|
|
|
---
|
|
|
|
### 2.6 Board Agents (1 hour)
|
|
|
|
**File**: `roboco/agents/board.py`
|
|
|
|
**Sub-tasks**:
|
|
1. Implement `_review_feature()` - GET task, check acceptance criteria
|
|
2. Implement `_read_channel_silently()` - GET `/channels/{slug}/messages`
|
|
3. Implement `_perform_audit()` - Aggregate queries for patterns
|
|
|
|
---
|
|
|
|
## Phase 3: Real-Time (P2) - ~2 hours
|
|
|
|
### 3.1 WebSocket Channel Validation (1 hour)
|
|
|
|
**File**: `roboco/api/websocket.py`
|
|
|
|
```python
|
|
from roboco.services.permissions import PermissionService
|
|
|
|
async def validate_channel_access(
|
|
channel_id: UUID,
|
|
agent_id: UUID,
|
|
session: AsyncSession
|
|
) -> bool:
|
|
perm_service = PermissionService(session)
|
|
return await perm_service.can_read_channel(agent_id, channel_id)
|
|
```
|
|
|
|
---
|
|
|
|
### 3.2 Per-Agent Notification Delivery (1 hour)
|
|
|
|
**File**: `roboco/api/websocket.py`
|
|
|
|
Add agent-specific connections tracking:
|
|
|
|
```python
|
|
class ConnectionManager:
|
|
def __init__(self) -> None:
|
|
# ... existing ...
|
|
# Add per-agent notification connections
|
|
self.notification_connections: dict[UUID, set[WebSocket]] = {}
|
|
|
|
async def connect_notifications(
|
|
self, websocket: WebSocket, agent_id: UUID
|
|
) -> None:
|
|
await websocket.accept()
|
|
if agent_id not in self.notification_connections:
|
|
self.notification_connections[agent_id] = set()
|
|
self.notification_connections[agent_id].add(websocket)
|
|
|
|
async def broadcast_notification(
|
|
agent_ids: list[UUID],
|
|
notification_id: UUID,
|
|
notification_type: str,
|
|
subject: str,
|
|
priority: str,
|
|
) -> None:
|
|
event = {
|
|
"type": "notification",
|
|
"notification_id": str(notification_id),
|
|
"notification_type": notification_type,
|
|
"subject": subject,
|
|
"priority": priority,
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
data = json.dumps(event)
|
|
|
|
for agent_id in agent_ids:
|
|
connections = manager.notification_connections.get(agent_id, set())
|
|
await asyncio.gather(
|
|
*[conn.send_text(data) for conn in connections],
|
|
return_exceptions=True,
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Intelligence (P2) - ~3 hours
|
|
|
|
### 4.1 LLM-based Message Extraction (1 hour)
|
|
|
|
**File**: `roboco/services/extraction.py`
|
|
|
|
```python
|
|
async def extract_with_llm(
|
|
self,
|
|
content: str,
|
|
agent_id: UUID,
|
|
channel_id: UUID,
|
|
session_id: UUID,
|
|
group_id: UUID,
|
|
task_id: UUID | None = None,
|
|
) -> ExtractionResult:
|
|
from anthropic import AsyncAnthropic
|
|
from roboco.config import settings
|
|
|
|
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
|
|
|
# Use LLM to classify message segments
|
|
prompt = f"""Classify the following agent output into message types.
|
|
For each distinct segment, identify:
|
|
- type: reasoning, dialogue, decision, action, blocker, or technical
|
|
- content: the segment text
|
|
- confidence: 0.0 to 1.0
|
|
|
|
Agent output:
|
|
{content}
|
|
|
|
Return as JSON array of objects with type, content, confidence."""
|
|
|
|
response = await client.messages.create(
|
|
model="claude-3-haiku-20240307", # Fast, cheap for classification
|
|
max_tokens=2000,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
|
|
# Parse response and create ExtractedMessage objects
|
|
# ... parsing logic ...
|
|
|
|
return ExtractionResult(...)
|
|
```
|
|
|
|
---
|
|
|
|
### 4.2 OptimalService RAG Methods (2 hours)
|
|
|
|
**File**: `roboco/services/optimal.py`
|
|
|
|
Implement `search()`, `query()`, `index()` using piragi:
|
|
|
|
```python
|
|
async def search(
|
|
self,
|
|
query: str,
|
|
index_types: list[IndexType] | None = None,
|
|
limit: int = 10,
|
|
) -> list[SearchResult]:
|
|
"""Search across knowledge base indexes."""
|
|
types_to_search = index_types or list(IndexType)
|
|
all_results: list[SearchResult] = []
|
|
|
|
for index_type in types_to_search:
|
|
index = self._get_index(index_type)
|
|
# Use piragi's search method
|
|
results = await index.search(query, top_k=limit)
|
|
for r in results:
|
|
all_results.append(SearchResult(
|
|
content=r.content,
|
|
source=r.metadata.get("source", "unknown"),
|
|
score=r.score,
|
|
index_type=index_type,
|
|
metadata=r.metadata,
|
|
))
|
|
|
|
# Sort by score, return top limit
|
|
all_results.sort(key=lambda x: x.score, reverse=True)
|
|
return all_results[:limit]
|
|
|
|
async def query(
|
|
self,
|
|
query: str,
|
|
context: QueryContext | None = None,
|
|
) -> RAGResponse:
|
|
"""RAG query with context."""
|
|
# Get relevant context
|
|
results = await self.search(
|
|
query,
|
|
index_types=context.index_types if context else None,
|
|
limit=5,
|
|
)
|
|
|
|
# Build context for LLM
|
|
context_text = "\n\n".join([r.content for r in results])
|
|
|
|
# Query with RAG context
|
|
prompt = f"""Based on the following context, answer the question.
|
|
|
|
Context:
|
|
{context_text}
|
|
|
|
Question: {query}
|
|
|
|
Answer:"""
|
|
|
|
from anthropic import AsyncAnthropic
|
|
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
|
response = await client.messages.create(
|
|
model=settings.default_model,
|
|
max_tokens=1000,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
|
|
return RAGResponse(
|
|
answer=response.content[0].text,
|
|
citations=results,
|
|
query=query,
|
|
context_used=len(results),
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Checklist
|
|
|
|
After each phase, verify:
|
|
|
|
- [ ] `uv run ruff format .` passes
|
|
- [ ] `uv run ruff check .` passes
|
|
- [ ] `uv run mypy roboco/` passes
|
|
- [ ] No TODO comments remain in modified files
|
|
- [ ] API calls work against running server
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
Each phase is independent. If issues arise:
|
|
1. Revert the specific file changes
|
|
2. Leave TODO comments in place for that section
|
|
3. Create a new sub-task for the problematic area
|
|
|
|
---
|
|
|
|
## Definition of Done
|
|
|
|
- [ ] All 37 TODOs resolved or converted to tracked issues
|
|
- [ ] Health checks verify actual service connectivity
|
|
- [ ] Agents can call LLM and receive responses
|
|
- [ ] Agents can send messages via API
|
|
- [ ] WebSocket notifications delivered to connected agents
|
|
- [ ] RAG queries return relevant results
|
|
- [ ] All type checks pass
|
|
- [ ] All linting passes
|