Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
todo.py346 linesDownload Raw Back to middleware
1"""Planning and task management middleware for agents."""2 3from collections.abc import Awaitable, Callable4from typing import Annotated, Any, Literal, cast5 6from langchain_core.messages import AIMessage, SystemMessage, ToolMessage7from langchain_core.tools import InjectedToolCallId, StructuredTool, tool8from langgraph.runtime import Runtime9from langgraph.types import Command10from pydantic import BaseModel11from typing_extensions import NotRequired, TypedDict, override12 13from langchain.agents.middleware.types import (14    AgentMiddleware,15    AgentState,16    ContextT,17    ModelRequest,18    ModelResponse,19    OmitFromInput,20    ResponseT,21)22from langchain.tools import ToolRuntime23 24 25class Todo(TypedDict):26    """A single todo item with content and status."""27 28    content: str29    """The content/description of the todo item."""30 31    status: Literal["pending", "in_progress", "completed"]32    """The current status of the todo item."""33 34 35class PlanningState(AgentState[ResponseT]):36    """State schema for the todo middleware.37 38    Type Parameters:39        ResponseT: The type of the structured response. Defaults to `Any`.40    """41 42    todos: Annotated[NotRequired[list[Todo]], OmitFromInput]43    """List of todo items for tracking task progress."""44 45 46class WriteTodosInput(BaseModel):47    """Input schema for the `write_todos` tool."""48 49    todos: list[Todo]50 51 52WRITE_TODOS_TOOL_DESCRIPTION = """Use this tool to create and manage a structured task list for your current work session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.53 54Only use this tool if you think it will be helpful in staying organized. If the user's request is trivial and takes less than 3 steps, it is better to NOT use this tool and just do the task directly.55 56## When to Use This Tool57Use this tool in these scenarios:58 591. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions602. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations613. User explicitly requests todo list - When the user directly asks you to use the todo list624. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)635. The plan may need future revisions or updates based on results from the first few steps64 65## How to Use This Tool661. When you start working on a task - Mark it as in_progress BEFORE beginning work.672. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation.683. You can also update future tasks, such as deleting them if they are no longer necessary, or adding new tasks that are necessary. Don't change previously completed tasks.694. You can make several updates to the todo list at once. For example, when you complete a task, you can mark the next task you need to start as in_progress.70 71## When NOT to Use This Tool72It is important to skip using this tool when:731. There is only a single, straightforward task742. The task is trivial and tracking it provides no benefit753. The task can be completed in less than 3 trivial steps764. The task is purely conversational or informational77 78## Task States and Management79 801. **Task States**: Use these states to track progress:81   - pending: Task not yet started82   - in_progress: Currently working on (you can have multiple tasks in_progress at a time if they are not related to each other and can be run in parallel)83   - completed: Task finished successfully84 852. **Task Management**:86   - Update task status in real-time as you work87   - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)88   - Complete current tasks before starting new ones89   - Remove tasks that are no longer relevant from the list entirely90   - IMPORTANT: When you write this todo list, you should mark your first task (or tasks) as in_progress immediately!.91   - IMPORTANT: Unless all tasks are completed, you should always have at least one task in_progress to show the user that you are working on something.92 933. **Task Completion Requirements**:94   - ONLY mark a task as completed when you have FULLY accomplished it95   - If you encounter errors, blockers, or cannot finish, keep the task as in_progress96   - When blocked, create a new task describing what needs to be resolved97   - Never mark a task as completed if:98     - There are unresolved issues or errors99     - Work is partial or incomplete100     - You encountered blockers that prevent completion101     - You couldn't find necessary resources or dependencies102     - Quality standards haven't been met103 1044. **Task Breakdown**:105   - Create specific, actionable items106   - Break complex tasks into smaller, manageable steps107   - Use clear, descriptive task names108 109Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully110Remember: If you only need to make a few tool calls to complete a task, and it is clear what you need to do, it is better to just do the task directly and NOT call this tool at all."""  # noqa: E501111 112WRITE_TODOS_SYSTEM_PROMPT = """## `write_todos`113 114You have access to the `write_todos` tool to help you manage and plan complex objectives.115Use this tool for complex objectives to ensure that you are tracking each necessary step and giving the user visibility into your progress.116This tool is very helpful for planning complex objectives, and for breaking down these larger complex objectives into smaller steps.117 118It is critical that you mark todos as completed as soon as you are done with a step. Do not batch up multiple steps before marking them as completed.119For simple objectives that only require a few steps, it is better to just complete the objective directly and NOT use this tool.120Writing todos takes time and tokens, use it when it is helpful for managing complex many-step problems! But not for simple few-step requests.121 122## Important To-Do List Usage Notes to Remember123- The `write_todos` tool should never be called multiple times in parallel.124- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant."""  # noqa: E501125 126 127@tool(description=WRITE_TODOS_TOOL_DESCRIPTION)128def write_todos(129    todos: list[Todo], tool_call_id: Annotated[str, InjectedToolCallId]130) -> Command[Any]:131    """Create and manage a structured task list for your current work session."""132    return Command(133        update={134            "todos": todos,135            "messages": [ToolMessage(f"Updated todo list to {todos}", tool_call_id=tool_call_id)],136        }137    )138 139 140# Dynamically create the write_todos tool with the custom description141def _write_todos(142    runtime: ToolRuntime[ContextT, PlanningState[ResponseT]], todos: list[Todo]143) -> Command[Any]:144    """Create and manage a structured task list for your current work session."""145    return Command(146        update={147            "todos": todos,148            "messages": [149                ToolMessage(f"Updated todo list to {todos}", tool_call_id=runtime.tool_call_id)150            ],151        }152    )153 154 155async def _awrite_todos(156    runtime: ToolRuntime[ContextT, PlanningState[ResponseT]], todos: list[Todo]157) -> Command[Any]:158    """Create and manage a structured task list for your current work session."""159    return _write_todos(runtime, todos)160 161 162class TodoListMiddleware(AgentMiddleware[PlanningState[ResponseT], ContextT, ResponseT]):163    """Middleware that provides todo list management capabilities to agents.164 165    This middleware adds a `write_todos` tool that allows agents to create and manage166    structured task lists for complex multi-step operations. It's designed to help167    agents track progress, organize complex tasks, and provide users with visibility168    into task completion status.169 170    The middleware automatically injects system prompts that guide the agent on when171    and how to use the todo functionality effectively. It also enforces that the172    `write_todos` tool is called at most once per model turn, since the tool replaces173    the entire todo list and parallel calls would create ambiguity about precedence.174 175    Example:176        ```python177        from langchain.agents.middleware import TodoListMiddleware178        from langchain.agents import create_agent179 180        agent = create_agent("openai:gpt-4o", middleware=[TodoListMiddleware()])181 182        # Agent now has access to write_todos tool and todo state tracking183        result = await agent.invoke({"messages": [HumanMessage("Help me refactor my codebase")]})184 185        print(result["todos"])  # Array of todo items with status tracking186        ```187    """188 189    state_schema = PlanningState  # type: ignore[assignment]190 191    def __init__(192        self,193        *,194        system_prompt: str = WRITE_TODOS_SYSTEM_PROMPT,195        tool_description: str = WRITE_TODOS_TOOL_DESCRIPTION,196    ) -> None:197        """Initialize the `TodoListMiddleware` with optional custom prompts.198 199        Args:200            system_prompt: Custom system prompt to guide the agent on using the todo201                tool.202            tool_description: Custom description for the `write_todos` tool.203        """204        super().__init__()205        self.system_prompt = system_prompt206        self.tool_description = tool_description207 208        self.tools = [209            StructuredTool.from_function(210                name="write_todos",211                description=tool_description,212                func=_write_todos,213                coroutine=_awrite_todos,214                args_schema=WriteTodosInput,215                infer_schema=False,216            )217        ]218 219    def wrap_model_call(220        self,221        request: ModelRequest[ContextT],222        handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],223    ) -> ModelResponse[ResponseT] | AIMessage:224        """Update the system message to include the todo system prompt.225 226        Args:227            request: Model request to execute (includes state and runtime).228            handler: Async callback that executes the model request and returns229                `ModelResponse`.230 231        Returns:232            The model call result.233        """234        if request.system_message is not None:235            new_system_content = [236                *request.system_message.content_blocks,237                {"type": "text", "text": f"\n\n{self.system_prompt}"},238            ]239        else:240            new_system_content = [{"type": "text", "text": self.system_prompt}]241        new_system_message = SystemMessage(242            content=cast("list[str | dict[str, str]]", new_system_content)243        )244        return handler(request.override(system_message=new_system_message))245 246    async def awrap_model_call(247        self,248        request: ModelRequest[ContextT],249        handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],250    ) -> ModelResponse[ResponseT] | AIMessage:251        """Update the system message to include the todo system prompt.252 253        Args:254            request: Model request to execute (includes state and runtime).255            handler: Async callback that executes the model request and returns256                `ModelResponse`.257 258        Returns:259            The model call result.260        """261        if request.system_message is not None:262            new_system_content = [263                *request.system_message.content_blocks,264                {"type": "text", "text": f"\n\n{self.system_prompt}"},265            ]266        else:267            new_system_content = [{"type": "text", "text": self.system_prompt}]268        new_system_message = SystemMessage(269            content=cast("list[str | dict[str, str]]", new_system_content)270        )271        return await handler(request.override(system_message=new_system_message))272 273    @override274    def after_model(275        self, state: PlanningState[ResponseT], runtime: Runtime[ContextT]276    ) -> dict[str, Any] | None:277        """Check for parallel write_todos tool calls and return errors if detected.278 279        The todo list is designed to be updated at most once per model turn. Since280        the `write_todos` tool replaces the entire todo list with each call, making281        multiple parallel calls would create ambiguity about which update should take282        precedence. This method prevents such conflicts by rejecting any response that283        contains multiple write_todos tool calls.284 285        Args:286            state: The current agent state containing messages.287            runtime: The LangGraph runtime instance.288 289        Returns:290            A dict containing error ToolMessages for each write_todos call if multiple291            parallel calls are detected, otherwise None to allow normal execution.292        """293        messages = state["messages"]294        if not messages:295            return None296 297        last_ai_msg = next((msg for msg in reversed(messages) if isinstance(msg, AIMessage)), None)298        if not last_ai_msg or not last_ai_msg.tool_calls:299            return None300 301        # Count write_todos tool calls302        write_todos_calls = [tc for tc in last_ai_msg.tool_calls if tc["name"] == "write_todos"]303 304        if len(write_todos_calls) > 1:305            # Create error tool messages for all write_todos calls306            error_messages = [307                ToolMessage(308                    content=(309                        "Error: The `write_todos` tool should never be called multiple times "310                        "in parallel. Please call it only once per model invocation to update "311                        "the todo list."312                    ),313                    tool_call_id=tc["id"],314                    status="error",315                )316                for tc in write_todos_calls317            ]318 319            # Keep the tool calls in the AI message but return error messages320            # This follows the same pattern as HumanInTheLoopMiddleware321            return {"messages": error_messages}322 323        return None324 325    @override326    async def aafter_model(327        self, state: PlanningState[ResponseT], runtime: Runtime[ContextT]328    ) -> dict[str, Any] | None:329        """Check for parallel write_todos tool calls and return errors if detected.330 331        Async version of `after_model`. The todo list is designed to be updated at332        most once per model turn. Since the `write_todos` tool replaces the entire333        todo list with each call, making multiple parallel calls would create ambiguity334        about which update should take precedence. This method prevents such conflicts335        by rejecting any response that contains multiple write_todos tool calls.336 337        Args:338            state: The current agent state containing messages.339            runtime: The LangGraph runtime instance.340 341        Returns:342            A dict containing error ToolMessages for each write_todos call if multiple343            parallel calls are detected, otherwise None to allow normal execution.344        """345        return self.after_model(state, runtime)346 
codekingpro/portable-devtools · Team Ai