mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
TOON Optimizations
This commit is contained in:
+14
-1
@@ -36,6 +36,7 @@ dependencies = [
|
||||
"anthropic",
|
||||
"openai", # For embeddings
|
||||
"tiktoken", # Token counting
|
||||
"python-toon", # Token-efficient LLM serialization
|
||||
|
||||
# MCP (Model Context Protocol)
|
||||
"mcp",
|
||||
@@ -120,11 +121,23 @@ select = [
|
||||
"SIM", # flake8-simplify
|
||||
"TCH", # flake8-type-checking
|
||||
"PTH", # flake8-use-pathlib
|
||||
"ERA", # eradicate (commented code)
|
||||
"PL", # Pylint
|
||||
"RUF", # Ruff-specific
|
||||
]
|
||||
|
||||
# MCP servers: Tool functions require explicit parameters for schema generation.
|
||||
# PLR0913 - MCP tools need many params (no dataclass workaround)
|
||||
# PLR0915 - Factory functions define all tools (unavoidable nesting)
|
||||
# PLC0415 - Lazy imports for circular import avoidance
|
||||
# E501 - Docstrings and guidance messages
|
||||
#
|
||||
# Services: Internal methods often need multiple related parameters for DB ops.
|
||||
# PLR0913 - Service methods wrapping DB/external calls need multiple params
|
||||
# PLC0415 - Lazy imports for circular import avoidance
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"roboco/mcp/*.py" = ["PLR0913", "PLR0915", "PLC0415", "E501"]
|
||||
"roboco/services/*.py" = ["PLC0415", "PLR0913"]
|
||||
|
||||
# =============================================================================
|
||||
# MyPy Configuration
|
||||
# =============================================================================
|
||||
|
||||
@@ -21,6 +21,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from roboco.api.websocket import broadcast_agent_chunk
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.models import AgentRole, AgentStatus, Team
|
||||
|
||||
logger = structlog.get_logger()
|
||||
@@ -113,6 +114,7 @@ class Agent(ABC):
|
||||
self._running = False
|
||||
self._task: asyncio.Task | None = None
|
||||
self._llm_client: AsyncAnthropic | None = None
|
||||
self._toon = ToonAdapter()
|
||||
|
||||
self.log = logger.bind(
|
||||
agent_id=str(config.id),
|
||||
@@ -157,6 +159,52 @@ class Agent(ABC):
|
||||
self._llm_client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
return self._llm_client
|
||||
|
||||
# =========================================================================
|
||||
# TOON SERIALIZATION (for token-efficient LLM communication)
|
||||
# =========================================================================
|
||||
|
||||
def format_context(self, data: dict[str, Any]) -> str:
|
||||
"""
|
||||
Format context data for LLM using TOON.
|
||||
|
||||
TOON (Token-Oriented Object Notation) reduces token consumption
|
||||
by 30-60% compared to JSON while maintaining semantic clarity.
|
||||
|
||||
Args:
|
||||
data: Dictionary to encode for LLM prompt.
|
||||
|
||||
Returns:
|
||||
TOON-formatted string.
|
||||
"""
|
||||
return self._toon.encode(data)
|
||||
|
||||
def format_context_labeled(self, label: str, data: dict[str, Any]) -> str:
|
||||
"""
|
||||
Format labeled context data for embedding in prompts.
|
||||
|
||||
Args:
|
||||
label: Section label (e.g., "Task Context").
|
||||
data: Dictionary to encode.
|
||||
|
||||
Returns:
|
||||
Labeled TOON-formatted string.
|
||||
"""
|
||||
return self._toon.format_for_prompt(label, data)
|
||||
|
||||
def parse_llm_response(self, response: str) -> dict[str, Any] | list[Any]:
|
||||
"""
|
||||
Parse structured data from LLM response.
|
||||
|
||||
Attempts TOON parsing first, falls back to JSON.
|
||||
|
||||
Args:
|
||||
response: Raw LLM response text.
|
||||
|
||||
Returns:
|
||||
Parsed Python dict or list.
|
||||
"""
|
||||
return self._toon.decode(response)
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE METHODS
|
||||
# =========================================================================
|
||||
|
||||
+23
-21
@@ -153,17 +153,20 @@ class ProductOwnerAgent(Agent):
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
acceptance_criteria = result.get("acceptance_criteria", [])
|
||||
|
||||
# Use LLM to check if criteria are met
|
||||
prompt = f"""
|
||||
Review this completed feature against its acceptance criteria:
|
||||
# Use TOON for token-efficient context encoding
|
||||
task_context = self.format_context_labeled(
|
||||
"Feature Review",
|
||||
{
|
||||
"title": result.get("title", "Unknown"),
|
||||
"description": result.get("description", "No description"),
|
||||
"acceptance_criteria": acceptance_criteria,
|
||||
"dev_notes": result.get("dev_notes", "None"),
|
||||
},
|
||||
)
|
||||
|
||||
Task: {result.get("title", "Unknown")}
|
||||
Description: {result.get("description", "No description")}
|
||||
prompt = f"""Review this completed feature against its acceptance criteria:
|
||||
|
||||
Acceptance Criteria:
|
||||
{chr(10).join(f"- {c}" for c in acceptance_criteria)}
|
||||
|
||||
Dev Notes: {result.get("dev_notes", "None")}
|
||||
{task_context}
|
||||
|
||||
Determine if all criteria are met. Respond with:
|
||||
ACCEPTED: [reason] or NEEDS_CHANGES: [what's missing]
|
||||
@@ -465,11 +468,15 @@ class AuditorAgent(Agent):
|
||||
if not self._observations:
|
||||
return
|
||||
|
||||
prompt = f"""
|
||||
Analyze these observations for quality and efficiency issues:
|
||||
# Use TOON for token-efficient context encoding
|
||||
observations_context = self.format_context_labeled(
|
||||
"Observations",
|
||||
{"recent": self._observations[-50:]},
|
||||
)
|
||||
|
||||
Observations:
|
||||
{chr(10).join(str(o) for o in self._observations[-50:])}
|
||||
prompt = f"""Analyze these observations for quality and efficiency issues:
|
||||
|
||||
{observations_context}
|
||||
|
||||
Look for:
|
||||
1. Efficiency issues - wasted effort, unclear processes
|
||||
@@ -478,14 +485,9 @@ Look for:
|
||||
4. Process violations - skipping QA, missing documentation
|
||||
5. Team health - frustration, conflicts
|
||||
|
||||
For each issue found, provide:
|
||||
- Category
|
||||
- Severity (info/warning/concern/critical)
|
||||
- Description
|
||||
- Evidence
|
||||
- Recommendation
|
||||
|
||||
Be thorough but fair.
|
||||
Format response as TOON tabular:
|
||||
[N,]{{category,severity,description,evidence,recommendation}}:
|
||||
efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff steps
|
||||
"""
|
||||
analysis = await self.think(prompt)
|
||||
self.log.info("Analysis complete", analysis_length=len(analysis))
|
||||
|
||||
+57
-24
@@ -249,13 +249,16 @@ class DeveloperAgent(Agent):
|
||||
# Read task requirements
|
||||
requirements = await self._read_task_requirements(ctx.task_id)
|
||||
|
||||
# Use LLM to understand and identify gaps
|
||||
prompt = f"""
|
||||
You are analyzing a task before beginning work.
|
||||
# Format context using TOON for token efficiency
|
||||
task_context = self.format_context_labeled(
|
||||
"Task Context",
|
||||
{"title": ctx.title, "requirements": requirements},
|
||||
)
|
||||
|
||||
Task: {ctx.title}
|
||||
Requirements:
|
||||
{requirements}
|
||||
# Use LLM to understand and identify gaps
|
||||
prompt = f"""You are analyzing a task before beginning work.
|
||||
|
||||
{task_context}
|
||||
|
||||
Analyze:
|
||||
1. What exactly needs to be done?
|
||||
@@ -293,28 +296,48 @@ If clarification needed, respond with: "QUESTION: [your question]"
|
||||
"""
|
||||
self.log.info("PLAN phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Use LLM to create plan
|
||||
prompt = f"""
|
||||
Create an implementation plan for this task:
|
||||
# Format context using TOON
|
||||
plan_context = self.format_context_labeled(
|
||||
"Task",
|
||||
{
|
||||
"title": ctx.title,
|
||||
"understanding": ctx.journal_entries[-1]
|
||||
if ctx.journal_entries
|
||||
else "No context",
|
||||
},
|
||||
)
|
||||
|
||||
Task: {ctx.title}
|
||||
Understanding: {ctx.journal_entries[-1] if ctx.journal_entries else "No context"}
|
||||
# Use LLM to create plan - request TOON tabular response
|
||||
prompt = f"""Create an implementation plan for this task:
|
||||
|
||||
Break this into ordered subtasks. For each subtask:
|
||||
{plan_context}
|
||||
|
||||
Break this into ordered subtasks. For each subtask provide:
|
||||
- Clear description
|
||||
- Files to modify
|
||||
- Estimated complexity (small/medium/large)
|
||||
|
||||
Format as JSON array:
|
||||
[
|
||||
{{"description": "...", "files": ["..."], "complexity": "small|medium|large"}},
|
||||
...
|
||||
]
|
||||
Format response as TOON tabular:
|
||||
[N,]{{description,files,complexity}}:
|
||||
Implement the main logic,src/main.py|src/utils.py,medium
|
||||
Add unit tests,tests/test_main.py,small
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
|
||||
# Parse subtasks (simplified - would use proper JSON parsing)
|
||||
ctx.subtasks = [{"description": response, "files": [], "complexity": "medium"}]
|
||||
# Parse subtasks using TOON (falls back to JSON)
|
||||
try:
|
||||
subtasks = self.parse_llm_response(response)
|
||||
if isinstance(subtasks, list):
|
||||
ctx.subtasks = subtasks
|
||||
else:
|
||||
ctx.subtasks = [
|
||||
{"description": response, "files": [], "complexity": "medium"}
|
||||
]
|
||||
except ValueError:
|
||||
# Fallback if parsing fails
|
||||
ctx.subtasks = [
|
||||
{"description": response, "files": [], "complexity": "medium"}
|
||||
]
|
||||
|
||||
# Journal entry
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
@@ -349,12 +372,22 @@ Format as JSON array:
|
||||
|
||||
subtask = ctx.subtasks[ctx.current_subtask]
|
||||
|
||||
# Use LLM to work on subtask
|
||||
prompt = f"""
|
||||
Execute this subtask:
|
||||
# Format context using TOON
|
||||
execute_context = self.format_context_labeled(
|
||||
"Execution Context",
|
||||
{
|
||||
"task": ctx.title,
|
||||
"subtask_number": ctx.current_subtask + 1,
|
||||
"total_subtasks": len(ctx.subtasks),
|
||||
"description": subtask.get("description", ""),
|
||||
"files": subtask.get("files", []),
|
||||
},
|
||||
)
|
||||
|
||||
Task: {ctx.title}
|
||||
Subtask {ctx.current_subtask + 1}/{len(ctx.subtasks)}: {subtask.get("description", "")}
|
||||
# Use LLM to work on subtask
|
||||
prompt = f"""Execute this subtask:
|
||||
|
||||
{execute_context}
|
||||
|
||||
Provide:
|
||||
1. Code changes needed
|
||||
|
||||
+27
-18
@@ -321,19 +321,21 @@ Respond with structured analysis.
|
||||
|
||||
doc_spec = ctx.documents_needed[ctx.current_doc]
|
||||
|
||||
# Use LLM to write documentation
|
||||
prompt = f"""
|
||||
Write documentation for this task.
|
||||
# Use TOON for token-efficient context encoding
|
||||
doc_context = self.format_context_labeled(
|
||||
"Documentation Task",
|
||||
{
|
||||
"title": ctx.title,
|
||||
"doc_type": doc_spec.doc_type.value,
|
||||
"target_path": doc_spec.path,
|
||||
"summary": ctx.summary,
|
||||
"dev_notes": ctx.dev_notes or "None",
|
||||
},
|
||||
)
|
||||
|
||||
Task: {ctx.title}
|
||||
Document Type: {doc_spec.doc_type.value}
|
||||
Target Path: {doc_spec.path}
|
||||
prompt = f"""Write documentation for this task.
|
||||
|
||||
Summary:
|
||||
{ctx.summary}
|
||||
|
||||
Developer Notes:
|
||||
{ctx.dev_notes or "None"}
|
||||
{doc_context}
|
||||
|
||||
Write professional, clear documentation following best practices.
|
||||
Include:
|
||||
@@ -373,14 +375,19 @@ Format appropriately for the document type.
|
||||
if not doc_spec.content:
|
||||
continue
|
||||
|
||||
prompt = f"""
|
||||
Review this documentation for quality:
|
||||
# Use TOON for token-efficient context encoding
|
||||
review_context = self.format_context_labeled(
|
||||
"Document Review",
|
||||
{
|
||||
"title": doc_spec.title,
|
||||
"doc_type": doc_spec.doc_type.value,
|
||||
"content": doc_spec.content,
|
||||
},
|
||||
)
|
||||
|
||||
Document: {doc_spec.title}
|
||||
Type: {doc_spec.doc_type.value}
|
||||
prompt = f"""Review this documentation for quality:
|
||||
|
||||
Content:
|
||||
{doc_spec.content}
|
||||
{review_context}
|
||||
|
||||
Check:
|
||||
1. Accuracy - Does it correctly describe the feature?
|
||||
@@ -388,7 +395,9 @@ Check:
|
||||
3. Clarity - Is it easy to understand?
|
||||
4. Examples - Are examples helpful and correct?
|
||||
|
||||
If issues found, provide suggestions.
|
||||
Format response as TOON:
|
||||
{{accuracy,completeness,clarity,examples,suggestions}}:
|
||||
good,complete,clear,helpful,None
|
||||
"""
|
||||
review = await self.think(prompt)
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
|
||||
+51
-28
@@ -199,12 +199,15 @@ class CellPMAgent(Agent):
|
||||
new_tasks = await self._get_unassigned_tasks()
|
||||
|
||||
for task_id in new_tasks:
|
||||
# Assess complexity and priority
|
||||
prompt = f"""
|
||||
Assess this task for prioritization:
|
||||
# Use TOON for token-efficient context encoding
|
||||
triage_context = self.format_context_labeled(
|
||||
"Task Triage",
|
||||
{"task_id": str(task_id), "cell": self.cell_name},
|
||||
)
|
||||
|
||||
Task ID: {task_id}
|
||||
Cell: {self.cell_name}
|
||||
prompt = f"""Assess this task for prioritization:
|
||||
|
||||
{triage_context}
|
||||
|
||||
Consider:
|
||||
1. Complexity (low/medium/high)
|
||||
@@ -212,7 +215,9 @@ Consider:
|
||||
3. Priority (P0-P3)
|
||||
4. Best dev fit based on skills
|
||||
|
||||
Provide assessment.
|
||||
Format response as TOON:
|
||||
{{complexity,dependencies,priority,dev_fit}}:
|
||||
medium,TASK-abc123,P1,backend-dev-1
|
||||
"""
|
||||
assessment = await self.think(prompt)
|
||||
self.log.info(
|
||||
@@ -249,11 +254,15 @@ Provide assessment.
|
||||
questions = await self._get_pending_questions()
|
||||
|
||||
for question in questions:
|
||||
# Try to answer or route appropriately
|
||||
prompt = f"""
|
||||
A cell member has a question:
|
||||
# Use TOON for token-efficient context encoding
|
||||
question_context = self.format_context_labeled(
|
||||
"Cell Question",
|
||||
{"question": question, "cell": self.cell_name},
|
||||
)
|
||||
|
||||
{question}
|
||||
prompt = f"""A cell member needs help:
|
||||
|
||||
{question_context}
|
||||
|
||||
As the Cell PM, provide:
|
||||
1. Answer if you can
|
||||
@@ -610,23 +619,29 @@ class MainPMAgent(Agent):
|
||||
self.log.debug("PRIORITIZE phase")
|
||||
|
||||
if self._board_directives:
|
||||
directives = chr(10).join(f"- {d}" for d in self._board_directives)
|
||||
status_lines = []
|
||||
for k, v in self._cell_statuses.items():
|
||||
active = v.active_tasks
|
||||
blocked = v.blocked_tasks
|
||||
status_lines.append(f"- {k}: {active} active, {blocked} blocked")
|
||||
cell_status = chr(10).join(status_lines)
|
||||
prompt = f"""
|
||||
Translate these Board directives into cell priorities:
|
||||
# Build status data for TOON encoding
|
||||
cell_status_data = {
|
||||
name: {"active": s.active_tasks, "blocked": s.blocked_tasks}
|
||||
for name, s in self._cell_statuses.items()
|
||||
}
|
||||
|
||||
Directives:
|
||||
{directives}
|
||||
# Use TOON for token-efficient context encoding
|
||||
priority_context = self.format_context_labeled(
|
||||
"Prioritization Context",
|
||||
{
|
||||
"directives": self._board_directives,
|
||||
"cell_status": cell_status_data,
|
||||
},
|
||||
)
|
||||
|
||||
Current Cell Status:
|
||||
{cell_status}
|
||||
prompt = f"""Translate these Board directives into cell priorities:
|
||||
|
||||
Provide prioritized task list for each cell.
|
||||
{priority_context}
|
||||
|
||||
Format response as TOON tabular:
|
||||
[N,]{{cell,priority,task_description}}:
|
||||
backend-cell,P0,Implement critical auth fix
|
||||
frontend-cell,P1,Update dashboard layout
|
||||
"""
|
||||
priorities = await self.think(prompt)
|
||||
self.log.info("Priorities set", priorities=priorities[:200])
|
||||
@@ -636,11 +651,19 @@ Provide prioritized task list for each cell.
|
||||
self.log.debug("COORDINATE phase")
|
||||
|
||||
for issue in self._cross_cell_issues:
|
||||
prompt = f"""
|
||||
Resolve this cross-cell issue:
|
||||
# Use TOON for token-efficient context encoding
|
||||
issue_context = self.format_context_labeled(
|
||||
"Cross-Cell Issue",
|
||||
{
|
||||
"description": issue.get("description"),
|
||||
"cells": issue.get("cells"),
|
||||
"task_id": issue.get("task_id"),
|
||||
},
|
||||
)
|
||||
|
||||
Issue: {issue.get("description")}
|
||||
Cells Involved: {issue.get("cells")}
|
||||
prompt = f"""Resolve this cross-cell issue:
|
||||
|
||||
{issue_context}
|
||||
|
||||
Propose a resolution that unblocks all parties.
|
||||
"""
|
||||
|
||||
+33
-34
@@ -211,27 +211,22 @@ class QAAgent(Agent):
|
||||
dev_notes = await self._read_dev_notes(ctx.task_id)
|
||||
commits = await self._get_task_commits(ctx.task_id)
|
||||
|
||||
# Use LLM to understand and create test plan
|
||||
prompt = f"""
|
||||
You are a QA engineer reviewing a completed task.
|
||||
# Use TOON for token-efficient context encoding
|
||||
task_context = self.format_context_labeled(
|
||||
"QA Review Context",
|
||||
{
|
||||
"title": ctx.title,
|
||||
"requirements": requirements,
|
||||
"dev_notes": dev_notes,
|
||||
"commits": commits,
|
||||
},
|
||||
)
|
||||
|
||||
Task: {ctx.title}
|
||||
prompt = f"""You are a QA engineer reviewing a completed task.
|
||||
|
||||
Requirements:
|
||||
{requirements}
|
||||
|
||||
Developer Notes:
|
||||
{dev_notes}
|
||||
|
||||
Commits:
|
||||
{commits}
|
||||
{task_context}
|
||||
|
||||
Based on this, create test cases to verify the implementation.
|
||||
For each test case provide:
|
||||
1. Name
|
||||
2. What to test
|
||||
3. Steps to execute
|
||||
4. Expected outcome
|
||||
|
||||
Focus on:
|
||||
- Acceptance criteria verification
|
||||
@@ -239,8 +234,10 @@ Focus on:
|
||||
- Integration points
|
||||
- Error handling
|
||||
|
||||
Format as JSON array.
|
||||
"""
|
||||
Format response as TOON tabular:
|
||||
[N,]{{name,description,steps,expected}}:
|
||||
Acceptance Criteria,Verify all criteria met,Review implementation|Check each criterion,All criteria satisfied
|
||||
""" # noqa: E501
|
||||
_response = await self.think(prompt) # Response informs test case structure
|
||||
|
||||
# Create test cases (simplified parsing)
|
||||
@@ -289,24 +286,26 @@ Format as JSON array.
|
||||
|
||||
test_case = ctx.test_cases[ctx.current_test]
|
||||
|
||||
# Use LLM to execute test
|
||||
prompt = f"""
|
||||
Execute this test case:
|
||||
# Use TOON for token-efficient context encoding
|
||||
test_context = self.format_context_labeled(
|
||||
"Test Case",
|
||||
{
|
||||
"name": test_case.name,
|
||||
"description": test_case.description,
|
||||
"steps": test_case.steps,
|
||||
"expected": test_case.expected,
|
||||
},
|
||||
)
|
||||
|
||||
Test: {test_case.name}
|
||||
Description: {test_case.description}
|
||||
Steps: {", ".join(test_case.steps)}
|
||||
Expected: {test_case.expected}
|
||||
prompt = f"""Execute this test case:
|
||||
|
||||
Simulate executing this test and provide:
|
||||
1. RESULT: PASS or FAIL
|
||||
2. ACTUAL: What was observed
|
||||
3. NOTES: Any additional findings
|
||||
{test_context}
|
||||
|
||||
Format:
|
||||
RESULT: [PASS|FAIL]
|
||||
ACTUAL: [observation]
|
||||
NOTES: [notes]
|
||||
Simulate executing this test and provide results.
|
||||
|
||||
Format response as TOON:
|
||||
{{result,actual,notes}}:
|
||||
PASS,All criteria verified successfully,No issues found
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
LLM Communication Layer
|
||||
|
||||
Provides utilities for efficient communication with Large Language Models,
|
||||
including TOON serialization for token-efficient data transfer.
|
||||
"""
|
||||
|
||||
from roboco.llm.metrics import ToonMetrics
|
||||
from roboco.llm.toon_adapter import ToonAdapter, ToonConfig
|
||||
|
||||
__all__ = [
|
||||
"ToonAdapter",
|
||||
"ToonConfig",
|
||||
"ToonMetrics",
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
TOON Metrics
|
||||
|
||||
Tracks token savings and usage statistics for TOON vs JSON serialization.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonMetrics:
|
||||
"""
|
||||
Metrics for tracking TOON serialization efficiency.
|
||||
|
||||
Tracks character counts (as proxy for tokens) for JSON vs TOON
|
||||
to measure actual savings in production.
|
||||
"""
|
||||
|
||||
json_chars: int = 0
|
||||
toon_chars: int = 0
|
||||
encode_count: int = 0
|
||||
decode_count: int = 0
|
||||
decode_fallback_count: int = 0
|
||||
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
@property
|
||||
def savings_percent(self) -> float:
|
||||
"""Calculate percentage of characters saved using TOON."""
|
||||
if self.json_chars == 0:
|
||||
return 0.0
|
||||
return (1 - self.toon_chars / self.json_chars) * 100
|
||||
|
||||
@property
|
||||
def fallback_rate(self) -> float:
|
||||
"""Calculate rate of fallback to JSON decoding."""
|
||||
if self.decode_count == 0:
|
||||
return 0.0
|
||||
return (self.decode_fallback_count / self.decode_count) * 100
|
||||
|
||||
def record_encode(self, json_chars: int, toon_chars: int) -> None:
|
||||
"""Record an encode operation with character counts."""
|
||||
self.json_chars += json_chars
|
||||
self.toon_chars += toon_chars
|
||||
self.encode_count += 1
|
||||
|
||||
def record_decode(self, used_fallback: bool = False) -> None:
|
||||
"""Record a decode operation."""
|
||||
self.decode_count += 1
|
||||
if used_fallback:
|
||||
self.decode_fallback_count += 1
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert metrics to dictionary for logging/reporting."""
|
||||
return {
|
||||
"json_chars": self.json_chars,
|
||||
"toon_chars": self.toon_chars,
|
||||
"savings_percent": round(self.savings_percent, 2),
|
||||
"encode_count": self.encode_count,
|
||||
"decode_count": self.decode_count,
|
||||
"fallback_rate": round(self.fallback_rate, 2),
|
||||
"started_at": self.started_at.isoformat(),
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all metrics."""
|
||||
self.json_chars = 0
|
||||
self.toon_chars = 0
|
||||
self.encode_count = 0
|
||||
self.decode_count = 0
|
||||
self.decode_fallback_count = 0
|
||||
self.started_at = datetime.now(UTC)
|
||||
|
||||
|
||||
# Global metrics holder
|
||||
class _MetricsHolder:
|
||||
"""Holder for singleton ToonMetrics instance."""
|
||||
|
||||
instance: ToonMetrics | None = None
|
||||
|
||||
|
||||
def get_toon_metrics() -> ToonMetrics:
|
||||
"""Get the global TOON metrics instance."""
|
||||
if _MetricsHolder.instance is None:
|
||||
_MetricsHolder.instance = ToonMetrics()
|
||||
return _MetricsHolder.instance
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
TOON Adapter
|
||||
|
||||
Provides serialization/deserialization between Python objects and TOON format
|
||||
for token-efficient LLM communication. TOON (Token-Oriented Object Notation)
|
||||
achieves 30-60% fewer tokens than JSON while maintaining semantic clarity.
|
||||
|
||||
Usage:
|
||||
adapter = ToonAdapter()
|
||||
|
||||
# Encode data for LLM prompt
|
||||
toon_str = adapter.encode({"name": "Alice", "age": 30})
|
||||
|
||||
# Decode LLM response (falls back to JSON if TOON fails)
|
||||
data = adapter.decode(response_text)
|
||||
|
||||
# Format for embedding in prompt
|
||||
prompt_section = adapter.format_for_prompt("Task Context", task_data)
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
import toon
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToonConfig:
|
||||
"""Configuration for TOON encoding."""
|
||||
|
||||
delimiter: str = ","
|
||||
indent: int = 2
|
||||
include_length: bool = True
|
||||
|
||||
|
||||
class ToonAdapter:
|
||||
"""
|
||||
Adapter for TOON serialization at LLM boundaries.
|
||||
|
||||
Converts Python dicts/Pydantic models to TOON for sending to LLMs,
|
||||
and parses TOON responses back to Python objects. Falls back to
|
||||
JSON parsing if TOON decode fails.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ToonConfig | None = None) -> None:
|
||||
"""
|
||||
Initialize the TOON adapter.
|
||||
|
||||
Args:
|
||||
config: Optional configuration for TOON encoding.
|
||||
"""
|
||||
self.config = config or ToonConfig()
|
||||
self.log = logger.bind(component="toon_adapter")
|
||||
|
||||
def encode(self, data: dict[str, Any] | list[Any] | BaseModel) -> str:
|
||||
"""
|
||||
Convert Python object to TOON for LLM consumption.
|
||||
|
||||
Args:
|
||||
data: Dictionary, list, or Pydantic model to encode.
|
||||
|
||||
Returns:
|
||||
TOON-formatted string.
|
||||
"""
|
||||
if isinstance(data, BaseModel):
|
||||
data = data.model_dump()
|
||||
|
||||
return toon.encode(data, indent=self.config.indent)
|
||||
|
||||
def decode(self, toon_str: str) -> dict[str, Any] | list[Any]:
|
||||
"""
|
||||
Parse TOON response from LLM.
|
||||
|
||||
Falls back to JSON parsing if TOON decode fails, logging a warning.
|
||||
|
||||
Args:
|
||||
toon_str: TOON-formatted string from LLM response.
|
||||
|
||||
Returns:
|
||||
Parsed Python dict or list.
|
||||
|
||||
Raises:
|
||||
ValueError: If neither TOON nor JSON parsing succeeds.
|
||||
"""
|
||||
# Try TOON first
|
||||
toon_err_msg = ""
|
||||
try:
|
||||
return toon.decode(toon_str)
|
||||
except Exception as toon_error:
|
||||
toon_err_msg = str(toon_error)
|
||||
self.log.warning(
|
||||
"TOON decode failed, trying JSON fallback",
|
||||
error=toon_err_msg,
|
||||
)
|
||||
|
||||
# Fallback to JSON
|
||||
try:
|
||||
return json.loads(toon_str)
|
||||
except json.JSONDecodeError as json_error:
|
||||
self.log.error(
|
||||
"Both TOON and JSON decode failed",
|
||||
toon_error=toon_err_msg,
|
||||
json_error=str(json_error),
|
||||
)
|
||||
raise ValueError(
|
||||
f"Failed to decode response as TOON or JSON: {toon_str[:100]}..."
|
||||
) from json_error
|
||||
|
||||
def format_for_prompt(self, label: str, data: dict[str, Any]) -> str:
|
||||
"""
|
||||
Format data with label for embedding in LLM prompt.
|
||||
|
||||
Args:
|
||||
label: Section label (e.g., "Task Context", "Requirements").
|
||||
data: Data to encode.
|
||||
|
||||
Returns:
|
||||
Formatted string suitable for prompt inclusion.
|
||||
"""
|
||||
encoded = self.encode(data)
|
||||
return f"{label}:\n{encoded}"
|
||||
|
||||
def format_tabular_request(
|
||||
self,
|
||||
fields: list[str],
|
||||
description: str,
|
||||
example_rows: list[list[str]] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Format a request for tabular TOON response.
|
||||
|
||||
Args:
|
||||
fields: Column names for the table.
|
||||
description: What the LLM should return.
|
||||
example_rows: Optional example data rows.
|
||||
|
||||
Returns:
|
||||
Formatted instruction for LLM to return TOON tabular data.
|
||||
"""
|
||||
fields_str = ",".join(fields)
|
||||
header = f"[N,]{{{fields_str}}}:"
|
||||
instruction = f"{description}\n\nFormat response as TOON tabular:\n{header}"
|
||||
|
||||
if example_rows:
|
||||
instruction += "\n"
|
||||
for row in example_rows:
|
||||
instruction += f"{self.config.delimiter.join(row)}\n"
|
||||
|
||||
return instruction
|
||||
|
||||
def estimate_token_savings(
|
||||
self,
|
||||
data: dict[str, Any] | list[Any],
|
||||
) -> tuple[int, int, float]:
|
||||
"""
|
||||
Estimate token savings of TOON vs JSON for given data.
|
||||
|
||||
Args:
|
||||
data: Data to compare.
|
||||
|
||||
Returns:
|
||||
Tuple of (json_chars, toon_chars, savings_percent).
|
||||
"""
|
||||
json_str = json.dumps(data, separators=(",", ":"))
|
||||
toon_str = self.encode(data)
|
||||
|
||||
json_chars = len(json_str)
|
||||
toon_chars = len(toon_str)
|
||||
|
||||
savings = (1 - toon_chars / json_chars) * 100 if json_chars > 0 else 0.0
|
||||
|
||||
return json_chars, toon_chars, savings
|
||||
|
||||
|
||||
# Module-level singleton holder
|
||||
class _AdapterHolder:
|
||||
"""Holder for singleton ToonAdapter instance."""
|
||||
|
||||
instance: ToonAdapter | None = None
|
||||
|
||||
|
||||
def get_toon_adapter() -> ToonAdapter:
|
||||
"""Get the default TOON adapter singleton."""
|
||||
if _AdapterHolder.instance is None:
|
||||
_AdapterHolder.instance = ToonAdapter()
|
||||
return _AdapterHolder.instance
|
||||
@@ -21,6 +21,10 @@ from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
|
||||
# Global TOON adapter for encoding journal data
|
||||
_toon = ToonAdapter()
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
@@ -141,6 +145,7 @@ def create_journal_mcp_server(agent_id: str) -> FastMCP:
|
||||
return {
|
||||
"status": "created",
|
||||
"entry": entry,
|
||||
"entry_toon": _toon.encode(entry), # TOON-encoded for LLM token efficiency
|
||||
"guidance": "Journal entry saved. Use roboco_journal_search to find past entries.",
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.agents_config import CHANNEL_ACCESS
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
|
||||
# Global TOON adapter for encoding message data
|
||||
_toon = ToonAdapter()
|
||||
|
||||
|
||||
def _check_channel_access(agent_id: str, channel_slug: str, action: str) -> bool:
|
||||
|
||||
@@ -27,6 +27,10 @@ from fastapi import status
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
|
||||
# Global TOON adapter for encoding task data
|
||||
_toon = ToonAdapter()
|
||||
|
||||
# =============================================================================
|
||||
# VALID STATE TRANSITIONS
|
||||
@@ -63,15 +67,25 @@ def _format_task_response(
|
||||
guidance: str,
|
||||
project: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Format a standardized task response with guidance."""
|
||||
"""
|
||||
Format a standardized task response with guidance.
|
||||
|
||||
Includes both JSON task data and TOON-encoded version for
|
||||
token-efficient LLM consumption.
|
||||
"""
|
||||
# Encode task data as TOON for token efficiency when LLM processes response
|
||||
task_toon = _toon.encode(task)
|
||||
|
||||
response = {
|
||||
"status": task.get("status"),
|
||||
"task": task,
|
||||
"task_toon": task_toon, # TOON-encoded for LLM token efficiency
|
||||
"next_step": next_step,
|
||||
"guidance": guidance,
|
||||
}
|
||||
if project:
|
||||
response["project"] = project
|
||||
response["project_toon"] = _toon.encode(project)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
+22
-16
@@ -5,6 +5,7 @@ Documenter handoffs contain all the information needed for
|
||||
a Documenter to create production documentation from developer work.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -225,24 +226,29 @@ class DocumenterHandoff(TimestampMixin):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def create_handoff(
|
||||
task_id: UUID,
|
||||
summary: str,
|
||||
commits: list[dict[str, str]],
|
||||
dev_notes_location: str,
|
||||
new_functionality: list[str] | None = None,
|
||||
modified_behavior: list[str] | None = None,
|
||||
breaking_changes: list[str] | None = None,
|
||||
) -> DocumenterHandoff:
|
||||
@dataclass
|
||||
class HandoffParams:
|
||||
"""Parameters for creating a handoff document."""
|
||||
|
||||
task_id: UUID
|
||||
summary: str
|
||||
commits: list[dict[str, str]]
|
||||
dev_notes_location: str
|
||||
new_functionality: list[str] = field(default_factory=list)
|
||||
modified_behavior: list[str] = field(default_factory=list)
|
||||
breaking_changes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def create_handoff(params: HandoffParams) -> DocumenterHandoff:
|
||||
"""Create a basic handoff document."""
|
||||
handoff = DocumenterHandoff(
|
||||
task_id=task_id,
|
||||
summary=summary,
|
||||
commits=commits,
|
||||
dev_notes_location=dev_notes_location,
|
||||
new_functionality=new_functionality or [],
|
||||
modified_behavior=modified_behavior or [],
|
||||
breaking_changes=breaking_changes or [],
|
||||
task_id=params.task_id,
|
||||
summary=params.summary,
|
||||
commits=params.commits,
|
||||
dev_notes_location=params.dev_notes_location,
|
||||
new_functionality=params.new_functionality,
|
||||
modified_behavior=params.modified_behavior,
|
||||
breaking_changes=params.breaking_changes,
|
||||
)
|
||||
|
||||
# Always add changelog as required
|
||||
|
||||
+117
-91
@@ -5,6 +5,7 @@ Personal agent journals for reflection, growth tracking, and debugging.
|
||||
Each agent maintains their own journal with entries tied to tasks and sessions.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -114,170 +115,195 @@ class Journal(TimestampMixin):
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def create_task_reflection(
|
||||
journal_id: UUID,
|
||||
task_id: UUID,
|
||||
title: str,
|
||||
what_done: str,
|
||||
what_learned: str,
|
||||
what_struggled: str,
|
||||
next_steps: list[str],
|
||||
tags: list[str] | None = None,
|
||||
) -> JournalEntry:
|
||||
@dataclass
|
||||
class TaskReflectionParams:
|
||||
"""Parameters for creating a task reflection entry."""
|
||||
|
||||
journal_id: UUID
|
||||
task_id: UUID
|
||||
title: str
|
||||
what_done: str
|
||||
what_learned: str
|
||||
what_struggled: str
|
||||
next_steps: list[str]
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecisionLogParams:
|
||||
"""Parameters for creating a decision log entry."""
|
||||
|
||||
journal_id: UUID
|
||||
title: str
|
||||
context: str
|
||||
options: list[dict[str, str]]
|
||||
chosen: str
|
||||
rationale: str
|
||||
consequences: list[str]
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LearningEntryParams:
|
||||
"""Parameters for creating a learning entry."""
|
||||
|
||||
journal_id: UUID
|
||||
title: str
|
||||
what_learned: str
|
||||
how_applied: str | None = None
|
||||
source: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StruggleEntryParams:
|
||||
"""Parameters for creating a struggle entry."""
|
||||
|
||||
journal_id: UUID
|
||||
title: str
|
||||
what_struggled: str
|
||||
attempted_solutions: list[str]
|
||||
resolution: str | None = None
|
||||
help_needed: str | None = None
|
||||
task_id: UUID | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneralEntryParams:
|
||||
"""Parameters for creating a general journal entry."""
|
||||
|
||||
journal_id: UUID
|
||||
title: str
|
||||
content: str
|
||||
task_id: UUID | None = None
|
||||
session_id: UUID | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
is_private: bool = False
|
||||
|
||||
|
||||
def create_task_reflection(params: TaskReflectionParams) -> JournalEntry:
|
||||
"""Create a task reflection entry."""
|
||||
content = f"""## What I Did
|
||||
{what_done}
|
||||
{params.what_done}
|
||||
|
||||
## What I Learned
|
||||
{what_learned}
|
||||
{params.what_learned}
|
||||
|
||||
## What I Struggled With
|
||||
{what_struggled}
|
||||
{params.what_struggled}
|
||||
|
||||
## Next Steps
|
||||
{chr(10).join(f"- [ ] {step}" for step in next_steps)}
|
||||
{chr(10).join(f"- [ ] {step}" for step in params.next_steps)}
|
||||
"""
|
||||
return JournalEntry(
|
||||
journal_id=journal_id,
|
||||
journal_id=params.journal_id,
|
||||
type=JournalEntryType.TASK_REFLECTION,
|
||||
title=title,
|
||||
title=params.title,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
tags=tags or [],
|
||||
task_id=params.task_id,
|
||||
tags=params.tags,
|
||||
)
|
||||
|
||||
|
||||
def create_decision_log(
|
||||
journal_id: UUID,
|
||||
title: str,
|
||||
context: str,
|
||||
options: list[dict[str, str]],
|
||||
chosen: str,
|
||||
rationale: str,
|
||||
consequences: list[str],
|
||||
task_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> JournalEntry:
|
||||
def create_decision_log(params: DecisionLogParams) -> JournalEntry:
|
||||
"""Create a decision log entry."""
|
||||
options_text = ""
|
||||
for i, opt in enumerate(options, 1):
|
||||
for i, opt in enumerate(params.options, 1):
|
||||
options_text += f"\n**Option {i}: {opt.get('name', f'Option {i}')}**\n"
|
||||
options_text += f"- Pros: {opt.get('pros', 'N/A')}\n"
|
||||
options_text += f"- Cons: {opt.get('cons', 'N/A')}\n"
|
||||
|
||||
content = f"""## Context
|
||||
{context}
|
||||
{params.context}
|
||||
|
||||
## Options Considered
|
||||
{options_text}
|
||||
|
||||
## Decision
|
||||
Chose **{chosen}** because {rationale}
|
||||
Chose **{params.chosen}** because {params.rationale}
|
||||
|
||||
## Consequences
|
||||
{chr(10).join(f"- {c}" for c in consequences)}
|
||||
{chr(10).join(f"- {c}" for c in params.consequences)}
|
||||
"""
|
||||
return JournalEntry(
|
||||
journal_id=journal_id,
|
||||
journal_id=params.journal_id,
|
||||
type=JournalEntryType.DECISION_LOG,
|
||||
title=title,
|
||||
title=params.title,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
tags=tags or [],
|
||||
task_id=params.task_id,
|
||||
tags=params.tags,
|
||||
)
|
||||
|
||||
|
||||
def create_learning_entry(
|
||||
journal_id: UUID,
|
||||
title: str,
|
||||
what_learned: str,
|
||||
how_applied: str | None = None,
|
||||
source: str | None = None,
|
||||
task_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> JournalEntry:
|
||||
def create_learning_entry(params: LearningEntryParams) -> JournalEntry:
|
||||
"""Create a learning entry."""
|
||||
content = f"""## What I Learned
|
||||
{what_learned}
|
||||
{params.what_learned}
|
||||
"""
|
||||
if how_applied:
|
||||
if params.how_applied:
|
||||
content += f"""
|
||||
## How I Applied It
|
||||
{how_applied}
|
||||
{params.how_applied}
|
||||
"""
|
||||
if source:
|
||||
if params.source:
|
||||
content += f"""
|
||||
## Source
|
||||
{source}
|
||||
{params.source}
|
||||
"""
|
||||
return JournalEntry(
|
||||
journal_id=journal_id,
|
||||
journal_id=params.journal_id,
|
||||
type=JournalEntryType.LEARNING,
|
||||
title=title,
|
||||
title=params.title,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
tags=tags or [],
|
||||
task_id=params.task_id,
|
||||
tags=params.tags,
|
||||
sentiment="positive",
|
||||
)
|
||||
|
||||
|
||||
def create_struggle_entry(
|
||||
journal_id: UUID,
|
||||
title: str,
|
||||
what_struggled: str,
|
||||
attempted_solutions: list[str],
|
||||
resolution: str | None = None,
|
||||
help_needed: str | None = None,
|
||||
task_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> JournalEntry:
|
||||
def create_struggle_entry(params: StruggleEntryParams) -> JournalEntry:
|
||||
"""Create a struggle/difficulty entry."""
|
||||
content = f"""## What I Struggled With
|
||||
{what_struggled}
|
||||
{params.what_struggled}
|
||||
|
||||
## What I Tried
|
||||
{chr(10).join(f"- {s}" for s in attempted_solutions)}
|
||||
{chr(10).join(f"- {s}" for s in params.attempted_solutions)}
|
||||
"""
|
||||
if resolution:
|
||||
if params.resolution:
|
||||
content += f"""
|
||||
## Resolution
|
||||
{resolution}
|
||||
{params.resolution}
|
||||
"""
|
||||
if help_needed:
|
||||
if params.help_needed:
|
||||
content += f"""
|
||||
## Help Needed
|
||||
{help_needed}
|
||||
{params.help_needed}
|
||||
"""
|
||||
return JournalEntry(
|
||||
journal_id=journal_id,
|
||||
journal_id=params.journal_id,
|
||||
type=JournalEntryType.STRUGGLE,
|
||||
title=title,
|
||||
title=params.title,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
tags=tags or [],
|
||||
task_id=params.task_id,
|
||||
tags=params.tags,
|
||||
sentiment="frustrated",
|
||||
)
|
||||
|
||||
|
||||
def create_general_entry(
|
||||
journal_id: UUID,
|
||||
title: str,
|
||||
content: str,
|
||||
task_id: UUID | None = None,
|
||||
session_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
is_private: bool = False,
|
||||
) -> JournalEntry:
|
||||
def create_general_entry(params: GeneralEntryParams) -> JournalEntry:
|
||||
"""Create a general journal entry."""
|
||||
return JournalEntry(
|
||||
journal_id=journal_id,
|
||||
journal_id=params.journal_id,
|
||||
type=JournalEntryType.GENERAL,
|
||||
title=title,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
session_id=session_id,
|
||||
tags=tags or [],
|
||||
is_private=is_private,
|
||||
title=params.title,
|
||||
content=params.content,
|
||||
task_id=params.task_id,
|
||||
session_id=params.session_id,
|
||||
tags=params.tags,
|
||||
is_private=params.is_private,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,19 @@ logger = structlog.get_logger()
|
||||
# Maximum length for raw excerpt storage
|
||||
MAX_EXCERPT_LENGTH = 200
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractionContext:
|
||||
"""Context for message extraction."""
|
||||
|
||||
content: str
|
||||
agent_id: UUID
|
||||
channel_id: UUID
|
||||
session_id: UUID
|
||||
group_id: UUID
|
||||
task_id: UUID | None = None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EXTRACTION PATTERNS
|
||||
# =============================================================================
|
||||
@@ -202,40 +215,27 @@ class ExtractionService:
|
||||
# Mention pattern
|
||||
self._mention_pattern = re.compile(r"@(\w+)")
|
||||
|
||||
async def extract(
|
||||
self,
|
||||
content: str,
|
||||
agent_id: UUID,
|
||||
channel_id: UUID,
|
||||
session_id: UUID,
|
||||
group_id: UUID,
|
||||
task_id: UUID | None = None,
|
||||
) -> ExtractionResult:
|
||||
async def extract(self, ctx: ExtractionContext) -> ExtractionResult:
|
||||
"""
|
||||
Extract messages from raw content.
|
||||
|
||||
Args:
|
||||
content: Raw LLM output text
|
||||
agent_id: Agent who produced the content
|
||||
channel_id: Target channel
|
||||
session_id: Current session
|
||||
group_id: Group within channel
|
||||
task_id: Optional related task
|
||||
ctx: Extraction context with content and metadata
|
||||
|
||||
Returns:
|
||||
ExtractionResult with extracted messages
|
||||
"""
|
||||
if len(content) < self.config.min_content_length:
|
||||
if len(ctx.content) < self.config.min_content_length:
|
||||
return ExtractionResult(
|
||||
messages=[],
|
||||
raw_content=content,
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
raw_content=ctx.content,
|
||||
agent_id=ctx.agent_id,
|
||||
channel_id=ctx.channel_id,
|
||||
session_id=ctx.session_id,
|
||||
)
|
||||
|
||||
# Segment the content
|
||||
segments = self._segment_content(content)
|
||||
segments = self._segment_content(ctx.content)
|
||||
|
||||
messages: list[ExtractedMessage] = []
|
||||
pattern_matches: dict[str, list[str]] = {}
|
||||
@@ -264,15 +264,15 @@ class ExtractionService:
|
||||
# Create message
|
||||
message = ExtractedMessage(
|
||||
id=uuid4(),
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
group_id=group_id,
|
||||
session_id=session_id,
|
||||
agent_id=ctx.agent_id,
|
||||
channel_id=ctx.channel_id,
|
||||
group_id=ctx.group_id,
|
||||
session_id=ctx.session_id,
|
||||
type=msg_type,
|
||||
content=segment.strip(),
|
||||
content_length=len(segment.strip()),
|
||||
mentions=mentions,
|
||||
task_id=task_id,
|
||||
task_id=ctx.task_id,
|
||||
confidence=confidence,
|
||||
raw_excerpt=segment[:MAX_EXCERPT_LENGTH]
|
||||
if len(segment) > MAX_EXCERPT_LENGTH
|
||||
@@ -284,17 +284,17 @@ class ExtractionService:
|
||||
|
||||
result = ExtractionResult(
|
||||
messages=messages,
|
||||
raw_content=content,
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
raw_content=ctx.content,
|
||||
agent_id=ctx.agent_id,
|
||||
channel_id=ctx.channel_id,
|
||||
session_id=ctx.session_id,
|
||||
pattern_matches=pattern_matches,
|
||||
confidence_scores=confidence_scores,
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Extraction complete",
|
||||
agent_id=str(agent_id),
|
||||
agent_id=str(ctx.agent_id),
|
||||
message_count=result.message_count,
|
||||
types=result.types_extracted,
|
||||
)
|
||||
@@ -365,46 +365,41 @@ class ExtractionService:
|
||||
|
||||
return best_type, confidence, matches
|
||||
|
||||
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:
|
||||
async def extract_with_llm(self, ctx: ExtractionContext) -> ExtractionResult:
|
||||
"""
|
||||
Extract messages using LLM classification.
|
||||
|
||||
This is more accurate but slower and more expensive.
|
||||
Falls back to pattern matching if LLM unavailable.
|
||||
Uses TOON format for token-efficient communication.
|
||||
"""
|
||||
from anthropic import AsyncAnthropic
|
||||
|
||||
from roboco.config import settings
|
||||
from roboco.llm import ToonAdapter
|
||||
|
||||
toon = ToonAdapter()
|
||||
|
||||
try:
|
||||
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
|
||||
# Build prompt for LLM classification
|
||||
# Build prompt for LLM classification using TOON
|
||||
prompt = f"""Analyze this agent output and classify each distinct segment.
|
||||
|
||||
Agent output:
|
||||
{content}
|
||||
{ctx.content}
|
||||
|
||||
For each segment, identify:
|
||||
- type: one of [reasoning, dialogue, decision, action, blocker, technical]
|
||||
- content: the segment text
|
||||
- confidence: 0.0 to 1.0
|
||||
|
||||
Return as JSON array of objects. Example:
|
||||
[
|
||||
{{"type": "reasoning", "content": "Analyzing the problem...", "confidence": 0.9}},
|
||||
{{"type": "action", "content": "Creating file utils.py", "confidence": 0.95}}
|
||||
]
|
||||
Return as TOON tabular format:
|
||||
[N,]{{type,content,confidence}}:
|
||||
reasoning,Analyzing the problem...,0.9
|
||||
action,Creating file utils.py,0.95
|
||||
|
||||
Output only valid JSON, no other text."""
|
||||
Output only valid TOON, no other text."""
|
||||
|
||||
response = await client.messages.create(
|
||||
model="claude-3-haiku-20240307", # Fast, cheap for classification
|
||||
@@ -412,11 +407,9 @@ Output only valid JSON, no other text."""
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
|
||||
# Parse response
|
||||
import json
|
||||
|
||||
# Parse response using TOON (falls back to JSON)
|
||||
response_text = response.content[0].text
|
||||
segments = json.loads(response_text)
|
||||
segments = toon.decode(response_text)
|
||||
|
||||
messages: list[ExtractedMessage] = []
|
||||
for segment in segments:
|
||||
@@ -430,11 +423,11 @@ Output only valid JSON, no other text."""
|
||||
id=uuid4(),
|
||||
content=msg_content,
|
||||
message_type=msg_type,
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
group_id=group_id,
|
||||
task_id=task_id,
|
||||
agent_id=ctx.agent_id,
|
||||
channel_id=ctx.channel_id,
|
||||
session_id=ctx.session_id,
|
||||
group_id=ctx.group_id,
|
||||
task_id=ctx.task_id,
|
||||
confidence=confidence,
|
||||
metadata={"extraction_method": "llm"},
|
||||
)
|
||||
@@ -442,7 +435,7 @@ Output only valid JSON, no other text."""
|
||||
|
||||
return ExtractionResult(
|
||||
messages=messages,
|
||||
raw_content=content,
|
||||
raw_content=ctx.content,
|
||||
extraction_time=0.0, # Could measure actual time
|
||||
confidence=sum(m.confidence for m in messages) / max(1, len(messages)),
|
||||
)
|
||||
@@ -450,14 +443,7 @@ Output only valid JSON, no other text."""
|
||||
except Exception as e:
|
||||
# Fall back to pattern matching
|
||||
self.log.warning("LLM extraction failed, using patterns", error=str(e))
|
||||
return await self.extract(
|
||||
content=content,
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
group_id=group_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
return await self.extract(ctx)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -496,26 +482,11 @@ class ExtractionPipeline:
|
||||
"""Register a callback for extracted messages."""
|
||||
self._message_callbacks.append(callback)
|
||||
|
||||
async def process_buffer(
|
||||
self,
|
||||
content: str,
|
||||
agent_id: UUID,
|
||||
channel_id: UUID,
|
||||
session_id: UUID,
|
||||
group_id: UUID,
|
||||
task_id: UUID | None = None,
|
||||
) -> ExtractionResult:
|
||||
async def process_buffer(self, ctx: ExtractionContext) -> ExtractionResult:
|
||||
"""
|
||||
Process a buffer and invoke callbacks for each message.
|
||||
"""
|
||||
result = await self.extraction.extract(
|
||||
content=content,
|
||||
agent_id=agent_id,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
group_id=group_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
result = await self.extraction.extract(ctx)
|
||||
|
||||
# Invoke callbacks for each message
|
||||
for message in result.messages:
|
||||
|
||||
@@ -1796,7 +1796,7 @@ name = "nvidia-cudnn-cu12"
|
||||
version = "9.10.2.21"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
|
||||
@@ -1807,7 +1807,7 @@ name = "nvidia-cufft-cu12"
|
||||
version = "11.3.3.83"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
|
||||
@@ -1834,9 +1834,9 @@ name = "nvidia-cusolver-cu12"
|
||||
version = "11.7.3.90"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
{ name = "nvidia-cusparse-cu12" },
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
|
||||
@@ -1847,7 +1847,7 @@ name = "nvidia-cusparse-cu12"
|
||||
version = "12.5.8.93"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
|
||||
@@ -2062,7 +2062,7 @@ name = "pexpect"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "ptyprocess" },
|
||||
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
||||
wheels = [
|
||||
@@ -2577,6 +2577,15 @@ wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-toon"
|
||||
version = "0.1.3"
|
||||
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
|
||||
sdist = { url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/4e/92/640c83ca46d5fe9c49895449a8932f55252537dd13dd22186cbac3a1ce59/python_toon-0.1.3.tar.gz", hash = "sha256:ca348b214c4f1cdad3579fd83dd60032d9eb87eb349c2d430ad9eb6371f174bf", size = 31280, upload-time = "2025-11-04T09:12:20.949Z" }
|
||||
wheels = [
|
||||
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/26/a4/2f2def0378b44f913d2d6cb3bc5b1a15267b363937ab1cb9afb07ce2313c/python_toon-0.1.3-py3-none-any.whl", hash = "sha256:a27b0ee4a729e730d1037d0a63eb8b344b3e5a26e3dc9a173067b6c31a868ee6", size = 21797, upload-time = "2025-11-04T09:12:19.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
@@ -2781,6 +2790,7 @@ dependencies = [
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "python-toon" },
|
||||
{ name = "redis" },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
{ name = "structlog" },
|
||||
@@ -2842,6 +2852,7 @@ requires-dist = [
|
||||
{ name = "pytest-xdist", marker = "extra == 'dev'" },
|
||||
{ name = "python-jose", extras = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "python-toon" },
|
||||
{ name = "redis" },
|
||||
{ name = "rich", marker = "extra == 'dev'" },
|
||||
{ name = "ruff", marker = "extra == 'dev'" },
|
||||
|
||||
Reference in New Issue
Block a user