| | |
| | from typing import Dict, List, Literal, Optional |
| |
|
| | from app.exceptions import ToolError |
| | from app.tool.base import BaseTool, ToolResult |
| |
|
| |
|
| | _PLANNING_TOOL_DESCRIPTION = """ |
| | A planning tool that allows the agent to create and manage plans for solving complex tasks. |
| | The tool provides functionality for creating plans, updating plan steps, and tracking progress. |
| | """ |
| |
|
| |
|
| | class PlanningTool(BaseTool): |
| | """ |
| | A planning tool that allows the agent to create and manage plans for solving complex tasks. |
| | The tool provides functionality for creating plans, updating plan steps, and tracking progress. |
| | """ |
| |
|
| | name: str = "planning" |
| | description: str = _PLANNING_TOOL_DESCRIPTION |
| | parameters: dict = { |
| | "type": "object", |
| | "properties": { |
| | "command": { |
| | "description": "The command to execute. Available commands: create, update, list, get, set_active, mark_step, delete.", |
| | "enum": [ |
| | "create", |
| | "update", |
| | "list", |
| | "get", |
| | "set_active", |
| | "mark_step", |
| | "delete", |
| | ], |
| | "type": "string", |
| | }, |
| | "plan_id": { |
| | "description": "Unique identifier for the plan. Required for create, update, set_active, and delete commands. Optional for get and mark_step (uses active plan if not specified).", |
| | "type": "string", |
| | }, |
| | "title": { |
| | "description": "Title for the plan. Required for create command, optional for update command.", |
| | "type": "string", |
| | }, |
| | "steps": { |
| | "description": "List of plan steps. Required for create command, optional for update command.", |
| | "type": "array", |
| | "items": {"type": "string"}, |
| | }, |
| | "step_index": { |
| | "description": "Index of the step to update (0-based). Required for mark_step command.", |
| | "type": "integer", |
| | }, |
| | "step_status": { |
| | "description": "Status to set for a step. Used with mark_step command.", |
| | "enum": ["not_started", "in_progress", "completed", "blocked"], |
| | "type": "string", |
| | }, |
| | "step_notes": { |
| | "description": "Additional notes for a step. Optional for mark_step command.", |
| | "type": "string", |
| | }, |
| | }, |
| | "required": ["command"], |
| | "additionalProperties": False, |
| | } |
| |
|
| | plans: dict = {} |
| | _current_plan_id: Optional[str] = None |
| |
|
| | async def execute( |
| | self, |
| | *, |
| | command: Literal[ |
| | "create", "update", "list", "get", "set_active", "mark_step", "delete" |
| | ], |
| | plan_id: Optional[str] = None, |
| | title: Optional[str] = None, |
| | steps: Optional[List[str]] = None, |
| | step_index: Optional[int] = None, |
| | step_status: Optional[ |
| | Literal["not_started", "in_progress", "completed", "blocked"] |
| | ] = None, |
| | step_notes: Optional[str] = None, |
| | **kwargs, |
| | ): |
| | """ |
| | Execute the planning tool with the given command and parameters. |
| | |
| | Parameters: |
| | - command: The operation to perform |
| | - plan_id: Unique identifier for the plan |
| | - title: Title for the plan (used with create command) |
| | - steps: List of steps for the plan (used with create command) |
| | - step_index: Index of the step to update (used with mark_step command) |
| | - step_status: Status to set for a step (used with mark_step command) |
| | - step_notes: Additional notes for a step (used with mark_step command) |
| | """ |
| |
|
| | if command == "create": |
| | return self._create_plan(plan_id, title, steps) |
| | elif command == "update": |
| | return self._update_plan(plan_id, title, steps) |
| | elif command == "list": |
| | return self._list_plans() |
| | elif command == "get": |
| | return self._get_plan(plan_id) |
| | elif command == "set_active": |
| | return self._set_active_plan(plan_id) |
| | elif command == "mark_step": |
| | return self._mark_step(plan_id, step_index, step_status, step_notes) |
| | elif command == "delete": |
| | return self._delete_plan(plan_id) |
| | else: |
| | raise ToolError( |
| | f"Unrecognized command: {command}. Allowed commands are: create, update, list, get, set_active, mark_step, delete" |
| | ) |
| |
|
| | def _create_plan( |
| | self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]] |
| | ) -> ToolResult: |
| | """Create a new plan with the given ID, title, and steps.""" |
| | if not plan_id: |
| | raise ToolError("Parameter `plan_id` is required for command: create") |
| |
|
| | if plan_id in self.plans: |
| | raise ToolError( |
| | f"A plan with ID '{plan_id}' already exists. Use 'update' to modify existing plans." |
| | ) |
| |
|
| | if not title: |
| | raise ToolError("Parameter `title` is required for command: create") |
| |
|
| | if ( |
| | not steps |
| | or not isinstance(steps, list) |
| | or not all(isinstance(step, str) for step in steps) |
| | ): |
| | raise ToolError( |
| | "Parameter `steps` must be a non-empty list of strings for command: create" |
| | ) |
| |
|
| | |
| | plan = { |
| | "plan_id": plan_id, |
| | "title": title, |
| | "steps": steps, |
| | "step_statuses": ["not_started"] * len(steps), |
| | "step_notes": [""] * len(steps), |
| | } |
| |
|
| | self.plans[plan_id] = plan |
| | self._current_plan_id = plan_id |
| |
|
| | return ToolResult( |
| | output=f"Plan created successfully with ID: {plan_id}\n\n{self._format_plan(plan)}" |
| | ) |
| |
|
| | def _update_plan( |
| | self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]] |
| | ) -> ToolResult: |
| | """Update an existing plan with new title or steps.""" |
| | if not plan_id: |
| | raise ToolError("Parameter `plan_id` is required for command: update") |
| |
|
| | if plan_id not in self.plans: |
| | raise ToolError(f"No plan found with ID: {plan_id}") |
| |
|
| | plan = self.plans[plan_id] |
| |
|
| | if title: |
| | plan["title"] = title |
| |
|
| | if steps: |
| | if not isinstance(steps, list) or not all( |
| | isinstance(step, str) for step in steps |
| | ): |
| | raise ToolError( |
| | "Parameter `steps` must be a list of strings for command: update" |
| | ) |
| |
|
| | |
| | old_steps = plan["steps"] |
| | old_statuses = plan["step_statuses"] |
| | old_notes = plan["step_notes"] |
| |
|
| | |
| | new_statuses = [] |
| | new_notes = [] |
| |
|
| | for i, step in enumerate(steps): |
| | |
| | if i < len(old_steps) and step == old_steps[i]: |
| | new_statuses.append(old_statuses[i]) |
| | new_notes.append(old_notes[i]) |
| | else: |
| | new_statuses.append("not_started") |
| | new_notes.append("") |
| |
|
| | plan["steps"] = steps |
| | plan["step_statuses"] = new_statuses |
| | plan["step_notes"] = new_notes |
| |
|
| | return ToolResult( |
| | output=f"Plan updated successfully: {plan_id}\n\n{self._format_plan(plan)}" |
| | ) |
| |
|
| | def _list_plans(self) -> ToolResult: |
| | """List all available plans.""" |
| | if not self.plans: |
| | return ToolResult( |
| | output="No plans available. Create a plan with the 'create' command." |
| | ) |
| |
|
| | output = "Available plans:\n" |
| | for plan_id, plan in self.plans.items(): |
| | current_marker = " (active)" if plan_id == self._current_plan_id else "" |
| | completed = sum( |
| | 1 for status in plan["step_statuses"] if status == "completed" |
| | ) |
| | total = len(plan["steps"]) |
| | progress = f"{completed}/{total} steps completed" |
| | output += f"• {plan_id}{current_marker}: {plan['title']} - {progress}\n" |
| |
|
| | return ToolResult(output=output) |
| |
|
| | def _get_plan(self, plan_id: Optional[str]) -> ToolResult: |
| | """Get details of a specific plan.""" |
| | if not plan_id: |
| | |
| | if not self._current_plan_id: |
| | raise ToolError( |
| | "No active plan. Please specify a plan_id or set an active plan." |
| | ) |
| | plan_id = self._current_plan_id |
| |
|
| | if plan_id not in self.plans: |
| | raise ToolError(f"No plan found with ID: {plan_id}") |
| |
|
| | plan = self.plans[plan_id] |
| | return ToolResult(output=self._format_plan(plan)) |
| |
|
| | def _set_active_plan(self, plan_id: Optional[str]) -> ToolResult: |
| | """Set a plan as the active plan.""" |
| | if not plan_id: |
| | raise ToolError("Parameter `plan_id` is required for command: set_active") |
| |
|
| | if plan_id not in self.plans: |
| | raise ToolError(f"No plan found with ID: {plan_id}") |
| |
|
| | self._current_plan_id = plan_id |
| | return ToolResult( |
| | output=f"Plan '{plan_id}' is now the active plan.\n\n{self._format_plan(self.plans[plan_id])}" |
| | ) |
| |
|
| | def _mark_step( |
| | self, |
| | plan_id: Optional[str], |
| | step_index: Optional[int], |
| | step_status: Optional[str], |
| | step_notes: Optional[str], |
| | ) -> ToolResult: |
| | """Mark a step with a specific status and optional notes.""" |
| | if not plan_id: |
| | |
| | if not self._current_plan_id: |
| | raise ToolError( |
| | "No active plan. Please specify a plan_id or set an active plan." |
| | ) |
| | plan_id = self._current_plan_id |
| |
|
| | if plan_id not in self.plans: |
| | raise ToolError(f"No plan found with ID: {plan_id}") |
| |
|
| | if step_index is None: |
| | raise ToolError("Parameter `step_index` is required for command: mark_step") |
| |
|
| | plan = self.plans[plan_id] |
| |
|
| | if step_index < 0 or step_index >= len(plan["steps"]): |
| | raise ToolError( |
| | f"Invalid step_index: {step_index}. Valid indices range from 0 to {len(plan['steps'])-1}." |
| | ) |
| |
|
| | if step_status and step_status not in [ |
| | "not_started", |
| | "in_progress", |
| | "completed", |
| | "blocked", |
| | ]: |
| | raise ToolError( |
| | f"Invalid step_status: {step_status}. Valid statuses are: not_started, in_progress, completed, blocked" |
| | ) |
| |
|
| | if step_status: |
| | plan["step_statuses"][step_index] = step_status |
| |
|
| | if step_notes: |
| | plan["step_notes"][step_index] = step_notes |
| |
|
| | return ToolResult( |
| | output=f"Step {step_index} updated in plan '{plan_id}'.\n\n{self._format_plan(plan)}" |
| | ) |
| |
|
| | def _delete_plan(self, plan_id: Optional[str]) -> ToolResult: |
| | """Delete a plan.""" |
| | if not plan_id: |
| | raise ToolError("Parameter `plan_id` is required for command: delete") |
| |
|
| | if plan_id not in self.plans: |
| | raise ToolError(f"No plan found with ID: {plan_id}") |
| |
|
| | del self.plans[plan_id] |
| |
|
| | |
| | if self._current_plan_id == plan_id: |
| | self._current_plan_id = None |
| |
|
| | return ToolResult(output=f"Plan '{plan_id}' has been deleted.") |
| |
|
| | def _format_plan(self, plan: Dict) -> str: |
| | """Format a plan for display.""" |
| | output = f"Plan: {plan['title']} (ID: {plan['plan_id']})\n" |
| | output += "=" * len(output) + "\n\n" |
| |
|
| | |
| | total_steps = len(plan["steps"]) |
| | completed = sum(1 for status in plan["step_statuses"] if status == "completed") |
| | in_progress = sum( |
| | 1 for status in plan["step_statuses"] if status == "in_progress" |
| | ) |
| | blocked = sum(1 for status in plan["step_statuses"] if status == "blocked") |
| | not_started = sum( |
| | 1 for status in plan["step_statuses"] if status == "not_started" |
| | ) |
| |
|
| | output += f"Progress: {completed}/{total_steps} steps completed " |
| | if total_steps > 0: |
| | percentage = (completed / total_steps) * 100 |
| | output += f"({percentage:.1f}%)\n" |
| | else: |
| | output += "(0%)\n" |
| |
|
| | output += f"Status: {completed} completed, {in_progress} in progress, {blocked} blocked, {not_started} not started\n\n" |
| | output += "Steps:\n" |
| |
|
| | |
| | for i, (step, status, notes) in enumerate( |
| | zip(plan["steps"], plan["step_statuses"], plan["step_notes"]) |
| | ): |
| | status_symbol = { |
| | "not_started": "[ ]", |
| | "in_progress": "[→]", |
| | "completed": "[✓]", |
| | "blocked": "[!]", |
| | }.get(status, "[ ]") |
| |
|
| | output += f"{i}. {status_symbol} {step}\n" |
| | if notes: |
| | output += f" Notes: {notes}\n" |
| |
|
| | return output |
| |
|