The architecture has been updated
This commit is contained in:
parent
805f7a017e
commit
a01257ead9
1119 changed files with 226 additions and 352 deletions
|
|
@ -1,268 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Todo Tool Module - Planning & Task Management
|
||||
|
||||
Provides an in-memory task list the agent uses to decompose complex tasks,
|
||||
track progress, and maintain focus across long conversations. The state
|
||||
lives on the AIAgent instance (one per session) and is re-injected into
|
||||
the conversation after context compression events.
|
||||
|
||||
Design:
|
||||
- Single `todo` tool: provide `todos` param to write, omit to read
|
||||
- Every call returns the full current list
|
||||
- No system prompt mutation, no tool response modification
|
||||
- Behavioral guidance lives entirely in the tool schema description
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
|
||||
# Valid status values for todo items
|
||||
VALID_STATUSES = {"pending", "in_progress", "completed", "cancelled"}
|
||||
|
||||
|
||||
class TodoStore:
|
||||
"""
|
||||
In-memory todo list. One instance per AIAgent (one per session).
|
||||
|
||||
Items are ordered -- list position is priority. Each item has:
|
||||
- id: unique string identifier (agent-chosen)
|
||||
- content: task description
|
||||
- status: pending | in_progress | completed | cancelled
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._items: List[Dict[str, str]] = []
|
||||
|
||||
def write(self, todos: List[Dict[str, Any]], merge: bool = False) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Write todos. Returns the full current list after writing.
|
||||
|
||||
Args:
|
||||
todos: list of {id, content, status} dicts
|
||||
merge: if False, replace the entire list. If True, update
|
||||
existing items by id and append new ones.
|
||||
"""
|
||||
if not merge:
|
||||
# Replace mode: new list entirely
|
||||
self._items = [self._validate(t) for t in todos]
|
||||
else:
|
||||
# Merge mode: update existing items by id, append new ones
|
||||
existing = {item["id"]: item for item in self._items}
|
||||
for t in todos:
|
||||
item_id = str(t.get("id", "")).strip()
|
||||
if not item_id:
|
||||
continue # Can't merge without an id
|
||||
|
||||
if item_id in existing:
|
||||
# Update only the fields the LLM actually provided
|
||||
if "content" in t and t["content"]:
|
||||
existing[item_id]["content"] = str(t["content"]).strip()
|
||||
if "status" in t and t["status"]:
|
||||
status = str(t["status"]).strip().lower()
|
||||
if status in VALID_STATUSES:
|
||||
existing[item_id]["status"] = status
|
||||
else:
|
||||
# New item -- validate fully and append to end
|
||||
validated = self._validate(t)
|
||||
existing[validated["id"]] = validated
|
||||
self._items.append(validated)
|
||||
# Rebuild _items preserving order for existing items
|
||||
seen = set()
|
||||
rebuilt = []
|
||||
for item in self._items:
|
||||
current = existing.get(item["id"], item)
|
||||
if current["id"] not in seen:
|
||||
rebuilt.append(current)
|
||||
seen.add(current["id"])
|
||||
self._items = rebuilt
|
||||
return self.read()
|
||||
|
||||
def read(self) -> List[Dict[str, str]]:
|
||||
"""Return a copy of the current list."""
|
||||
return [item.copy() for item in self._items]
|
||||
|
||||
def has_items(self) -> bool:
|
||||
"""Check if there are any items in the list."""
|
||||
return len(self._items) > 0
|
||||
|
||||
def format_for_injection(self) -> Optional[str]:
|
||||
"""
|
||||
Render the todo list for post-compression injection.
|
||||
|
||||
Returns a human-readable string to append to the compressed
|
||||
message history, or None if the list is empty.
|
||||
"""
|
||||
if not self._items:
|
||||
return None
|
||||
|
||||
# Status markers for compact display
|
||||
markers = {
|
||||
"completed": "[x]",
|
||||
"in_progress": "[>]",
|
||||
"pending": "[ ]",
|
||||
"cancelled": "[~]",
|
||||
}
|
||||
|
||||
# Only inject pending/in_progress items — completed/cancelled ones
|
||||
# cause the model to re-do finished work after compression.
|
||||
active_items = [
|
||||
item for item in self._items
|
||||
if item["status"] in ("pending", "in_progress")
|
||||
]
|
||||
if not active_items:
|
||||
return None
|
||||
|
||||
lines = ["[Your active task list was preserved across context compression]"]
|
||||
for item in active_items:
|
||||
marker = markers.get(item["status"], "[?]")
|
||||
lines.append(f"- {marker} {item['id']}. {item['content']} ({item['status']})")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _validate(item: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""
|
||||
Validate and normalize a todo item.
|
||||
|
||||
Ensures required fields exist and status is valid.
|
||||
Returns a clean dict with only {id, content, status}.
|
||||
"""
|
||||
item_id = str(item.get("id", "")).strip()
|
||||
if not item_id:
|
||||
item_id = "?"
|
||||
|
||||
content = str(item.get("content", "")).strip()
|
||||
if not content:
|
||||
content = "(no description)"
|
||||
|
||||
status = str(item.get("status", "pending")).strip().lower()
|
||||
if status not in VALID_STATUSES:
|
||||
status = "pending"
|
||||
|
||||
return {"id": item_id, "content": content, "status": status}
|
||||
|
||||
|
||||
def todo_tool(
|
||||
todos: Optional[List[Dict[str, Any]]] = None,
|
||||
merge: bool = False,
|
||||
store: Optional[TodoStore] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Single entry point for the todo tool. Reads or writes depending on params.
|
||||
|
||||
Args:
|
||||
todos: if provided, write these items. If None, read current list.
|
||||
merge: if True, update by id. If False (default), replace entire list.
|
||||
store: the TodoStore instance from the AIAgent.
|
||||
|
||||
Returns:
|
||||
JSON string with the full current list and summary metadata.
|
||||
"""
|
||||
if store is None:
|
||||
return json.dumps({"error": "TodoStore not initialized"}, ensure_ascii=False)
|
||||
|
||||
if todos is not None:
|
||||
items = store.write(todos, merge)
|
||||
else:
|
||||
items = store.read()
|
||||
|
||||
# Build summary counts
|
||||
pending = sum(1 for i in items if i["status"] == "pending")
|
||||
in_progress = sum(1 for i in items if i["status"] == "in_progress")
|
||||
completed = sum(1 for i in items if i["status"] == "completed")
|
||||
cancelled = sum(1 for i in items if i["status"] == "cancelled")
|
||||
|
||||
return json.dumps({
|
||||
"todos": items,
|
||||
"summary": {
|
||||
"total": len(items),
|
||||
"pending": pending,
|
||||
"in_progress": in_progress,
|
||||
"completed": completed,
|
||||
"cancelled": cancelled,
|
||||
},
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
def check_todo_requirements() -> bool:
|
||||
"""Todo tool has no external requirements -- always available."""
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OpenAI Function-Calling Schema
|
||||
# =============================================================================
|
||||
# Behavioral guidance is baked into the description so it's part of the
|
||||
# static tool schema (cached, never changes mid-conversation).
|
||||
|
||||
TODO_SCHEMA = {
|
||||
"name": "todo",
|
||||
"description": (
|
||||
"Manage your task list for the current session. Use for complex tasks "
|
||||
"with 3+ steps or when the user provides multiple tasks. "
|
||||
"Call with no parameters to read the current list.\n\n"
|
||||
"Writing:\n"
|
||||
"- Provide 'todos' array to create/update items\n"
|
||||
"- merge=false (default): replace the entire list with a fresh plan\n"
|
||||
"- merge=true: update existing items by id, add any new ones\n\n"
|
||||
"Each item: {id: string, content: string, "
|
||||
"status: pending|in_progress|completed|cancelled}\n"
|
||||
"List order is priority. Only ONE item in_progress at a time.\n"
|
||||
"Mark items completed immediately when done. If something fails, "
|
||||
"cancel it and add a revised item.\n\n"
|
||||
"Always returns the full current list."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"todos": {
|
||||
"type": "array",
|
||||
"description": "Task items to write. Omit to read current list.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique item identifier"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Task description"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "in_progress", "completed", "cancelled"],
|
||||
"description": "Current status"
|
||||
}
|
||||
},
|
||||
"required": ["id", "content", "status"]
|
||||
}
|
||||
},
|
||||
"merge": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"true: update existing items by id, add new ones. "
|
||||
"false (default): replace the entire list."
|
||||
),
|
||||
"default": False
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# --- Registry ---
|
||||
from tools.registry import registry
|
||||
|
||||
registry.register(
|
||||
name="todo",
|
||||
toolset="todo",
|
||||
schema=TODO_SCHEMA,
|
||||
handler=lambda args, **kw: todo_tool(
|
||||
todos=args.get("todos"), merge=args.get("merge", False), store=kw.get("store")),
|
||||
check_fn=check_todo_requirements,
|
||||
emoji="📋",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue