├── agents_deconstructed ├── __init__.py ├── format_tools.py ├── xml.py ├── openai_functions.py └── react_chat.py ├── setup.py ├── README.md └── notebooks ├── xml.ipynb ├── openai_functions.ipynb └── react_chat.ipynb /agents_deconstructed/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="agents_deconstructed", 5 | version="0.0.2", 6 | description="Utils for running 'agents' where the core logic is not obfuscated.", 7 | packages=find_packages(), 8 | install_requires=[ 9 | "langchain", 10 | ], 11 | ) 12 | -------------------------------------------------------------------------------- /agents_deconstructed/format_tools.py: -------------------------------------------------------------------------------- 1 | """Methods for formatting tools.""" 2 | from typing import Sequence 3 | from langchain.tools import BaseTool 4 | 5 | 6 | def format_tools_description(tools: Sequence[BaseTool]) -> str: 7 | """Format tools just with their description.""" 8 | tool_string = "" 9 | for tool in tools: 10 | tool_string += f"{tool.name}: {tool.description}\n" 11 | return tool_string 12 | 13 | def format_tools_args(tools: Sequence[BaseTool]) -> str: 14 | """Format tools with their description and args.""" 15 | tool_descs = [] 16 | for tool in tools: 17 | tool_desc = ( 18 | f"> Tool Name: {tool.name}\n" 19 | f"Tool Description: {tool.description}\n" 20 | f"Tool Args: {tool.args}\n" 21 | ) 22 | tool_descs.append(tool_desc) 23 | return "\n".join(tool_descs) 24 | -------------------------------------------------------------------------------- /agents_deconstructed/xml.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.agent import AgentOutputParser 2 | from langchain.schema import AgentAction, AgentFinish 3 | from typing import Union 4 | 5 | AGENT_INSTRUCTIONS = """You are a helpful assistant. Help the user answer any questions. 6 | 7 | You have access to the following tools: 8 | 9 | {tools} 10 | 11 | In order to use a tool, you can use and tags. \ 12 | You will then get back a response in the form 13 | For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond: 14 | 15 | searchweather in SF 16 | 64 degrees 17 | 18 | When you are done, respond with a final answer between . For example: 19 | 20 | The weather in SF is 64 degrees 21 | 22 | Begin! 23 | 24 | Question: {question}""" 25 | 26 | def format_steps(intermediate_steps): 27 | log = "" 28 | for action, observation in intermediate_steps: 29 | log += ( 30 | f"{action.tool}{action.tool_input}" 31 | f"{observation}" 32 | ) 33 | return log 34 | 35 | 36 | class XMLAgentOutputParser(AgentOutputParser): 37 | """Output parser for XMLAgent.""" 38 | 39 | def parse(self, text: str) -> Union[AgentAction, AgentFinish]: 40 | if "" in text: 41 | tool, tool_input = text.split("") 42 | _tool = tool.split("")[1] 43 | _tool_input = tool_input.split("")[1] 44 | return AgentAction(tool=_tool, tool_input=_tool_input, log=text) 45 | elif "" in text: 46 | _, answer = text.split("") 47 | return AgentFinish(return_values={"output": answer}, log=text) 48 | else: 49 | raise ValueError 50 | 51 | def get_format_instructions(self) -> str: 52 | raise NotImplementedError 53 | 54 | @property 55 | def _type(self) -> str: 56 | return "xml-agent" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖🪟 Agents Deconstructed 2 | 3 | Deconstructing agents to let you harness their power in a more understandable and customizable way. 4 | 5 | ## 🚀 Overview 6 | 7 | [LangChain](https://github.com/langchain-ai/langchain) Agents (and agents in other frameworks) are powerful but tough to customize. 8 | At the heart of those agents lies a `while` loop. 9 | This package deconstructs agents by providing helper functions and then forcing the user to implement the `while` loop. 10 | This makes agents both more understandable and more customizable. 11 | 12 | ## 📄 Installation 13 | `pip install agents_deconstructed` 14 | 15 | ## 👨‍👩‍👧‍👦 Quickstart 16 | 17 | We currently have two notebooks showing how use `agents_deconstructed`: 18 | 19 | - [XML Agent](notebooks/xml.ipynb): An agent that uses XML formatting (tested with OpenAI and Anthropic Models.) 20 | - [OpenAI Functions](notebooks/openai_functions.ipynb): An agent that uses OpenAI Functions 21 | 22 | ## 🔧 Components 23 | 24 | `agents_deconstructed` provides a few types of helper functions: 25 | 26 | **Tool Formatters** 27 | 28 | Logic for converting tools into a format such that the language model can understand how to work with them. 29 | 30 | **Output Parsers** 31 | 32 | Logic for converting the output of an LLM into an `AgentAction` or `AgentFinish`. 33 | 34 | **Intermediate Steps** 35 | 36 | Logic for converting intermediate steps (tuples of `AgentAction` and `observation`) into a format such that the language model can understand how to work with them. 37 | 38 | **Prompts** 39 | 40 | The main instructions to the language model, this tells it out to think and how to output it's decisions. 41 | This is pretty tightly coupled to a `Output Parser` and way of formatting `Intermediate Steps` 42 | 43 | ## 💻 Algorithm 44 | 45 | With the above components, we can now easily create agent-like logic. 46 | Pseudo-code for that logic: 47 | 48 | ```python 49 | # Create agent as a prompt + LLM + output parser 50 | # This uses LangChain Expression Language for ease of composability 51 | agent = prompt | llm | output_parser 52 | # Initialize empty list to track steps 53 | steps = [] 54 | # Use this to group all traces together 55 | with trace_as_chain_group("agent-run") as group_manager: 56 | # Get the first prediction 57 | inputs = { 58 | "input": ..., 59 | # Format intermediate steps as expected 60 | "intermediate_steps": format_steps(steps), 61 | # Format tools as expected 62 | "tools": format_tools(tools), 63 | } 64 | action = agent.invoke(inputs, config={"callbacks": group_manager}) 65 | # Enter a loop until an AgentFinish is reached 66 | while not isinstance(action, AgentFinish): 67 | # Select the tool to use 68 | tool = ... 69 | # Run the tool 70 | observation = tool.run(action.tool_input, callbacks=group_manager) 71 | # Construct intermediate steps 72 | steps += [(action, observation)] 73 | # Do the next iteration 74 | inputs = { 75 | "input": ..., 76 | # Format intermediate steps as expected 77 | "intermediate_steps": format_steps(steps), 78 | # Format tools as expected 79 | "tools": format_tools(tools), 80 | } 81 | action = agent.invoke(inputs, config={"callbacks": group_manager}) 82 | ``` 83 | 84 | ## ⚖️ Comparison to LangChain Agents 85 | 86 | Here is a (biased) comparison against LangChain agents: 87 | 88 | **Pros**: 89 | 90 | - Uses LangChain Expression Language (LangChain Agents haven't switched to use this) 91 | - More customizable 92 | - More understandable 93 | 94 | **Cons**: 95 | 96 | - Doesn't yet have same level of robustness (LangChain agents have safeguards against tools erroring, selecting tools that don't exist, etc) 97 | - More work to set up 98 | 99 | **The Same**: 100 | 101 | - Logging to [LangSmith](https://smith.langchain.com/) (since we can use `trace_as_chain_group`) 102 | -------------------------------------------------------------------------------- /agents_deconstructed/openai_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from json import JSONDecodeError 3 | from typing import List, Tuple, Union 4 | 5 | from langchain.schema import ( 6 | AgentAction, 7 | AgentFinish, 8 | BasePromptTemplate, 9 | OutputParserException, 10 | ) 11 | 12 | from dataclasses import dataclass 13 | from langchain.schema.messages import ( 14 | AIMessage, 15 | BaseMessage, 16 | FunctionMessage, 17 | SystemMessage, 18 | ) 19 | from langchain.schema import ( 20 | ChatGeneration, 21 | Generation, 22 | OutputParserException, 23 | ) 24 | from langchain.schema.output_parser import BaseGenerationOutputParser 25 | 26 | @dataclass 27 | class FunctionsAgentAction(AgentAction): 28 | message_log: List[BaseMessage] 29 | 30 | def _convert_agent_action_to_messages( 31 | agent_action: AgentAction, observation: str 32 | ) -> List[BaseMessage]: 33 | """Convert an agent action to a message. 34 | 35 | This code is used to reconstruct the original AI message from the agent action. 36 | 37 | Args: 38 | agent_action: Agent action to convert. 39 | 40 | Returns: 41 | AIMessage that corresponds to the original tool invocation. 42 | """ 43 | if isinstance(agent_action, AgentAction): 44 | return agent_action.message_log + [ 45 | _create_function_message(agent_action, observation) 46 | ] 47 | else: 48 | return [AIMessage(content=agent_action.log)] 49 | 50 | 51 | def _create_function_message( 52 | agent_action: AgentAction, observation: str 53 | ) -> FunctionMessage: 54 | """Convert agent action and observation into a function message. 55 | Args: 56 | agent_action: the tool invocation request from the agent 57 | observation: the result of the tool invocation 58 | Returns: 59 | FunctionMessage that corresponds to the original tool invocation 60 | """ 61 | if not isinstance(observation, str): 62 | try: 63 | content = json.dumps(observation, ensure_ascii=False) 64 | except Exception: 65 | content = str(observation) 66 | else: 67 | content = observation 68 | return FunctionMessage( 69 | name=agent_action.tool, 70 | content=content, 71 | ) 72 | 73 | 74 | def format_intermediate_steps( 75 | intermediate_steps: List[Tuple[AgentAction, str]], 76 | ) -> List[BaseMessage]: 77 | """Format intermediate steps. 78 | Args: 79 | intermediate_steps: Steps the LLM has taken to date, along with observations 80 | Returns: 81 | list of messages to send to the LLM for the next prediction 82 | """ 83 | messages = [] 84 | 85 | for intermediate_step in intermediate_steps: 86 | agent_action, observation = intermediate_step 87 | messages.extend(_convert_agent_action_to_messages(agent_action, observation)) 88 | 89 | return messages 90 | 91 | class FunctionsAgentOutputParser(BaseGenerationOutputParser[Union[AgentAction, AgentFinish]]): 92 | 93 | def parse_result(self, result: List[Generation]) -> Union[AgentAction, AgentFinish]: 94 | generation = result[0] 95 | if not isinstance(generation, ChatGeneration): 96 | raise OutputParserException( 97 | "This output parser can only be used with a chat generation." 98 | ) 99 | message = generation.message 100 | 101 | function_call = message.additional_kwargs.get("function_call", {}) 102 | 103 | if function_call: 104 | function_name = function_call["name"] 105 | try: 106 | _tool_input = json.loads(function_call["arguments"]) 107 | except JSONDecodeError: 108 | raise OutputParserException( 109 | f"Could not parse tool input: {function_call} because " 110 | f"the `arguments` is not valid JSON." 111 | ) 112 | 113 | # HACK HACK HACK: 114 | # The code that encodes tool input into Open AI uses a special variable 115 | # name called `__arg1` to handle old style tools that do not expose a 116 | # schema and expect a single string argument as an input. 117 | # We unpack the argument here if it exists. 118 | # Open AI does not support passing in a JSON array as an argument. 119 | if "__arg1" in _tool_input: 120 | tool_input = _tool_input["__arg1"] 121 | else: 122 | tool_input = _tool_input 123 | 124 | content_msg = "responded: {content}\n" if message.content else "\n" 125 | 126 | return FunctionsAgentAction( 127 | tool=function_name, 128 | tool_input=tool_input, 129 | log=f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n", 130 | message_log=[message], 131 | ) 132 | 133 | return AgentFinish(return_values={"output": message.content}, log=message.content) 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /notebooks/xml.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "23daa091-aa91-464f-9af5-3778783ca920", 6 | "metadata": {}, 7 | "source": [ 8 | "# Simple XML\n", 9 | "\n", 10 | "This notebook goes over how run an agent that uses XML to format steps" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 5, 16 | "id": "b523ec52-74a4-48e7-8830-e252c4bc6acb", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# !pip install openai agents_deconstructed" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "812fd079-698c-4463-ae5f-1e0fa5f8019d", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from langchain.agents import tool\n", 31 | "from langchain.schema.agent import AgentFinish\n", 32 | "from langchain.callbacks.manager import (\n", 33 | " trace_as_chain_group,\n", 34 | ")\n", 35 | "from langchain import hub\n", 36 | "from langchain.chat_models import ChatAnthropic, ChatOpenAI" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 2, 42 | "id": "b4418c1b-14c2-4c89-835b-1bfd0203c90f", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from agents_deconstructed import xml\n", 47 | "from agents_deconstructed.format_tools import format_tools_description" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 4, 53 | "id": "79c2e71d-0900-4b01-8760-afcd089cc46c", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "# Create our 'agent' - this is a runnable\n", 58 | "prompt = hub.pull('shoggoth13/xml-agent')\n", 59 | "agent = prompt | ChatOpenAI(temperature=0).bind(stop=[\"\", \"\"]) | xml.XMLAgentOutputParser()" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 5, 65 | "id": "287a88c5-f508-41f9-8f19-3a9ae3ef203d", 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "# Create a dummy tool\n", 70 | "@tool\n", 71 | "def run_code(query: str):\n", 72 | " \"\"\"Input should be Python code. Use this to do math.\"\"\"\n", 73 | " return eval(query)" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 6, 79 | "id": "e84e4e03-a97f-4bb4-a869-b565ec4f8a74", 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "[(AgentAction(tool='run_code', tool_input='2 + 2', log='run_code2 + 2'), 4)]\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "# Use this to group all traces together\n", 92 | "with trace_as_chain_group(\"agent-run\") as group_manager:\n", 93 | " # Keep track of steps taken\n", 94 | " steps = []\n", 95 | " # Get the first response from the agent\n", 96 | " action = agent.invoke({\n", 97 | " \"question\": \"whats 2 + 2\",\n", 98 | " \"tools\": format_tools_description([run_code]),\n", 99 | " \"intermediate_steps\": xml.format_steps(steps)\n", 100 | " }, config={\"callbacks\": group_manager})\n", 101 | "\n", 102 | " # While it's not finished, iterate\n", 103 | " while not isinstance(action, AgentFinish):\n", 104 | " # Select the tool to use\n", 105 | " tool = {\n", 106 | " \"run_code\": run_code,\n", 107 | " }[action.tool]\n", 108 | " # Run the tool\n", 109 | " observation = tool.run(action.tool_input, callbacks=group_manager)\n", 110 | " # Construct intermediate steps\n", 111 | " steps += [(action, observation)]\n", 112 | " # Log the steps\n", 113 | " print(steps)\n", 114 | " # Do the next iteration\n", 115 | " action = agent.invoke({\n", 116 | " \"question\": \"whats 2 + 2\",\n", 117 | " \"tools\": format_tools_description([run_code]),\n", 118 | " \"intermediate_steps\": xml.format_steps(steps)\n", 119 | " }, config={\"callbacks\": group_manager})" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 7, 125 | "id": "09321ae0-ea15-48e1-8070-ca8b31dbfc8f", 126 | "metadata": {}, 127 | "outputs": [ 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "AgentFinish(return_values={'output': '2 + 2 equals 4'}, log='2 + 2 equals 4')\n" 133 | ] 134 | } 135 | ], 136 | "source": [ 137 | "print(action)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "id": "6e5cee08-dd11-4402-af95-6a477a41cb00", 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [] 147 | } 148 | ], 149 | "metadata": { 150 | "kernelspec": { 151 | "display_name": "Python 3 (ipykernel)", 152 | "language": "python", 153 | "name": "python3" 154 | }, 155 | "language_info": { 156 | "codemirror_mode": { 157 | "name": "ipython", 158 | "version": 3 159 | }, 160 | "file_extension": ".py", 161 | "mimetype": "text/x-python", 162 | "name": "python", 163 | "nbconvert_exporter": "python", 164 | "pygments_lexer": "ipython3", 165 | "version": "3.10.2" 166 | } 167 | }, 168 | "nbformat": 4, 169 | "nbformat_minor": 5 170 | } 171 | -------------------------------------------------------------------------------- /agents_deconstructed/react_chat.py: -------------------------------------------------------------------------------- 1 | """Adopted from Llama Index. 2 | 3 | https://github.com/jerryjliu/llama_index/blob/main/llama_index/agent/react/output_parser.py 4 | """ 5 | from langchain.schema import ( 6 | AgentAction, 7 | AgentFinish, 8 | ) 9 | from langchain.schema.output_parser import BaseOutputParser 10 | from langchain.schema.messages import HumanMessage, AIMessage 11 | from typing import Union, Tuple 12 | import ast 13 | 14 | import re 15 | 16 | 17 | def extract_tool_use(input_text: str) -> Tuple[str, str, str]: 18 | pattern = r"\s*Thought:(.*?)Action:(.*?)Action Input:(.*?)(?:\n|$)" 19 | 20 | match = re.search(pattern, input_text, re.DOTALL) 21 | if not match: 22 | raise ValueError( 23 | "Could not extract tool use from input text: {}".format(input_text) 24 | ) 25 | 26 | thought = match.group(1).strip() 27 | action = match.group(2).strip() 28 | action_input = match.group(3).strip() 29 | return thought, action, action_input 30 | 31 | 32 | def extract_final_response(input_text: str) -> Tuple[str, str]: 33 | pattern = r"\s*Thought:(.*?)Answer:(.*?)(?:\n|$)" 34 | 35 | match = re.search(pattern, input_text, re.DOTALL) 36 | if not match: 37 | raise ValueError( 38 | "Could not extract final answer from input text: {}".format(input_text) 39 | ) 40 | 41 | thought = match.group(1).strip() 42 | answer = match.group(2).strip() 43 | return thought, answer 44 | 45 | def extract_json_str(text: str) -> str: 46 | """Extract JSON string from text.""" 47 | # NOTE: this regex parsing is taken from langchain.output_parsers.pydantic 48 | match = re.search(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) 49 | if not match: 50 | raise ValueError(f"Could not extract json string from output: {text}") 51 | 52 | return match.group() 53 | 54 | 55 | class ReActOutputParser(BaseOutputParser): 56 | 57 | def parse(self, text: str) -> Union[AgentAction, AgentFinish]: 58 | """Parse output from ReAct agent. 59 | 60 | We expect the output to be in one of the following formats: 61 | 1. If the agent need to use a tool to answer the question: 62 | ``` 63 | Thought: 64 | Action: 65 | Action Input: 66 | ``` 67 | 2. If the agent can answer the question without any tools: 68 | ``` 69 | Thought: 70 | Answer: 71 | ``` 72 | """ 73 | if "Thought:" not in text: 74 | # NOTE: handle the case where the agent directly outputs the answer 75 | # instead of following the thought-answer format 76 | return AgentFinish( 77 | log="I can answer without any tools.", return_values={"output": text} 78 | ) 79 | 80 | if "Answer:" in text: 81 | thought, answer = extract_final_response(text) 82 | return AgentFinish( 83 | log=thought, return_values={"output": answer} 84 | ) 85 | 86 | if "Action:" in text: 87 | thought, action, action_input = extract_tool_use(text) 88 | json_str = extract_json_str(action_input) 89 | 90 | # NOTE: we found that json.loads does not reliably parse 91 | # json with single quotes, so we use ast instead 92 | # action_input_dict = json.loads(json_str) 93 | action_input_dict = ast.literal_eval(json_str) 94 | 95 | return AgentAction( 96 | log=thought, tool=action, tool_input=action_input_dict 97 | ) 98 | 99 | raise ValueError("Could not parse output: {}".format(text)) 100 | 101 | 102 | def format_steps(intermediate_steps): 103 | log = [] 104 | for action, observation in intermediate_steps: 105 | log.extend([ 106 | HumanMessage(content=action.log), 107 | AIMessage(content=f"Observation: {observation}") 108 | ]) 109 | return log 110 | 111 | REACT_AGENT_INSTRUCTIONS = """\ 112 | 113 | You are designed to help with a variety of tasks, from answering questions \ 114 | to providing summaries to other types of analyses. 115 | 116 | ## Tools 117 | You have access to a wide variety of tools. You are responsible for using 118 | the tools in any sequence you deem appropriate to complete the task at hand. 119 | This may require breaking the task into subtasks and using different tools 120 | to complete each subtask. 121 | 122 | You have access to the following tools: 123 | {tool_desc} 124 | 125 | ## Output Format 126 | To answer the question, please use the following format. 127 | 128 | ``` 129 | Thought: I need to use a tool to help me answer the question. 130 | Action: tool name (one of {tool_names}) 131 | Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {{"text": "hello world", "num_beams": 5}}) 132 | ``` 133 | Please use a valid JSON format for the action input. Do NOT do this {{'text': 'hello world', 'num_beams': 5}}. 134 | 135 | If this format is used, the user will respond in the following format: 136 | 137 | ``` 138 | Observation: tool response 139 | ``` 140 | 141 | You should keep repeating the above format until you have enough information 142 | to answer the question without using any more tools. At that point, you MUST respond 143 | in the following format: 144 | 145 | ``` 146 | Thought: I can answer without using any more tools. 147 | Answer: [your answer here] 148 | ``` 149 | 150 | ## Current Conversation 151 | Below is the current conversation consisting of interleaving human and assistant messages. 152 | 153 | """ 154 | -------------------------------------------------------------------------------- /notebooks/openai_functions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "23daa091-aa91-464f-9af5-3778783ca920", 6 | "metadata": {}, 7 | "source": [ 8 | "# OpenAI Functions\n", 9 | "\n", 10 | "This notebook goes over how run an agent that uses OpenAI Functions" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 5, 16 | "id": "b523ec52-74a4-48e7-8830-e252c4bc6acb", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# !pip install openai agents_deconstructed" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 14, 26 | "id": "812fd079-698c-4463-ae5f-1e0fa5f8019d", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from langchain.agents import tool\n", 31 | "from langchain.schema.agent import AgentFinish\n", 32 | "from langchain.callbacks.manager import (\n", 33 | " trace_as_chain_group,\n", 34 | ")\n", 35 | "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n", 36 | "from langchain.chat_models import ChatOpenAI\n", 37 | "from langchain.tools.convert_to_openai import format_tool_to_openai_function" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "id": "b4418c1b-14c2-4c89-835b-1bfd0203c90f", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "from agents_deconstructed import openai_functions" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 5, 53 | "id": "1341df6a-b9d6-4dc3-a6d0-c8055d40d6fe", 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "# Create a dummy tool\n", 58 | "@tool\n", 59 | "def run_code(query: str):\n", 60 | " \"\"\"Input should be Python code. Use this to do math.\"\"\"\n", 61 | " return eval(query)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 7, 67 | "id": "b6d078f2-dfc0-4754-91e8-403708483e3d", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "functions = [dict(format_tool_to_openai_function(t)) for t in [run_code]]" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 15, 77 | "id": "79c2e71d-0900-4b01-8760-afcd089cc46c", 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "# Create our 'agent' - this is a runnable\n", 82 | "agent = ChatPromptTemplate.from_messages([\n", 83 | " (\"system\", \"You are a helpful assistant\"),\n", 84 | " (\"user\", \"{question}\"),\n", 85 | " MessagesPlaceholder(variable_name=\"intermediate_steps\"),\n", 86 | "]) | ChatOpenAI(temperature=0).bind(functions=functions) | openai_functions.FunctionsAgentOutputParser()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 16, 92 | "id": "e84e4e03-a97f-4bb4-a869-b565ec4f8a74", 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "name": "stdout", 97 | "output_type": "stream", 98 | "text": [ 99 | "[(FunctionsAgentAction(tool='run_code', tool_input={'query': '2 + 2'}, log=\"\\nInvoking: `run_code` with `{'query': '2 + 2'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'run_code', 'arguments': '{\\n \"query\": \"2 + 2\"\\n}'}}, example=False)]), 4)]\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "# Use this to group all traces together\n", 105 | "with trace_as_chain_group(\"agent-run\") as group_manager:\n", 106 | " # Keep track of steps taken\n", 107 | " steps = []\n", 108 | " # Get the first response from the agent\n", 109 | " action = agent.invoke({\n", 110 | " \"question\": \"whats 2 + 2\",\n", 111 | " \"intermediate_steps\": openai_functions.format_intermediate_steps(steps)\n", 112 | " }, config={\"callbacks\": group_manager})\n", 113 | "\n", 114 | " # While it's not finished, iterate\n", 115 | " while not isinstance(action, AgentFinish):\n", 116 | " # Select the tool to use\n", 117 | " tool = {\n", 118 | " \"run_code\": run_code,\n", 119 | " }[action.tool]\n", 120 | " # Run the tool\n", 121 | " observation = tool.run(action.tool_input, callbacks=group_manager)\n", 122 | " # Construct intermediate steps\n", 123 | " steps += [(action, observation)]\n", 124 | " # Log the steps\n", 125 | " print(steps)\n", 126 | " # Do the next iteration\n", 127 | " action = agent.invoke({\n", 128 | " \"question\": \"whats 2 + 2\",\n", 129 | " \"intermediate_steps\": openai_functions.format_intermediate_steps(steps)\n", 130 | " }, config={\"callbacks\": group_manager})" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 17, 136 | "id": "09321ae0-ea15-48e1-8070-ca8b31dbfc8f", 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "name": "stdout", 141 | "output_type": "stream", 142 | "text": [ 143 | "AgentFinish(return_values={'output': '2 + 2 equals 4.'}, log='2 + 2 equals 4.')\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "print(action)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "id": "6e5cee08-dd11-4402-af95-6a477a41cb00", 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [] 158 | } 159 | ], 160 | "metadata": { 161 | "kernelspec": { 162 | "display_name": "Python 3 (ipykernel)", 163 | "language": "python", 164 | "name": "python3" 165 | }, 166 | "language_info": { 167 | "codemirror_mode": { 168 | "name": "ipython", 169 | "version": 3 170 | }, 171 | "file_extension": ".py", 172 | "mimetype": "text/x-python", 173 | "name": "python", 174 | "nbconvert_exporter": "python", 175 | "pygments_lexer": "ipython3", 176 | "version": "3.10.2" 177 | } 178 | }, 179 | "nbformat": 4, 180 | "nbformat_minor": 5 181 | } 182 | -------------------------------------------------------------------------------- /notebooks/react_chat.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3b080df1-e300-43a8-8249-1551a30e2913", 6 | "metadata": {}, 7 | "source": [ 8 | "# ReAct Chat\n", 9 | "\n", 10 | "This agent uses ReAct to reason about what to do. Works with Chat Models.\n", 11 | "\n", 12 | "Logic \"deconstructed\" from LlamaIndex" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "id": "ed0082ff-6801-48ad-a87e-03bf6afc4642", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "# !pip install openai agents_deconstructed" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 10, 28 | "id": "f2cdb5a6-6d7e-4c9f-b8c5-a43cd3cd0dff", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "from langchain.agents import tool\n", 33 | "from langchain.schema.agent import AgentFinish\n", 34 | "from langchain.callbacks.manager import (\n", 35 | " trace_as_chain_group,\n", 36 | ")\n", 37 | "from langchain import hub\n", 38 | "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n", 39 | "from langchain.chat_models import ChatOpenAI" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 3, 45 | "id": "45b71716-ee57-4103-8eac-ff4b4db2f48e", 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "from agents_deconstructed import react_chat\n", 50 | "from agents_deconstructed.format_tools import format_tools_args" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 4, 56 | "id": "cb691a5d-1f4e-4432-87d1-807b7660fcef", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "# Create a dummy tool\n", 61 | "@tool\n", 62 | "def run_code(query: str):\n", 63 | " \"\"\"Input should be Python code. Use this to do math.\"\"\"\n", 64 | " return eval(query)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 19, 70 | "id": "765d17bf-0aa4-4d41-a534-8135d893cc80", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# Create our prompt, either directly or from hub\n", 75 | "prompt = ChatPromptTemplate.from_messages([\n", 76 | " (\"system\", react_chat.REACT_AGENT_INSTRUCTIONS),\n", 77 | " (\"user\", \"{question}\"),\n", 78 | " MessagesPlaceholder(variable_name=\"intermediate_steps\"),\n", 79 | "])\n", 80 | "# Or call pull from hub!\n", 81 | "#prompt = hub.pull(\"shoggoth13/react-chat-agent\")" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 20, 87 | "id": "3c7f6f77-19b2-4238-b277-3bf6c0ae9e94", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "tools = [run_code]" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 21, 97 | "id": "217f893a-1b70-4fb7-b54a-15a21514b5df", 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "prompt = prompt.partial(\n", 102 | " tool_desc=format_tools_args(tools),\n", 103 | " tool_names=\", \".join([tool.name for tool in tools])\n", 104 | ")" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 22, 110 | "id": "60eb89c1-0a62-4c2d-b2e3-b5cf3d4491bd", 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "agent = prompt | ChatOpenAI(temperature=0) | react_chat.ReActOutputParser()" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 23, 120 | "id": "1c400980-13c7-4098-b85c-58d6c3aeb452", 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "[(AgentAction(tool='run_code', tool_input={'query': '2 + 2'}, log='I can use the `run_code` tool to perform the addition.'), 4)]\n" 128 | ] 129 | } 130 | ], 131 | "source": [ 132 | "# Use this to group all traces together\n", 133 | "with trace_as_chain_group(\"agent-run\") as group_manager:\n", 134 | " # Keep track of steps taken\n", 135 | " steps = []\n", 136 | " # Get the first response from the agent\n", 137 | " action = agent.invoke({\n", 138 | " \"question\": \"whats 2 + 2\",\n", 139 | " \"intermediate_steps\": react_chat.format_steps(steps)\n", 140 | " }, config={\"callbacks\": group_manager})\n", 141 | "\n", 142 | " # While it's not finished, iterate\n", 143 | " while not isinstance(action, AgentFinish):\n", 144 | " # Select the tool to use\n", 145 | " tool = {\n", 146 | " \"run_code\": run_code,\n", 147 | " }[action.tool]\n", 148 | " # Run the tool\n", 149 | " observation = tool.run(action.tool_input, callbacks=group_manager)\n", 150 | " # Construct intermediate steps\n", 151 | " steps += [(action, observation)]\n", 152 | " # Log the steps\n", 153 | " print(steps)\n", 154 | " # Do the next iteration\n", 155 | " action = agent.invoke({\n", 156 | " \"question\": \"whats 2 + 2\",\n", 157 | " \"intermediate_steps\": react_chat.format_steps(steps)\n", 158 | " }, config={\"callbacks\": group_manager})" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 24, 164 | "id": "170434d9-c737-4906-8c9e-6ae65d1fcf40", 165 | "metadata": {}, 166 | "outputs": [ 167 | { 168 | "data": { 169 | "text/plain": [ 170 | "AgentFinish(return_values={'output': '4'}, log='I can answer without using any more tools.')" 171 | ] 172 | }, 173 | "execution_count": 24, 174 | "metadata": {}, 175 | "output_type": "execute_result" 176 | } 177 | ], 178 | "source": [ 179 | "action" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": null, 185 | "id": "7bc5d6df-60a1-4010-b2a1-7bed2f7e1b41", 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [] 189 | } 190 | ], 191 | "metadata": { 192 | "kernelspec": { 193 | "display_name": "Python 3 (ipykernel)", 194 | "language": "python", 195 | "name": "python3" 196 | }, 197 | "language_info": { 198 | "codemirror_mode": { 199 | "name": "ipython", 200 | "version": 3 201 | }, 202 | "file_extension": ".py", 203 | "mimetype": "text/x-python", 204 | "name": "python", 205 | "nbconvert_exporter": "python", 206 | "pygments_lexer": "ipython3", 207 | "version": "3.10.2" 208 | } 209 | }, 210 | "nbformat": 4, 211 | "nbformat_minor": 5 212 | } 213 | --------------------------------------------------------------------------------