diff --git a/docs/references.md b/docs/references.md index 7d0cae0..97b865a 100644 --- a/docs/references.md +++ b/docs/references.md @@ -13,6 +13,8 @@ - [Combine control flow and state updates with Command](https://langchain-ai.github.io/langgraph/how-tos/graph-api/#combine-control-flow-and-state-updates-with-command) - [Command: a new tool for building multi-agent architectures in LangGraph](https://www.youtube.com/watch?v=6BJDKf90L9A) - [masamasa59/genai-agent-advanced-book > chapter6](https://github.com/masamasa59/genai-agent-advanced-book/blob/main/chapter6/arxiv_researcher/agent/paper_search_agent.py) +- [langchain-ai/deepagents](https://github.com/langchain-ai/deepagents) +- [Custom UI for Deep Agents](https://github.com/langchain-ai/deep-agents-ui) ### Sample Codes diff --git a/langgraph.json b/langgraph.json index aa1c5cc..79c6c53 100644 --- a/langgraph.json +++ b/langgraph.json @@ -4,6 +4,7 @@ "chat_with_tools_agent": "template_langgraph.agents.chat_with_tools_agent.agent:graph", "demo_agents_parallel_rag_agent": "template_langgraph.agents.demo_agents.parallel_rag_agent.agent:graph", "demo_agents_multi_agent": "template_langgraph.agents.demo_agents.multi_agent:graph", + "demo_agents_research_deep_agent": "template_langgraph.agents.demo_agents.research_deep_agent:graph", "demo_agents_weather_agent": "template_langgraph.agents.demo_agents.weather_agent:graph", "image_classifier_agent": "template_langgraph.agents.image_classifier_agent.agent:graph", "issue_formatter_agent": "template_langgraph.agents.issue_formatter_agent.agent:graph", diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/__init__.py b/template_langgraph/agents/demo_agents/deep_agent_core/__init__.py new file mode 100644 index 0000000..5baa5e5 --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/__init__.py @@ -0,0 +1,4 @@ +# ruff: noqa +from template_langgraph.agents.demo_agents.deep_agent_core.graph import create_deep_agent +from template_langgraph.agents.demo_agents.deep_agent_core.state import DeepAgentState +from template_langgraph.agents.demo_agents.deep_agent_core.sub_agent import SubAgent diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/graph.py b/template_langgraph/agents/demo_agents/deep_agent_core/graph.py new file mode 100644 index 0000000..27c3d93 --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/graph.py @@ -0,0 +1,80 @@ +# ruff: noqa: E501 +from collections.abc import Callable, Sequence +from typing import Any, TypeVar + +from langchain_core.language_models import LanguageModelLike +from langchain_core.tools import BaseTool +from langgraph.prebuilt import create_react_agent +from langgraph.types import Checkpointer + +from template_langgraph.agents.demo_agents.deep_agent_core.state import DeepAgentState +from template_langgraph.agents.demo_agents.deep_agent_core.sub_agent import SubAgent, _create_task_tool +from template_langgraph.agents.demo_agents.deep_agent_core.tools import ( + edit_file, + ls, + read_file, + write_file, + write_todos, +) +from template_langgraph.llms.azure_openais import AzureOpenAiWrapper + +StateSchema = TypeVar("StateSchema", bound=DeepAgentState) +StateSchemaType = type[StateSchema] + +base_prompt = """You have access to a number of standard tools + +## `write_todos` + +You have access to the `write_todos` tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress. +These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable. + +It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed. +## `task` + +- When doing web search, prefer to use the `task` tool in order to reduce context usage.""" + + +def create_deep_agent( + tools: Sequence[BaseTool | Callable | dict[str, Any]], + instructions: str, + model: str | LanguageModelLike | None = None, + subagents: list[SubAgent] = None, + state_schema: StateSchemaType | None = None, + config_schema: type[Any] | None = None, + checkpointer: Checkpointer | None = None, +): + """Create a deep agent. + + This agent will by default have access to a tool to write todos (write_todos), + and then four file editing tools: write_file, ls, read_file, edit_file. + + Args: + tools: The additional tools the agent should have access to. + instructions: The additional instructions the agent should have. Will go in + the system prompt. + model: The model to use. + subagents: The subagents to use. Each subagent should be a dictionary with the + following keys: + - `name` + - `description` (used by the main agent to decide whether to call the sub agent) + - `prompt` (used as the system prompt in the subagent) + - (optional) `tools` + state_schema: The schema of the deep agent. Should subclass from template_langgraph.agents.demo_agents.deep_agent_coretate + config_schema: The schema of the deep agent. + checkpointer: Optional checkpointer for persisting agent state between runs. + """ + prompt = instructions + base_prompt + built_in_tools = [write_todos, write_file, read_file, ls, edit_file] + if model is None: + model = AzureOpenAiWrapper().chat_model + state_schema = state_schema or DeepAgentState + task_tool = _create_task_tool(list(tools) + built_in_tools, instructions, subagents or [], model, state_schema) + all_tools = built_in_tools + list(tools) + [task_tool] + return create_react_agent( + model, + prompt=prompt, + tools=all_tools, + state_schema=state_schema, + config_schema=config_schema, + checkpointer=checkpointer, + ) diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/prompts.py b/template_langgraph/agents/demo_agents/deep_agent_core/prompts.py new file mode 100644 index 0000000..0d86801 --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/prompts.py @@ -0,0 +1,278 @@ +# ruff: noqa: E501 +WRITE_TODOS_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. +It also helps the user understand the progress of the task and overall progress of their requests. + +## When to Use This Tool +Use this tool proactively in these scenarios: + +1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions +2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations +3. User explicitly requests todo list - When the user directly asks you to use the todo list +4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) +5. After receiving new instructions - Immediately capture user requirements as todos +6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time +7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation + +## When NOT to Use This Tool + +Skip using this tool when: +1. There is only a single, straightforward task +2. The task is trivial and tracking it provides no organizational benefit +3. The task can be completed in less than 3 trivial steps +4. The task is purely conversational or informational + +NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly. + +## Examples of When to Use the Todo List + + +User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done! +Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation. +*Creates todo list with the following items:* +1. Create dark mode toggle component in Settings page +2. Add dark mode state management (context/store) +3. Implement CSS-in-JS styles for dark theme +4. Update existing components to support theme switching +5. Run tests and build process, addressing any failures or errors that occur +*Begins working on the first task* + + +The assistant used the todo list because: +1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes +2. The user explicitly requested tests and build be run afterward +3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task + + + + +User: Help me plan a comprehensive marketing campaign for our new product launch, including social media, email marketing, and press releases. +Assistant: I'll help you plan a comprehensive marketing campaign for your product launch. Let me create a todo list to organize all the components. +*Creates todo list with the following items:* +1. Define target audience and key messaging +2. Create social media content calendar and posting schedule +3. Design email marketing sequence and automation flows +4. Draft press release and identify media contacts +5. Set up tracking and analytics for campaign performance +6. Create visual assets and brand materials +7. Coordinate launch timeline and deliverables +*Begins working on the first task* + + +The assistant used the todo list because: +1. Marketing campaign planning involves multiple distinct channels and activities +2. Each component requires careful coordination and planning +3. The systematic approach ensures all aspects of the launch are covered +4. Progress tracking helps maintain timeline and deliverables + + + + +User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project +Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'. +*Uses search tools to locate all instances of getCwd in the codebase* +Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes. +*Creates todo list with specific items for each file that needs updating* + + +The assistant used the todo list because: +1. First, the assistant searched to understand the scope of the task +2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps +3. The todo list helps ensure every instance is tracked and updated systematically +4. This approach prevents missing any occurrences and maintains consistency + + + + +User: I need help organizing my home office: decluttering the desk, setting up a filing system, organizing digital files, and creating a daily workflow. +Assistant: I'll help you organize your home office systematically. Let me create a todo list to track each area of organization. +*Creates todo list with items like: 1) Sort and declutter desk items, 2) Set up physical filing system for documents, 3) Organize digital files and folders, 4) Create daily workflow and productivity systems, 5) Establish maintenance routines* +Let's start with decluttering your desk space to create a clean foundation for the other organizational systems. + + +The assistant used the todo list because: +1. The user provided multiple distinct organizational tasks +2. Each area requires different approaches and careful planning +3. Home organization benefits from systematic, step-by-step completion +4. Progress tracking ensures all areas are addressed thoroughly + + + +## Examples of When NOT to Use the Todo List + + +User: How do I print 'Hello World' in Python? +Assistant: In Python, you can print "Hello World" with this simple code: + +python +print("Hello World") + +This will output the text "Hello World" to the console when executed. + + +The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request. + + + + +User: What's the best way to water houseplants? +Assistant: The best way to water houseplants depends on the specific plant type, but here are some general guidelines: + +- Check soil moisture by inserting your finger about 1-2 inches into the soil +- Water thoroughly when the top inch feels dry, allowing water to drain from the bottom +- Use room temperature water and water in the morning when possible +- Avoid overwatering, which is more harmful than underwatering for most plants + +Different plants have different needs, so it's important to research your specific varieties for optimal care. + + +The assistant did not use the todo list because this is an informational request with no actual tasks to complete. The user is simply asking for advice and explanation, not for the assistant to perform multiple steps or activities. + + + + +User: Can you add a comment to the calculateTotal function to explain what it does? +Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does. +* Uses the Edit tool to add a comment to the calculateTotal function * + + +The assistant did not use the todo list because this is a single, straightforward task confined to one location. Adding a comment doesn't require tracking multiple steps or systematic organization. + + + + +User: What time is it in Tokyo right now? +Assistant: I'll check the current time in Tokyo for you. + +*Searches for current time in Tokyo* + +The current time in Tokyo, Japan is [current time]. Tokyo is in the Japan Standard Time (JST) zone, which is UTC+9. + + +The assistant did not use the todo list because this is a single information lookup with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward request. + + + +## Task States and Management + +1. **Task States**: Use these states to track progress: + - pending: Task not yet started + - in_progress: Currently working on (limit to ONE task at a time) + - completed: Task finished successfully + +2. **Task Management**: + - Update task status in real-time as you work + - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) + - Only have ONE task in_progress at any time + - Complete current tasks before starting new ones + - Remove tasks that are no longer relevant from the list entirely + +3. **Task Completion Requirements**: + - ONLY mark a task as completed when you have FULLY accomplished it + - If you encounter errors, blockers, or cannot finish, keep the task as in_progress + - When blocked, create a new task describing what needs to be resolved + - Never mark a task as completed if: + - There are unresolved issues or errors + - Work is partial or incomplete + - You encountered blockers that prevent completion + - You couldn't find necessary resources or dependencies + - Quality standards haven't been met + +4. **Task Breakdown**: + - Create specific, actionable items + - Break complex tasks into smaller, manageable steps + - Use clear, descriptive task names + +When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.""" + +TASK_DESCRIPTION_PREFIX = """Launch a new agent to handle complex, multi-step tasks autonomously. + +Available agent types and the tools they have access to: +- general-purpose: General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *) +{other_agents} +""" + +TASK_DESCRIPTION_SUFFIX = """When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. + +When to use the Agent tool: +- When you are instructed to execute custom slash commands. Use the Agent tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py") + +When NOT to use the Agent tool: +- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly +- If you are searching for a specific term or definition within a known location, use the Glob tool instead, to find the match more quickly +- If you are searching for content within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly +- Other tasks that are not related to the agent descriptions above + + +Usage notes: +1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses +2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. +3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. +4. The agent's outputs should generally be trusted +5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent +6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. + +Example usage: + + +"content-reviewer": use this agent after you are done creating significant content or documents +"greeting-responder": use this agent when to respond to user greetings with a friendly joke +"research-analyst": use this agent to conduct thorough research on complex topics + + + +user: "Please write a function that checks if a number is prime" +assistant: Sure let me write a function that checks if a number is prime +assistant: First let me use the Write tool to write a function that checks if a number is prime +assistant: I'm going to use the Write tool to write the following code: + +function isPrime(n) { + if (n <= 1) return false + for (let i = 2; i * i <= n; i++) { + if (n % i === 0) return false + } + return true +} + + +Since significant content was created and the task was completed, now use the content-reviewer agent to review the work + +assistant: Now let me use the content-reviewer agent to review the code +assistant: Uses the Task tool to launch with the content-reviewer agent + + + +user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?" + +This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis + +assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic. +assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take + + + +user: "Hello" + +Since the user is greeting, use the greeting-responder agent to respond with a friendly joke + +assistant: "I'm going to use the Task tool to launch with the greeting-responder agent" +""" +EDIT_DESCRIPTION = """Performs exact string replacements in files. + +Usage: +- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. +- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. +- ALWAYS prefer editing existing files. NEVER write new files unless explicitly required. +- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. +- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. +- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.""" +TOOL_DESCRIPTION = """Reads a file from the local filesystem. You can access any file directly by using this tool. +Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned. + +Usage: +- The file_path parameter must be an absolute path, not a relative path +- By default, it reads up to 2000 lines starting from the beginning of the file +- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters +- Any lines longer than 2000 characters will be truncated +- Results are returned using cat -n format, with line numbers starting at 1 +- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. +- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.""" diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/state.py b/template_langgraph/agents/demo_agents/deep_agent_core/state.py new file mode 100644 index 0000000..24102e2 --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/state.py @@ -0,0 +1,25 @@ +from typing import Annotated, Literal + +from langgraph.prebuilt.chat_agent_executor import AgentState +from typing_extensions import NotRequired, TypedDict + + +class Todo(TypedDict): + """Todo to track.""" + + content: str + status: Literal["pending", "in_progress", "completed"] + + +def file_reducer(l, r): # noqa + if l is None: + return r + elif r is None: + return l + else: + return {**l, **r} + + +class DeepAgentState(AgentState): + todos: NotRequired[list[Todo]] + files: Annotated[NotRequired[dict[str, str]], file_reducer] diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/sub_agent.py b/template_langgraph/agents/demo_agents/deep_agent_core/sub_agent.py new file mode 100644 index 0000000..7c018ca --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/sub_agent.py @@ -0,0 +1,72 @@ +from typing import Annotated, Any + +from langchain.chat_models import init_chat_model +from langchain_core.messages import ToolMessage +from langchain_core.tools import BaseTool, InjectedToolCallId, tool +from langgraph.prebuilt import InjectedState, create_react_agent +from langgraph.types import Command +from typing_extensions import NotRequired, TypedDict + +from template_langgraph.agents.demo_agents.deep_agent_core.prompts import ( + TASK_DESCRIPTION_PREFIX, + TASK_DESCRIPTION_SUFFIX, +) +from template_langgraph.agents.demo_agents.deep_agent_core.state import DeepAgentState + + +class SubAgent(TypedDict): + name: str + description: str + prompt: str + tools: NotRequired[list[str]] + # Optional per-subagent model configuration + model_settings: NotRequired[dict[str, Any]] + + +def _create_task_tool(tools, instructions, subagents: list[SubAgent], model, state_schema): + agents = {"general-purpose": create_react_agent(model, prompt=instructions, tools=tools)} + tools_by_name = {} + for tool_ in tools: + if not isinstance(tool_, BaseTool): + tool_ = tool(tool_) + tools_by_name[tool_.name] = tool_ + for _agent in subagents: + if "tools" in _agent: + _tools = [tools_by_name[t] for t in _agent["tools"]] + else: + _tools = tools + # Resolve per-subagent model if specified, else fallback to main model + if "model_settings" in _agent: + model_config = _agent["model_settings"] + # Always use get_default_model to ensure all settings are applied + sub_model = init_chat_model(**model_config) + else: + sub_model = model + agents[_agent["name"]] = create_react_agent( + sub_model, prompt=_agent["prompt"], tools=_tools, state_schema=state_schema + ) + + other_agents_string = [f"- {_agent['name']}: {_agent['description']}" for _agent in subagents] + + @tool(description=TASK_DESCRIPTION_PREFIX.format(other_agents=other_agents_string) + TASK_DESCRIPTION_SUFFIX) + async def task( + description: str, + subagent_type: str, + state: Annotated[DeepAgentState, InjectedState], + tool_call_id: Annotated[str, InjectedToolCallId], + ): + if subagent_type not in agents: + return ( + f"Error: invoked agent of type {subagent_type}, the only allowed types are {[f'`{k}`' for k in agents]}" + ) + sub_agent = agents[subagent_type] + state["messages"] = [{"role": "user", "content": description}] + result = await sub_agent.ainvoke(state) + return Command( + update={ + "files": result.get("files", {}), + "messages": [ToolMessage(result["messages"][-1].content, tool_call_id=tool_call_id)], + } + ) + + return task diff --git a/template_langgraph/agents/demo_agents/deep_agent_core/tools.py b/template_langgraph/agents/demo_agents/deep_agent_core/tools.py new file mode 100644 index 0000000..5ff6dcb --- /dev/null +++ b/template_langgraph/agents/demo_agents/deep_agent_core/tools.py @@ -0,0 +1,172 @@ +# ruff: noqa: E501 +from typing import Annotated + +from langchain_core.messages import ToolMessage +from langchain_core.tools import InjectedToolCallId, tool +from langgraph.prebuilt import InjectedState +from langgraph.types import Command + +from template_langgraph.agents.demo_agents.deep_agent_core.prompts import ( + EDIT_DESCRIPTION, + TOOL_DESCRIPTION, + WRITE_TODOS_DESCRIPTION, +) +from template_langgraph.agents.demo_agents.deep_agent_core.state import DeepAgentState, Todo + + +@tool(description=WRITE_TODOS_DESCRIPTION) +def write_todos(todos: list[Todo], tool_call_id: Annotated[str, InjectedToolCallId]) -> Command: + return Command( + update={ + "todos": todos, + "messages": [ToolMessage(f"Updated todo list to {todos}", tool_call_id=tool_call_id)], + } + ) + + +def ls(state: Annotated[DeepAgentState, InjectedState]) -> list[str]: + """List all files""" + return list(state.get("files", {}).keys()) + + +@tool(description=TOOL_DESCRIPTION) +def read_file( + file_path: str, + state: Annotated[DeepAgentState, InjectedState], + offset: int = 0, + limit: int = 2000, +) -> str: + """Read file.""" + mock_filesystem = state.get("files", {}) + if file_path not in mock_filesystem: + return f"Error: File '{file_path}' not found" + + # Get file content + content = mock_filesystem[file_path] + + # Handle empty file + if not content or content.strip() == "": + return "System reminder: File exists but has empty contents" + + # Split content into lines + lines = content.splitlines() + + # Apply line offset and limit + start_idx = offset + end_idx = min(start_idx + limit, len(lines)) + + # Handle case where offset is beyond file length + if start_idx >= len(lines): + return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)" + + # Format output with line numbers (cat -n format) + result_lines = [] + for i in range(start_idx, end_idx): + line_content = lines[i] + + # Truncate lines longer than 2000 characters + if len(line_content) > 2000: + line_content = line_content[:2000] + + # Line numbers start at 1, so add 1 to the index + line_number = i + 1 + result_lines.append(f"{line_number:6d}\t{line_content}") + + return "\n".join(result_lines) + + +def write_file( + file_path: str, + content: str, + state: Annotated[DeepAgentState, InjectedState], + tool_call_id: Annotated[str, InjectedToolCallId], +) -> Command: + """Write to a file.""" + files = state.get("files", {}) + files[file_path] = content + return Command( + update={ + "files": files, + "messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)], + } + ) + + +@tool(description=EDIT_DESCRIPTION) +def edit_file( + file_path: str, + old_string: str, + new_string: str, + state: Annotated[DeepAgentState, InjectedState], + tool_call_id: Annotated[str, InjectedToolCallId], + replace_all: bool = False, +) -> Command: + """Write to a file.""" + mock_filesystem = state.get("files", {}) + # Check if file exists in mock filesystem + if file_path not in mock_filesystem: + return Command( + update={ + "messages": [ToolMessage(f"Error: File '{file_path}' not found", tool_call_id=tool_call_id)], + } + ) + + # Get current file content + content = mock_filesystem[file_path] + + # Check if old_string exists in the file + if old_string not in content: + return Command( + update={ + "messages": [ + ToolMessage( + f"Error: String not found in file: '{old_string}'", + tool_call_id=tool_call_id, + ) + ], + } + ) + + # If not replace_all, check for uniqueness + if not replace_all: + occurrences = content.count(old_string) + if occurrences > 1: + return Command( + update={ + "messages": [ + ToolMessage( + f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.", + tool_call_id=tool_call_id, + ) + ] + } + ) + elif occurrences == 0: + return Command( + update={ + "messages": [ + ToolMessage( + f"Error: String not found in file: '{old_string}'", + tool_call_id=tool_call_id, + ) + ], + } + ) + + # Perform the replacement + if replace_all: + new_content = content.replace(old_string, new_string) + replacement_count = content.count(old_string) + result_msg = f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'" + else: + new_content = content.replace(old_string, new_string, 1) # Replace only first occurrence + result_msg = f"Successfully replaced string in '{file_path}'" + + # Update the mock filesystem + mock_filesystem[file_path] = new_content + return Command( + update={ + "files": mock_filesystem, + "messages": [ToolMessage(result_msg, tool_call_id=tool_call_id)], + } + ) diff --git a/template_langgraph/agents/demo_agents/research_deep_agent.py b/template_langgraph/agents/demo_agents/research_deep_agent.py new file mode 100644 index 0000000..407346d --- /dev/null +++ b/template_langgraph/agents/demo_agents/research_deep_agent.py @@ -0,0 +1,141 @@ +# ruff: noqa: E501 +from template_langgraph.agents.demo_agents.deep_agent_core import create_deep_agent +from template_langgraph.tools.common import get_default_tools + +sub_research_prompt = """You are a dedicated researcher. Your job is to conduct research based on the users questions. + +Conduct thorough research and then reply to the user with a detailed answer to their question + +only your FINAL answer will be passed on to the user. They will have NO knowledge of anything except your final message, so your final report should be your final message!""" + +research_sub_agent = { + "name": "research-agent", + "description": "Used to research more in depth questions. Only give this researcher one topic at a time. Do not pass multiple sub questions to this researcher. Instead, you should break down a large topic into the necessary components, and then call multiple research agents in parallel, one for each sub question.", + "prompt": sub_research_prompt, +} + +sub_critique_prompt = """You are a dedicated editor. You are being tasked to critique a report. + +You can find the report at `final_report.md`. + +You can find the question/topic for this report at `question.txt`. + +The user may ask for specific areas to critique the report in. Respond to the user with a detailed critique of the report. Things that could be improved. + +You can use the search tool to search for information, if that will help you critique the report + +Do not write to the `final_report.md` yourself. + +Things to check: +- Check that each section is appropriately named +- Check that the report is written as you would find in an essay or a textbook - it should be text heavy, do not let it just be a list of bullet points! +- Check that the report is comprehensive. If any paragraphs or sections are short, or missing important details, point it out. +- Check that the article covers key areas of the industry, ensures overall understanding, and does not omit important parts. +- Check that the article deeply analyzes causes, impacts, and trends, providing valuable insights +- Check that the article closely follows the research topic and directly answers questions +- Check that the article has a clear structure, fluent language, and is easy to understand. +""" + +critique_sub_agent = { + "name": "critique-agent", + "description": "Used to critique the final report. Give this agent some infomration about how you want it to critique the report.", + "prompt": sub_critique_prompt, +} + + +# Prompt prefix to steer the agent to be an expert researcher +research_instructions = """You are an expert researcher. Your job is to conduct thorough research, and then write a polished report. + +The first thing you should do is to write the original user question to `question.txt` so you have a record of it. + +Use the research-agent to conduct deep research. It will respond to your questions/topics with a detailed answer. + +When you think you enough information to write a final report, write it to `final_report.md` + +You can call the critique-agent to get a critique of the final report. After that (if needed) you can do more research and edit the `final_report.md` +You can do this however many times you want until are you satisfied with the result. + +Only edit the file once at a time (if you call this tool in parallel, there may be conflicts). + +Here are instructions for writing the final report: + + + +CRITICAL: Make sure the answer is written in the same language as the human messages! If you make a todo plan - you should note in the plan what language the report should be in so you dont forget! +Note: the language the report should be in is the language the QUESTION is in, not the language/country that the question is ABOUT. + +Please create a detailed answer to the overall research brief that: +1. Is well-organized with proper headings (# for title, ## for sections, ### for subsections) +2. Includes specific facts and insights from the research +3. References relevant sources using [Title](URL) format +4. Provides a balanced, thorough analysis. Be as comprehensive as possible, and include all information that is relevant to the overall research question. People are using you for deep research and will expect detailed, comprehensive answers. +5. Includes a "Sources" section at the end with all referenced links + +You can structure your report in a number of different ways. Here are some examples: + +To answer a question that asks you to compare two things, you might structure your report like this: +1/ intro +2/ overview of topic A +3/ overview of topic B +4/ comparison between A and B +5/ conclusion + +To answer a question that asks you to return a list of things, you might only need a single section which is the entire list. +1/ list of things or table of things +Or, you could choose to make each item in the list a separate section in the report. When asked for lists, you don't need an introduction or conclusion. +1/ item 1 +2/ item 2 +3/ item 3 + +To answer a question that asks you to summarize a topic, give a report, or give an overview, you might structure your report like this: +1/ overview of topic +2/ concept 1 +3/ concept 2 +4/ concept 3 +5/ conclusion + +If you think you can answer the question with a single section, you can do that too! +1/ answer + +REMEMBER: Section is a VERY fluid and loose concept. You can structure your report however you think is best, including in ways that are not listed above! +Make sure that your sections are cohesive, and make sense for the reader. + +For each section of the report, do the following: +- Use simple, clear language +- Use ## for section title (Markdown format) for each section of the report +- Do NOT ever refer to yourself as the writer of the report. This should be a professional report without any self-referential language. +- Do not say what you are doing in the report. Just write the report without any commentary from yourself. +- Each section should be as long as necessary to deeply answer the question with the information you have gathered. It is expected that sections will be fairly long and verbose. You are writing a deep research report, and users will expect a thorough answer. +- Use bullet points to list out information when appropriate, but by default, write in paragraph form. + +REMEMBER: +The brief and research may be in English, but you need to translate this information to the right language when writing the final answer. +Make sure the final answer report is in the SAME language as the human messages in the message history. + +Format the report in clear markdown with proper structure and include source references where appropriate. + + +- Assign each unique URL a single citation number in your text +- End with ### Sources that lists each source with corresponding numbers +- IMPORTANT: Number sources sequentially without gaps (1,2,3,4...) in the final list regardless of which sources you choose +- Each source should be a separate line item in a list, so that in markdown it is rendered as a list. +- Example format: + [1] Source Title: URL + [2] Source Title: URL +- Citations are extremely important. Make sure to include these, and pay a lot of attention to getting these right. Users will often use these citations to look into more information. + + + +You have access to a few tools. + +## `search_ai_search` + +Use this to run an Azure AI Search for getting KABUTO related information. You can specify the number of results, the topic, and whether raw content should be included. +""" + +# Create the agent +graph = create_deep_agent( + tools=get_default_tools(), + instructions=research_instructions, + subagents=[critique_sub_agent, research_sub_agent], +).with_config({"recursion_limit": 1000})