[docs]: purge message graph (#1330)

* purge message graph

* docstrings

* use with dicts
This commit is contained in:
Isaac Francisco
2024-08-14 11:16:38 -07:00
committed by GitHub
parent a3dd43b39b
commit 34fd833b04
7 changed files with 1050 additions and 74 deletions
@@ -26,7 +26,10 @@
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
"metadata": {},
"outputs": [],
"source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai"]
"source": [
"# %%capture --no-stderr\n",
"# %pip install -U langgraph langchain langchain_openai"
]
},
{
"cell_type": "code",
@@ -34,7 +37,24 @@
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith.\n",
"# This will help you visualize and debug the control flow\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""
]
},
{
"cell_type": "markdown",
@@ -55,7 +75,24 @@
"id": "828479af-cf9c-4888-a365-599643a96b55",
"metadata": {},
"outputs": [],
"source": ["from typing import List\n\nimport openai\n\n\n# This is flexible, but you can define your agent here, or call your agent API here.\ndef my_chat_bot(messages: List[dict]) -> dict:\n system_message = {\n \"role\": \"system\",\n \"content\": \"You are a customer support agent for an airline.\",\n }\n messages = [system_message] + messages\n completion = openai.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\"\n )\n return completion.choices[0].message.model_dump()"]
"source": [
"from typing import List\n",
"\n",
"import openai\n",
"\n",
"\n",
"# This is flexible, but you can define your agent here, or call your agent API here.\n",
"def my_chat_bot(messages: List[dict]) -> dict:\n",
" system_message = {\n",
" \"role\": \"system\",\n",
" \"content\": \"You are a customer support agent for an airline.\",\n",
" }\n",
" messages = [system_message] + messages\n",
" completion = openai.chat.completions.create(\n",
" messages=messages, model=\"gpt-3.5-turbo\"\n",
" )\n",
" return completion.choices[0].message.model_dump()"
]
},
{
"cell_type": "code",
@@ -77,7 +114,9 @@
"output_type": "execute_result"
}
],
"source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"]
"source": [
"my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"
]
},
{
"cell_type": "markdown",
@@ -96,7 +135,33 @@
"id": "32c147df-7f90-4b0d-9a6b-671677020353",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nsystem_prompt_template = \"\"\"You are a customer of an airline company. \\\nYou are interacting with a user who is a customer support person. \\\n\n{instructions}\n\nWhen you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt_template),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\ninstructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\nYou want them to give you ALL the money back. \\\nThis trip happened 5 years ago.\"\"\"\n\nprompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n\nmodel = ChatOpenAI()\n\nsimulated_user = prompt | model"]
"source": [
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"system_prompt_template = \"\"\"You are a customer of an airline company. \\\n",
"You are interacting with a user who is a customer support person. \\\n",
"\n",
"{instructions}\n",
"\n",
"When you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt_template),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"instructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\n",
"You want them to give you ALL the money back. \\\n",
"This trip happened 5 years ago.\"\"\"\n",
"\n",
"prompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n",
"\n",
"model = ChatOpenAI()\n",
"\n",
"simulated_user = prompt | model"
]
},
{
"cell_type": "code",
@@ -115,7 +180,12 @@
"output_type": "execute_result"
}
],
"source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"]
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n",
"simulated_user.invoke({\"messages\": messages})"
]
},
{
"cell_type": "markdown",
@@ -153,7 +223,20 @@
"id": "69e2a3a3-40f3-4223-9136-113738440be9",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.adapters.openai import convert_message_to_dict\nfrom langchain_core.messages import AIMessage\n\n\ndef chat_bot_node(messages):\n # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n messages = [convert_message_to_dict(m) for m in messages]\n # Call the chat bot\n chat_bot_response = my_chat_bot(messages)\n # Respond with an AI Message\n return AIMessage(content=chat_bot_response[\"content\"])"]
"source": [
"from langchain_community.adapters.openai import convert_message_to_dict\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"def chat_bot_node(state):\n",
" messages = state[\"messages\"]\n",
" # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n",
" messages = [convert_message_to_dict(m) for m in messages]\n",
" # Call the chat bot\n",
" chat_bot_response = my_chat_bot(messages)\n",
" # Respond with an AI Message\n",
" return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}"
]
},
{
"cell_type": "markdown",
@@ -169,7 +252,26 @@
"id": "7cad7527-ffa5-4c30-8585-b54a7a18bd98",
"metadata": {},
"outputs": [],
"source": ["def _swap_roles(messages):\n new_messages = []\n for m in messages:\n if isinstance(m, AIMessage):\n new_messages.append(HumanMessage(content=m.content))\n else:\n new_messages.append(AIMessage(content=m.content))\n return new_messages\n\n\ndef simulated_user_node(messages):\n # Swap roles of messages\n new_messages = _swap_roles(messages)\n # Call the simulated user\n response = simulated_user.invoke({\"messages\": new_messages})\n # This response is an AI message - we need to flip this to be a human message\n return HumanMessage(content=response.content)"]
"source": [
"def _swap_roles(messages):\n",
" new_messages = []\n",
" for m in messages:\n",
" if isinstance(m, AIMessage):\n",
" new_messages.append(HumanMessage(content=m.content))\n",
" else:\n",
" new_messages.append(AIMessage(content=m.content))\n",
" return new_messages\n",
"\n",
"\n",
"def simulated_user_node(state):\n",
" messages = state[\"messages\"]\n",
" # Swap roles of messages\n",
" new_messages = _swap_roles(messages)\n",
" # Call the simulated user\n",
" response = simulated_user.invoke({\"messages\": new_messages})\n",
" # This response is an AI message - we need to flip this to be a human message\n",
" return {\"messages\":[HumanMessage(content=response.content)]}"
]
},
{
"cell_type": "markdown",
@@ -192,7 +294,16 @@
"id": "28004fbf-a2f3-46b7-bde7-46c7adaf97fb",
"metadata": {},
"outputs": [],
"source": ["def should_continue(messages):\n if len(messages) > 6:\n return \"end\"\n elif messages[-1].content == \"FINISHED\":\n return \"end\"\n else:\n return \"continue\""]
"source": [
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" if len(messages) > 6:\n",
" return \"end\"\n",
" elif messages[-1].content == \"FINISHED\":\n",
" return \"end\"\n",
" else:\n",
" return \"continue\""
]
},
{
"cell_type": "markdown",
@@ -210,7 +321,36 @@
"id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c",
"metadata": {},
"outputs": [],
"source": ["from langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\ngraph_builder.add_node(\"user\", simulated_user_node)\ngraph_builder.add_node(\"chat_bot\", chat_bot_node)\n# Every response from your chat bot will automatically go to the\n# simulated user\ngraph_builder.add_edge(\"chat_bot\", \"user\")\ngraph_builder.add_conditional_edges(\n \"user\",\n should_continue,\n # If the finish criteria are met, we will stop the simulation,\n # otherwise, the virtual user's message will be sent to your chat bot\n {\n \"end\": END,\n \"continue\": \"chat_bot\",\n },\n)\n# The input will first go to your chat bot\ngraph_builder.add_edge(START, \"chat_bot\")\nsimulation = graph_builder.compile()"]
"source": [
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"graph_builder = StateGraph(State)\n",
"graph_builder.add_node(\"user\", simulated_user_node)\n",
"graph_builder.add_node(\"chat_bot\", chat_bot_node)\n",
"# Every response from your chat bot will automatically go to the\n",
"# simulated user\n",
"graph_builder.add_edge(\"chat_bot\", \"user\")\n",
"graph_builder.add_conditional_edges(\n",
" \"user\",\n",
" should_continue,\n",
" # If the finish criteria are met, we will stop the simulation,\n",
" # otherwise, the virtual user's message will be sent to your chat bot\n",
" {\n",
" \"end\": END,\n",
" \"continue\": \"chat_bot\",\n",
" },\n",
")\n",
"# The input will first go to your chat bot\n",
"graph_builder.add_edge(START, \"chat_bot\")\n",
"simulation = graph_builder.compile()"
]
},
{
"cell_type": "markdown",
@@ -251,7 +391,13 @@
]
}
],
"source": ["for chunk in simulation.stream([]):\n # Print out all events aside from the final end chunk\n if END not in chunk:\n print(chunk)\n print(\"----\")"]
"source": [
"for chunk in simulation.stream({}):\n",
" # Print out all events aside from the final end chunk\n",
" if END not in chunk:\n",
" print(chunk)\n",
" print(\"----\")"
]
},
{
"cell_type": "code",
@@ -259,7 +405,7 @@
"id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0",
"metadata": {},
"outputs": [],
"source": [""]
"source": []
}
],
"metadata": {
@@ -177,10 +177,16 @@
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import START, MessageGraph\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"memory = MemorySaver()\n",
"workflow = MessageGraph()\n",
"workflow = StateGraph(State)\n",
"workflow.add_node(\"info\", chain)\n",
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
"\n",
File diff suppressed because one or more lines are too long
+137 -11
View File
@@ -32,7 +32,10 @@
"id": "8b323f43-328b-4b4b-88b0-6c84dc0a1d60",
"metadata": {},
"outputs": [],
"source": ["%pip install -U --quiet langgraph langchain-fireworks\n%pip install -U --quiet tavily-python"]
"source": [
"%pip install -U --quiet langgraph langchain-fireworks\n",
"%pip install -U --quiet tavily-python"
]
},
{
"cell_type": "code",
@@ -40,7 +43,24 @@
"id": "3368f330-cad6-4d35-a291-68fbf4389d98",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n\n_set_if_undefined(\"FIREWORKS_API_KEY\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n",
"\n",
"_set_if_undefined(\"FIREWORKS_API_KEY\")"
]
},
{
"cell_type": "markdown",
@@ -58,7 +78,28 @@
"id": "cc10028f-9cef-4936-9419-cbdf06d24f1e",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_fireworks import ChatFireworks\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n \" Generate the best essay possible for the user's request.\"\n \" If the user provides critique, respond with a revised version of your previous attempts.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nllm = ChatFireworks(\n model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n model_kwargs={\"max_tokens\": 32768},\n)\ngenerate = prompt | llm"]
"source": [
"from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_fireworks import ChatFireworks\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n",
" \" Generate the best essay possible for the user's request.\"\n",
" \" If the user provides critique, respond with a revised version of your previous attempts.\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"llm = ChatFireworks(\n",
" model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n",
" model_kwargs={\"max_tokens\": 32768},\n",
")\n",
"generate = prompt | llm"
]
},
{
"cell_type": "code",
@@ -86,7 +127,15 @@
]
}
],
"source": ["essay = \"\"\nrequest = HumanMessage(\n content=\"Write an essay on why the little prince is relevant in modern childhood\"\n)\nfor chunk in generate.stream({\"messages\": [request]}):\n print(chunk.content, end=\"\")\n essay += chunk.content"]
"source": [
"essay = \"\"\n",
"request = HumanMessage(\n",
" content=\"Write an essay on why the little prince is relevant in modern childhood\"\n",
")\n",
"for chunk in generate.stream({\"messages\": [request]}):\n",
" print(chunk.content, end=\"\")\n",
" essay += chunk.content"
]
},
{
"cell_type": "markdown",
@@ -102,7 +151,19 @@
"id": "a705be92-88c0-4f4f-b4c2-cdcd9af8cb2c",
"metadata": {},
"outputs": [],
"source": ["reflection_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nreflect = reflection_prompt | llm"]
"source": [
"reflection_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n",
" \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"reflect = reflection_prompt | llm"
]
},
{
"cell_type": "code",
@@ -132,7 +193,12 @@
]
}
],
"source": ["reflection = \"\"\nfor chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n print(chunk.content, end=\"\")\n reflection += chunk.content"]
"source": [
"reflection = \"\"\n",
"for chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n",
" print(chunk.content, end=\"\")\n",
" reflection += chunk.content"
]
},
{
"cell_type": "markdown",
@@ -170,7 +236,12 @@
]
}
],
"source": ["for chunk in generate.stream(\n {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n):\n print(chunk.content, end=\"\")"]
"source": [
"for chunk in generate.stream(\n",
" {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n",
"):\n",
" print(chunk.content, end=\"\")"
]
},
{
"cell_type": "markdown",
@@ -188,7 +259,50 @@
"id": "9e9a9d7c-5d2e-4194-b745-4511ec20db76",
"metadata": {},
"outputs": [],
"source": ["from typing import List, Sequence\n\nfrom langgraph.graph import END, MessageGraph, START\n\n\nasync def generation_node(state: Sequence[BaseMessage]):\n return await generate.ainvoke({\"messages\": state})\n\n\nasync def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n # Other messages we need to adjust\n cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n # First message is the original user request. We hold it the same for all nodes\n translated = [messages[0]] + [\n cls_map[msg.type](content=msg.content) for msg in messages[1:]\n ]\n res = await reflect.ainvoke({\"messages\": translated})\n # We treat the output of this as human feedback for the generator\n return HumanMessage(content=res.content)\n\n\nbuilder = MessageGraph()\nbuilder.add_node(\"generate\", generation_node)\nbuilder.add_node(\"reflect\", reflection_node)\nbuilder.add_edge(START, \"generate\")\n\n\ndef should_continue(state: List[BaseMessage]):\n if len(state) > 6:\n # End after 3 iterations\n return END\n return \"reflect\"\n\n\nbuilder.add_conditional_edges(\"generate\", should_continue)\nbuilder.add_edge(\"reflect\", \"generate\")\ngraph = builder.compile()"]
"source": [
"from typing import Annotated, List, Sequence\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
" \n",
"async def generation_node(state: Sequence[BaseMessage]):\n",
" return await generate.ainvoke({\"messages\": state})\n",
"\n",
"\n",
"async def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n",
" # Other messages we need to adjust\n",
" cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n",
" # First message is the original user request. We hold it the same for all nodes\n",
" translated = [messages[0]] + [\n",
" cls_map[msg.type](content=msg.content) for msg in messages[1:]\n",
" ]\n",
" res = await reflect.ainvoke({\"messages\": translated})\n",
" # We treat the output of this as human feedback for the generator\n",
" return HumanMessage(content=res.content)\n",
"\n",
"\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"generate\", generation_node)\n",
"builder.add_node(\"reflect\", reflection_node)\n",
"builder.add_edge(START, \"generate\")\n",
"\n",
"\n",
"def should_continue(state: List[BaseMessage]):\n",
" if len(state) > 6:\n",
" # End after 3 iterations\n",
" return END\n",
" return \"reflect\"\n",
"\n",
"\n",
"builder.add_conditional_edges(\"generate\", should_continue)\n",
"builder.add_edge(\"reflect\", \"generate\")\n",
"graph = builder.compile()"
]
},
{
"cell_type": "code",
@@ -219,7 +333,17 @@
]
}
],
"source": ["async for event in graph.astream(\n [\n HumanMessage(\n content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n )\n ],\n):\n print(event)\n print(\"---\")"]
"source": [
"async for event in graph.astream(\n",
" [\n",
" HumanMessage(\n",
" content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n",
" )\n",
" ],\n",
"):\n",
" print(event)\n",
" print(\"---\")"
]
},
{
"cell_type": "code",
@@ -371,7 +495,9 @@
]
}
],
"source": ["ChatPromptTemplate.from_messages(event[END]).pretty_print()"]
"source": [
"ChatPromptTemplate.from_messages(event[END]).pretty_print()"
]
},
{
"cell_type": "markdown",
@@ -389,7 +515,7 @@
"id": "7c0e3efd-7f54-410e-bd31-36185a46b9a8",
"metadata": {},
"outputs": [],
"source": [""]
"source": []
}
],
"metadata": {
+261 -13
View File
@@ -40,7 +40,10 @@
"id": "1b64a6f6-1d32-48be-92b5-66c3b04b17f7",
"metadata": {},
"outputs": [],
"source": ["%pip install -U --quiet langgraph langchain_anthropic\n%pip install -U --quiet tavily-python"]
"source": [
"%pip install -U --quiet langgraph langchain_anthropic\n",
"%pip install -U --quiet tavily-python"
]
},
{
"cell_type": "code",
@@ -48,7 +51,25 @@
"id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f",
"metadata": {},
"outputs": [],
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n\n_set_if_undefined(\"ANTHROPIC_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"]
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n",
"\n",
"_set_if_undefined(\"ANTHROPIC_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")"
]
},
{
"cell_type": "code",
@@ -56,7 +77,15 @@
"id": "567b6c4a",
"metadata": {},
"outputs": [],
"source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n# You could also use OpenAI or another provider\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"]
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n",
"# You could also use OpenAI or another provider\n",
"# from langchain_openai import ChatOpenAI\n",
"\n",
"# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
]
},
{
"cell_type": "markdown",
@@ -81,7 +110,13 @@
"id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2",
"metadata": {},
"outputs": [],
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"]
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n",
"\n",
"search = TavilySearchAPIWrapper()\n",
"tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"
]
},
{
"cell_type": "markdown",
@@ -97,7 +132,54 @@
"id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.messages import HumanMessage, ToolMessage\nfrom langchain_core.output_parsers.openai_tools import PydanticToolsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n\n\nclass Reflection(BaseModel):\n missing: str = Field(description=\"Critique of what is missing.\")\n superfluous: str = Field(description=\"Critique of what is superfluous\")\n\n\nclass AnswerQuestion(BaseModel):\n \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n\n answer: str = Field(description=\"~250 word detailed answer to the question.\")\n reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n search_queries: list[str] = Field(\n description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n )\n\n\nclass ResponderWithRetries:\n def __init__(self, runnable, validator):\n self.runnable = runnable\n self.validator = validator\n\n def respond(self, state: list):\n response = []\n for attempt in range(3):\n response = self.runnable.invoke(\n {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n )\n try:\n self.validator.invoke(response)\n return response\n except ValidationError as e:\n state = state + [\n response,\n ToolMessage(\n content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n + self.validator.schema_json()\n + \" Respond by fixing all validation errors.\",\n tool_call_id=response.tool_calls[0][\"id\"],\n ),\n ]\n return response"]
"source": [
"from langchain_core.messages import HumanMessage, ToolMessage\n",
"from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n",
"\n",
"\n",
"class Reflection(BaseModel):\n",
" missing: str = Field(description=\"Critique of what is missing.\")\n",
" superfluous: str = Field(description=\"Critique of what is superfluous\")\n",
"\n",
"\n",
"class AnswerQuestion(BaseModel):\n",
" \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n",
"\n",
" answer: str = Field(description=\"~250 word detailed answer to the question.\")\n",
" reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n",
" search_queries: list[str] = Field(\n",
" description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n",
" )\n",
"\n",
"\n",
"class ResponderWithRetries:\n",
" def __init__(self, runnable, validator):\n",
" self.runnable = runnable\n",
" self.validator = validator\n",
"\n",
" def respond(self, state: list):\n",
" response = []\n",
" for attempt in range(3):\n",
" response = self.runnable.invoke(\n",
" {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
" )\n",
" try:\n",
" self.validator.invoke(response)\n",
" return response\n",
" except ValidationError as e:\n",
" state = state + [\n",
" response,\n",
" ToolMessage(\n",
" content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n",
" + self.validator.schema_json()\n",
" + \" Respond by fixing all validation errors.\",\n",
" tool_call_id=response.tool_calls[0][\"id\"],\n",
" ),\n",
" ]\n",
" return response"
]
},
{
"cell_type": "code",
@@ -114,7 +196,40 @@
]
}
],
"source": ["import datetime\n\nactor_prompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are expert researcher.\nCurrent time: {time}\n\n1. {first_instruction}\n2. Reflect and critique your answer. Be severe to maximize improvement.\n3. Recommend search queries to research information and improve your answer.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"user\",\n \"\\n\\n<system>Reflect on the user's original question and the\"\n \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n ),\n ]\n).partial(\n time=lambda: datetime.datetime.now().isoformat(),\n)\ninitial_answer_chain = actor_prompt_template.partial(\n first_instruction=\"Provide a detailed ~250 word answer.\",\n function_name=AnswerQuestion.__name__,\n) | llm.bind_tools(tools=[AnswerQuestion])\nvalidator = PydanticToolsParser(tools=[AnswerQuestion])\n\nfirst_responder = ResponderWithRetries(\n runnable=initial_answer_chain, validator=validator\n)"]
"source": [
"import datetime\n",
"\n",
"actor_prompt_template = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are expert researcher.\n",
"Current time: {time}\n",
"\n",
"1. {first_instruction}\n",
"2. Reflect and critique your answer. Be severe to maximize improvement.\n",
"3. Recommend search queries to research information and improve your answer.\"\"\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"user\",\n",
" \"\\n\\n<system>Reflect on the user's original question and the\"\n",
" \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n",
" ),\n",
" ]\n",
").partial(\n",
" time=lambda: datetime.datetime.now().isoformat(),\n",
")\n",
"initial_answer_chain = actor_prompt_template.partial(\n",
" first_instruction=\"Provide a detailed ~250 word answer.\",\n",
" function_name=AnswerQuestion.__name__,\n",
") | llm.bind_tools(tools=[AnswerQuestion])\n",
"validator = PydanticToolsParser(tools=[AnswerQuestion])\n",
"\n",
"first_responder = ResponderWithRetries(\n",
" runnable=initial_answer_chain, validator=validator\n",
")"
]
},
{
"cell_type": "code",
@@ -122,7 +237,10 @@
"id": "5922e1fe-7533-4f41-8b1d-d812707c1968",
"metadata": {},
"outputs": [],
"source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"]
"source": [
"example_question = \"Why is reflection useful in AI?\"\n",
"initial = first_responder.respond([HumanMessage(content=example_question)])"
]
},
{
"cell_type": "markdown",
@@ -140,7 +258,38 @@
"id": "2605fd8d-c663-446f-ba25-751190195749",
"metadata": {},
"outputs": [],
"source": ["revise_instructions = \"\"\"Revise your previous answer using the new information.\n - You should use the previous critique to add important information to your answer.\n - You MUST include numerical citations in your revised answer to ensure it can be verified.\n - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n - [1] https://example.com\n - [2] https://example.com\n - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n\"\"\"\n\n\n# Extend the initial answer schema to include references.\n# Forcing citation in the model encourages grounded responses\nclass ReviseAnswer(AnswerQuestion):\n \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n\n cite your reflection with references, and finally\n add search queries to improve the answer.\"\"\"\n\n references: list[str] = Field(\n description=\"Citations motivating your updated answer.\"\n )\n\n\nrevision_chain = actor_prompt_template.partial(\n first_instruction=revise_instructions,\n function_name=ReviseAnswer.__name__,\n) | llm.bind_tools(tools=[ReviseAnswer])\nrevision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n\nrevisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"]
"source": [
"revise_instructions = \"\"\"Revise your previous answer using the new information.\n",
" - You should use the previous critique to add important information to your answer.\n",
" - You MUST include numerical citations in your revised answer to ensure it can be verified.\n",
" - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n",
" - [1] https://example.com\n",
" - [2] https://example.com\n",
" - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n",
"\"\"\"\n",
"\n",
"\n",
"# Extend the initial answer schema to include references.\n",
"# Forcing citation in the model encourages grounded responses\n",
"class ReviseAnswer(AnswerQuestion):\n",
" \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n",
"\n",
" cite your reflection with references, and finally\n",
" add search queries to improve the answer.\"\"\"\n",
"\n",
" references: list[str] = Field(\n",
" description=\"Citations motivating your updated answer.\"\n",
" )\n",
"\n",
"\n",
"revision_chain = actor_prompt_template.partial(\n",
" first_instruction=revise_instructions,\n",
" function_name=ReviseAnswer.__name__,\n",
") | llm.bind_tools(tools=[ReviseAnswer])\n",
"revision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n",
"\n",
"revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"
]
},
{
"cell_type": "code",
@@ -159,7 +308,25 @@
"output_type": "execute_result"
}
],
"source": ["import json\n\nrevised = revisor.respond(\n [\n HumanMessage(content=example_question),\n initial,\n ToolMessage(\n tool_call_id=initial.tool_calls[0][\"id\"],\n content=json.dumps(\n tavily_tool.invoke(\n {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n )\n ),\n ),\n ]\n)\nrevised"]
"source": [
"import json\n",
"\n",
"revised = revisor.respond(\n",
" [\n",
" HumanMessage(content=example_question),\n",
" initial,\n",
" ToolMessage(\n",
" tool_call_id=initial.tool_calls[0][\"id\"],\n",
" content=json.dumps(\n",
" tavily_tool.invoke(\n",
" {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n",
" )\n",
" ),\n",
" ),\n",
" ]\n",
")\n",
"revised"
]
},
{
"cell_type": "markdown",
@@ -177,7 +344,24 @@
"id": "fccd6a17",
"metadata": {},
"outputs": [],
"source": ["from langchain_core.tools import StructuredTool\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef run_queries(search_queries: list[str], **kwargs):\n \"\"\"Run the generated queries.\"\"\"\n return tavily_tool.batch([{\"query\": query} for query in search_queries])\n\n\ntool_node = ToolNode(\n [\n StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n ]\n)"]
"source": [
"from langchain_core.tools import StructuredTool\n",
"\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"\n",
"def run_queries(search_queries: list[str], **kwargs):\n",
" \"\"\"Run the generated queries.\"\"\"\n",
" return tavily_tool.batch([{\"query\": query} for query in search_queries])\n",
"\n",
"\n",
"tool_node = ToolNode(\n",
" [\n",
" StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n",
" StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
@@ -196,7 +380,55 @@
"id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1",
"metadata": {},
"outputs": [],
"source": ["from typing import Literal\n\nfrom langgraph.graph import END, MessageGraph, START\n\nMAX_ITERATIONS = 5\nbuilder = MessageGraph()\nbuilder.add_node(\"draft\", first_responder.respond)\n\n\nbuilder.add_node(\"execute_tools\", tool_node)\nbuilder.add_node(\"revise\", revisor.respond)\n# draft -> execute_tools\nbuilder.add_edge(\"draft\", \"execute_tools\")\n# execute_tools -> revise\nbuilder.add_edge(\"execute_tools\", \"revise\")\n\n# Define looping logic:\n\n\ndef _get_num_iterations(state: list):\n i = 0\n for m in state[::-1]:\n if m.type not in {\"tool\", \"ai\"}:\n break\n i += 1\n return i\n\n\ndef event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n # in our case, we'll just stop after N plans\n num_iterations = _get_num_iterations(state)\n if num_iterations > MAX_ITERATIONS:\n return END\n return \"execute_tools\"\n\n\n# revise -> execute_tools OR end\nbuilder.add_conditional_edges(\"revise\", event_loop)\nbuilder.add_edge(START, \"draft\")\ngraph = builder.compile()"]
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.graph import END, StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"MAX_ITERATIONS = 5\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"draft\", first_responder.respond)\n",
"\n",
"\n",
"builder.add_node(\"execute_tools\", tool_node)\n",
"builder.add_node(\"revise\", revisor.respond)\n",
"# draft -> execute_tools\n",
"builder.add_edge(\"draft\", \"execute_tools\")\n",
"# execute_tools -> revise\n",
"builder.add_edge(\"execute_tools\", \"revise\")\n",
"\n",
"# Define looping logic:\n",
"\n",
"\n",
"def _get_num_iterations(state: list):\n",
" i = 0\n",
" for m in state[::-1]:\n",
" if m.type not in {\"tool\", \"ai\"}:\n",
" break\n",
" i += 1\n",
" return i\n",
"\n",
"\n",
"def event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n",
" # in our case, we'll just stop after N plans\n",
" num_iterations = _get_num_iterations(state)\n",
" if num_iterations > MAX_ITERATIONS:\n",
" return END\n",
" return \"execute_tools\"\n",
"\n",
"\n",
"# revise -> execute_tools OR end\n",
"builder.add_conditional_edges(\"revise\", event_loop)\n",
"builder.add_edge(START, \"draft\")\n",
"graph = builder.compile()"
]
},
{
"cell_type": "code",
@@ -215,7 +447,15 @@
"output_type": "display_data"
}
],
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
},
{
"cell_type": "code",
@@ -330,7 +570,15 @@
]
}
],
"source": ["events = graph.stream(\n [HumanMessage(content=\"How should we handle the climate crisis?\")],\n stream_mode=\"values\",\n)\nfor i, step in enumerate(events):\n print(f\"Step {i}\")\n step[-1].pretty_print()"]
"source": [
"events = graph.stream(\n",
" [HumanMessage(content=\"How should we handle the climate crisis?\")],\n",
" stream_mode=\"values\",\n",
")\n",
"for i, step in enumerate(events):\n",
" print(f\"Step {i}\")\n",
" step[-1].pretty_print()"
]
},
{
"cell_type": "markdown",
+15 -9
View File
@@ -221,28 +221,34 @@ def tools_condition(
```pycon
>>> from langchain_anthropic import ChatAnthropic
>>> from langchain_core.tools import tool
>>>
>>> from langgraph.graph import MessageGraph
...
>>> from langgraph.graph import StateGraph
>>> from langgraph.prebuilt import ToolNode, tools_condition
>>>
>>> from langgraph.graph.message import add_messages
...
>>> from typing import TypedDict, Annotated
...
>>> @tool
>>> def divide(a: float, b: float) -> int:
>>> \"\"\"Return a / b.\"\"\"
>>> return a / b
>>>
... \"\"\"Return a / b.\"\"\"
... return a / b
...
>>> llm = ChatAnthropic(model="claude-3-haiku-20240307")
>>> tools = [divide]
...
>>> class State(TypedDict):
... messages: Annotated[list, add_messages]
>>>
>>> graph_builder = MessageGraph()
>>> graph_builder = StateGraph(State)
>>> graph_builder.add_node("tools", ToolNode(tools))
>>> graph_builder.add_node("chatbot", llm.bind_tools(tools))
>>> graph_builder.add_node("chatbot", lambda state: {"messages":llm.bind_tools(tools).invoke(state['messages'])})
>>> graph_builder.add_edge("tools", "chatbot")
>>> graph_builder.add_conditional_edges(
... "chatbot", tools_condition
... )
>>> graph_builder.set_entry_point("chatbot")
>>> graph = graph_builder.compile()
>>> graph.invoke([("user", "What's 329993 divided by 13662?")])
>>> graph.invoke({"messages": {"role": "user", "content": "What's 329993 divided by 13662?"}})
```
"""
if isinstance(state, list):
@@ -72,13 +72,14 @@ class ValidationNode(RunnableCallable):
Examples:
Example usage for re-prompting the model to generate a valid response:
>>> from typing import Literal
>>> from typing import Literal, Annotated, TypedDict
...
>>> from langchain_anthropic import ChatAnthropic
>>> from langchain_core.pydantic_v1 import BaseModel, validator
...
>>> from langgraph.graph import END, START, MessageGraph
>>> from langgraph.graph import END, START, StateGraph
>>> from langgraph.prebuilt import ValidationNode
>>> from langgraph.graph.message import add_messages
...
...
>>> class SelectNumber(BaseModel):
@@ -91,7 +92,10 @@ class ValidationNode(RunnableCallable):
... return v
...
...
>>> builder = MessageGraph()
>>> class State(TypedDict):
... messages: Annotated[list, add_messages]
...
>>> builder = StateGraph(State)
>>> llm = ChatAnthropic(model="claude-3-haiku-20240307").bind_tools([SelectNumber])
>>> builder.add_node("model", llm)
>>> builder.add_node("validation", ValidationNode([SelectNumber]))