mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-07 18:27:52 +02:00
Update hierarchical agents notebook
Update hierarchical agents notebook
This commit is contained in:
@@ -21,16 +21,17 @@
|
||||
"\n",
|
||||
"This notebook is inspired by the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://arxiv.org/abs/2308.08155), by Wu, et. al. In the rest of this notebook, you will:\n",
|
||||
"\n",
|
||||
"1. Define some utilities to help create the graph and their relations\n",
|
||||
"2. Write the tools and agent implementations for each team\n",
|
||||
"3. Compose everything together.\n",
|
||||
"1. Define the agents' tools to access the web and write files\n",
|
||||
"2. Define some utilities to help create the graph and agents\n",
|
||||
"3. Create and define each team (web research + doc writing)\n",
|
||||
"4. Compose everything together.\n",
|
||||
"\n",
|
||||
"But before all of that, some setup:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -41,7 +42,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 1,
|
||||
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -68,144 +69,24 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "504ee1c6-2b6a-439d-9046-df54e1e15698",
|
||||
"id": "354568e2-aef0-4af9-8a79-e64d3eea752f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define Utilities\n",
|
||||
"## Create Tools\n",
|
||||
"\n",
|
||||
"We are going to create a few utility functions to make it more concise when we want to:\n",
|
||||
"Each team will be composed of one or more agents each with one or more tools. Below, define all the tools to be used by your different teams.\n",
|
||||
"\n",
|
||||
"1. Create a worker agent and add it to a graph.\n",
|
||||
"2. Create a supervisor for the sub-graph.\n",
|
||||
"We'll start with the research team.\n",
|
||||
"\n",
|
||||
"These will simplify the graph compositional code at the end for us so it's easier to see what's going on."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "e09fb60f-1aac-455b-b67d-8d2e4ccfd747",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Any, Callable, List, Optional, TypedDict, Union\n",
|
||||
"\n",
|
||||
"from langchain.agents import AgentExecutor, create_openai_functions_agent\n",
|
||||
"from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_core.runnables import Runnable\n",
|
||||
"from langchain_core.tools import BaseTool\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def create_agent(\n",
|
||||
" llm: ChatOpenAI,\n",
|
||||
" tools: list,\n",
|
||||
" system_prompt: str,\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n",
|
||||
" system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n",
|
||||
" \" Do not ask for clarification.\"\n",
|
||||
" \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n",
|
||||
" \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n",
|
||||
" prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" system_prompt,\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" agent = create_openai_functions_agent(llm, tools, prompt)\n",
|
||||
" executor = AgentExecutor(agent=agent, tools=tools)\n",
|
||||
" return executor\n",
|
||||
" # chain = executor | (\n",
|
||||
" # lambda x: {\"messages\": [HumanMessage(content=x[\"output\"], name=name)]}\n",
|
||||
" # )\n",
|
||||
" # if prelude is not None:\n",
|
||||
" # chain = prelude | chain\n",
|
||||
" # graph_builder.add_node(name, chain)\n",
|
||||
" # return name\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def agent_node(state, agent, name):\n",
|
||||
" result = agent.invoke(state)\n",
|
||||
" return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def create_team_supervisor(\n",
|
||||
" llm: ChatOpenAI, system_prompt, members\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"An LLM-based router.\"\"\"\n",
|
||||
" options = [\"FINISH\"] + members\n",
|
||||
" function_def = {\n",
|
||||
" \"name\": \"route\",\n",
|
||||
" \"description\": \"Select the next role.\",\n",
|
||||
" \"parameters\": {\n",
|
||||
" \"title\": \"routeSchema\",\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\n",
|
||||
" \"next\": {\n",
|
||||
" \"title\": \"Next\",\n",
|
||||
" \"anyOf\": [\n",
|
||||
" {\"enum\": options},\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" \"required\": [\"next\"],\n",
|
||||
" },\n",
|
||||
" }\n",
|
||||
" prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\"system\", system_prompt),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"Given the conversation above, who should act next?\"\n",
|
||||
" \" Or should we FINISH? Select one of: {options}\",\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
" ).partial(options=str(options), team_members=\", \".join(members))\n",
|
||||
" chain = (\n",
|
||||
" prompt\n",
|
||||
" | llm.bind_functions(functions=[function_def], function_call=\"route\")\n",
|
||||
" | JsonOutputFunctionsParser()\n",
|
||||
" )\n",
|
||||
" return chain\n",
|
||||
" # graph_builder.add_node(supervisor_name, chain)\n",
|
||||
" # conditional_map = {k: k for k in members}\n",
|
||||
" # conditional_map[\"FINISH\"] = END\n",
|
||||
"\n",
|
||||
" # for member in members:\n",
|
||||
" # graph_builder.add_edge(member, supervisor_name)\n",
|
||||
" # graph_builder.add_conditional_edges(\n",
|
||||
" # supervisor_name, lambda x: x[\"next\"], conditional_map\n",
|
||||
" # )\n",
|
||||
" # return supervisor_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "00282b1f-bb4d-4ee7-9bae-e8e6f586f12e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define agents + tools\n",
|
||||
"\n",
|
||||
"Now we can get to define our hierachical teams. \"Choose your player!\"\n",
|
||||
"\n",
|
||||
"### Research Team\n",
|
||||
"**Research team tools**\n",
|
||||
"\n",
|
||||
"The research team can use a search engine and url scraper to find information on the web. Feel free to add additional functionality below to boost the team performance!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "f04c6778-403b-4b49-9b93-678e910d5cec",
|
||||
"execution_count": 2,
|
||||
"id": "4024eb89-843d-4cc3-ab3f-e1eb4d031179",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -233,154 +114,29 @@
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "53db0c78-e357-48ba-ae5f-3fc04735a3b7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import functools\n",
|
||||
"import operator\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
|
||||
"from langchain_openai.chat_models import ChatOpenAI\n",
|
||||
"import functools\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Research team graph state\n",
|
||||
"class State(TypedDict):\n",
|
||||
" # A message is added after each team member finishes\n",
|
||||
" messages: Annotated[List[BaseMessage], operator.add]\n",
|
||||
" # The team members are tracked so they are aware of\n",
|
||||
" # the others' skill-sets\n",
|
||||
" team_members: List[str]\n",
|
||||
" # Used to route work. The supervisor calls a function\n",
|
||||
" # that will update this every time it makes a decision\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"search_agent = create_agent(llm, [tavily_tool], \"You are a research assistant who can search for up-to-date info using the tavily search engine.\")\n",
|
||||
"search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n",
|
||||
"\n",
|
||||
"research_agent = create_agent(llm, [scrape_webpages], \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\")\n",
|
||||
"research_node = functools.partial(agent_node, agent=research_agent, name=\"Web Scraper\")\n",
|
||||
"\n",
|
||||
"supervisor_agent = create_team_supervisor(\n",
|
||||
" llm,\n",
|
||||
" \"You are a supervisor tasked with managing a conversation between the\"\n",
|
||||
" \" following workers: Search, Web Scraper. Given the following user request,\"\n",
|
||||
" \" respond with the worker to act next. Each worker will perform a\"\n",
|
||||
" \" task and respond with their results and status. When finished,\"\n",
|
||||
" \" respond with FINISH.\",\n",
|
||||
" [\"Search\", \"Web Scraper\"],\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_graph = StateGraph(State)\n",
|
||||
"research_graph.add_node(\"Search\", search_node)\n",
|
||||
"research_graph.add_node(\"Web Scraper\", research_node)\n",
|
||||
"research_graph.add_node(\"supervisor\", supervisor_agent)\n",
|
||||
"\n",
|
||||
"research_graph.add_edge(\"Search\", \"supervisor\")\n",
|
||||
"research_graph.add_edge(\"Web Scraper\", \"supervisor\")\n",
|
||||
"research_graph.add_conditional_edges(\n",
|
||||
" \"supervisor\",\n",
|
||||
" lambda x: x[\"next\"],\n",
|
||||
" {\n",
|
||||
" \"Search\": \"Search\",\n",
|
||||
" \"Web Scraper\": \"Web Scraper\",\n",
|
||||
" \"FINISH\": END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_graph.set_entry_point(\"supervisor\")\n",
|
||||
"\n",
|
||||
"chain = research_graph.compile()\n",
|
||||
"\n",
|
||||
"# The following functions interoperate between the top level graph state\n",
|
||||
"# and the state of the research sub-graph\n",
|
||||
"# this makes it so that the states of each graph don't get intermixed\n",
|
||||
"def enter_chain(message: str):\n",
|
||||
" results = {\n",
|
||||
" \"messages\": [HumanMessage(content=message)],\n",
|
||||
" }\n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def return_final_response(state):\n",
|
||||
" return {\"final_response\": state[\"messages\"][-1]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_chain = (\n",
|
||||
" enter_chain\n",
|
||||
" | chain\n",
|
||||
" | return_final_response\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "912b0604-a178-4246-a36f-2dedae606680",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "GraphRecursionError",
|
||||
"evalue": "Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limitby setting the `recursion_limit` config key.",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
||||
"\u001b[0;31mGraphRecursionError\u001b[0m Traceback (most recent call last)",
|
||||
"Cell \u001b[0;32mIn[12], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43ms\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mresearch_chain\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mwhat is temperature in SF right now?\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m__end__\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43ms\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43ms\u001b[49m\u001b[43m)\u001b[49m\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:2416\u001b[0m, in \u001b[0;36mRunnableSequence.stream\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 2410\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mstream\u001b[39m(\n\u001b[1;32m 2411\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 2412\u001b[0m \u001b[38;5;28minput\u001b[39m: Input,\n\u001b[1;32m 2413\u001b[0m config: Optional[RunnableConfig] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 2414\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Optional[Any],\n\u001b[1;32m 2415\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Output]:\n\u001b[0;32m-> 2416\u001b[0m \u001b[38;5;28;01myield from\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtransform(\u001b[38;5;28miter\u001b[39m([\u001b[38;5;28minput\u001b[39m]), config, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:2403\u001b[0m, in \u001b[0;36mRunnableSequence.transform\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 2397\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtransform\u001b[39m(\n\u001b[1;32m 2398\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 2399\u001b[0m \u001b[38;5;28minput\u001b[39m: Iterator[Input],\n\u001b[1;32m 2400\u001b[0m config: Optional[RunnableConfig] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 2401\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Optional[Any],\n\u001b[1;32m 2402\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Output]:\n\u001b[0;32m-> 2403\u001b[0m \u001b[38;5;28;01myield from\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_transform_stream_with_config(\n\u001b[1;32m 2404\u001b[0m \u001b[38;5;28minput\u001b[39m,\n\u001b[1;32m 2405\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_transform,\n\u001b[1;32m 2406\u001b[0m patch_config(config, run_name\u001b[38;5;241m=\u001b[39m(config \u001b[38;5;129;01mor\u001b[39;00m {})\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_name\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mname),\n\u001b[1;32m 2407\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m 2408\u001b[0m )\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:1497\u001b[0m, in \u001b[0;36mRunnable._transform_stream_with_config\u001b[0;34m(self, input, transformer, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 1495\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1496\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m-> 1497\u001b[0m chunk: Output \u001b[38;5;241m=\u001b[39m \u001b[43mcontext\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43miterator\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 1498\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m chunk\n\u001b[1;32m 1499\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m final_output_supported:\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:2367\u001b[0m, in \u001b[0;36mRunnableSequence._transform\u001b[0;34m(self, input, run_manager, config)\u001b[0m\n\u001b[1;32m 2358\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m step \u001b[38;5;129;01min\u001b[39;00m steps:\n\u001b[1;32m 2359\u001b[0m final_pipeline \u001b[38;5;241m=\u001b[39m step\u001b[38;5;241m.\u001b[39mtransform(\n\u001b[1;32m 2360\u001b[0m final_pipeline,\n\u001b[1;32m 2361\u001b[0m patch_config(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 2364\u001b[0m ),\n\u001b[1;32m 2365\u001b[0m )\n\u001b[0;32m-> 2367\u001b[0m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mfinal_pipeline\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 2368\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:3435\u001b[0m, in \u001b[0;36mRunnableLambda.transform\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 3428\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtransform\u001b[39m(\n\u001b[1;32m 3429\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 3430\u001b[0m \u001b[38;5;28minput\u001b[39m: Iterator[Input],\n\u001b[1;32m 3431\u001b[0m config: Optional[RunnableConfig] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 3432\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Optional[Any],\n\u001b[1;32m 3433\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Output]:\n\u001b[1;32m 3434\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfunc\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m-> 3435\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform_stream_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3436\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3437\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3438\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3439\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3440\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 3441\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\n\u001b[1;32m 3442\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:1497\u001b[0m, in \u001b[0;36mRunnable._transform_stream_with_config\u001b[0;34m(self, input, transformer, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 1495\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1496\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m-> 1497\u001b[0m chunk: Output \u001b[38;5;241m=\u001b[39m context\u001b[38;5;241m.\u001b[39mrun(\u001b[38;5;28mnext\u001b[39m, iterator) \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 1498\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m chunk\n\u001b[1;32m 1499\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m final_output_supported:\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:3380\u001b[0m, in \u001b[0;36mRunnableLambda._transform\u001b[0;34m(self, input, run_manager, config, **kwargs)\u001b[0m\n\u001b[1;32m 3372\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_transform\u001b[39m(\n\u001b[1;32m 3373\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 3374\u001b[0m \u001b[38;5;28minput\u001b[39m: Iterator[Input],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 3377\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 3378\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Output]:\n\u001b[1;32m 3379\u001b[0m final: Optional[Input] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m-> 3380\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43michunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m:\u001b[49m\n\u001b[1;32m 3381\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mfinal\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mis\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m:\u001b[49m\n\u001b[1;32m 3382\u001b[0m \u001b[43m \u001b[49m\u001b[43mfinal\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43michunk\u001b[49m\n",
|
||||
"File \u001b[0;32m~/workplace/permchain/langgraph/pregel/__init__.py:567\u001b[0m, in \u001b[0;36mPregel.transform\u001b[0;34m(self, input, config, output_keys, input_keys, **kwargs)\u001b[0m\n\u001b[1;32m 558\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtransform\u001b[39m(\n\u001b[1;32m 559\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 560\u001b[0m \u001b[38;5;28minput\u001b[39m: Iterator[Union[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any], Any]],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 565\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 566\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Union[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any], Any]]:\n\u001b[0;32m--> 567\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform_stream_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 568\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 569\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 570\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 571\u001b[0m \u001b[43m \u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 572\u001b[0m \u001b[43m \u001b[49m\u001b[43minput_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 573\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 574\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 575\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\n",
|
||||
"File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:1497\u001b[0m, in \u001b[0;36mRunnable._transform_stream_with_config\u001b[0;34m(self, input, transformer, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 1495\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1496\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m-> 1497\u001b[0m chunk: Output \u001b[38;5;241m=\u001b[39m context\u001b[38;5;241m.\u001b[39mrun(\u001b[38;5;28mnext\u001b[39m, iterator) \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 1498\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m chunk\n\u001b[1;32m 1499\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m final_output_supported:\n",
|
||||
"File \u001b[0;32m~/workplace/permchain/langgraph/pregel/__init__.py:299\u001b[0m, in \u001b[0;36mPregel._transform\u001b[0;34m(self, input, run_manager, config, input_keys, output_keys)\u001b[0m\n\u001b[1;32m 297\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[1;32m 298\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m step \u001b[38;5;241m==\u001b[39m config[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[0;32m--> 299\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m GraphRecursionError(\n\u001b[1;32m 300\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRecursion limit of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mconfig[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m reached\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 301\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwithout hitting a stop condition. You can increase the limit\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 302\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mby setting the `recursion_limit` config key.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 303\u001b[0m )\n\u001b[1;32m 305\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdebug:\n\u001b[1;32m 306\u001b[0m print_step_start(step, next_tasks)\n",
|
||||
"\u001b[0;31mGraphRecursionError\u001b[0m: Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limitby setting the `recursion_limit` config key."
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for s in research_chain.stream(\"what is temperature in SF right now?\"):\n",
|
||||
" if \"__end__\" not in s:\n",
|
||||
" print(s)\n",
|
||||
" print(\"---\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "749b99ab-f6f0-4c5d-a90b-10102465d186",
|
||||
"id": "1c427982-fadf-4721-a77e-2465df9fc6bc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Document Writing Team\n",
|
||||
"**Document writing team tools**\n",
|
||||
"\n",
|
||||
"We will construct a graph in a similar fashion. This time using different tools.\n",
|
||||
"Next up, we will give some tools for the doc writing team to use.\n",
|
||||
"We define some bare-bones file-access tools below.\n",
|
||||
"\n",
|
||||
"Note that we are giving file-system access to our agent here, which is not safe in all cases."
|
||||
"Note that this gives the agents access to your file-system, which can be unsafe. We also haven't optimized the tool descriptions for performance."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "202806d6-80bf-4153-ac16-ed6059236f2a",
|
||||
"execution_count": 3,
|
||||
"id": "f20a18ca-2709-4c12-84f3-88678591a9fa",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"from tempfile import TemporaryDirectory\n",
|
||||
"from typing import Dict\n",
|
||||
"from typing import Dict, Optional\n",
|
||||
"\n",
|
||||
"from langchain_experimental.utilities import PythonREPL\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
@@ -435,23 +191,18 @@
|
||||
" ],\n",
|
||||
") -> Annotated[str, \"Path of the edited document file.\"]:\n",
|
||||
" \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n",
|
||||
" # Read the contents of the file\n",
|
||||
"\n",
|
||||
" with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n",
|
||||
" lines = file.readlines()\n",
|
||||
"\n",
|
||||
" # Adjust the line numbers for 0-indexing and sort\n",
|
||||
" sorted_inserts = sorted(inserts.items())\n",
|
||||
"\n",
|
||||
" # Perform the insertions\n",
|
||||
" for line_number, text in sorted_inserts:\n",
|
||||
" if 1 <= line_number <= len(lines) + 1:\n",
|
||||
" # Insert the text at the specified line number\n",
|
||||
" lines.insert(line_number - 1, text + \"\\n\")\n",
|
||||
" else:\n",
|
||||
" return f\"Error: Line number {line_number} is out of range.\"\n",
|
||||
"\n",
|
||||
" # Write the modified content back to the file\n",
|
||||
" with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n",
|
||||
" file.writelines(lines)\n",
|
||||
"\n",
|
||||
@@ -476,9 +227,276 @@
|
||||
" return f\"Succesfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "504ee1c6-2b6a-439d-9046-df54e1e15698",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Helper Utilities\n",
|
||||
"\n",
|
||||
"We are going to create a few utility functions to make it more concise when we want to:\n",
|
||||
"\n",
|
||||
"1. Create a worker agent.\n",
|
||||
"2. Create a supervisor for the sub-graph.\n",
|
||||
"\n",
|
||||
"These will simplify the graph compositional code at the end for us so it's easier to see what's going on."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 4,
|
||||
"id": "e09fb60f-1aac-455b-b67d-8d2e4ccfd747",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Any, Callable, List, Optional, TypedDict, Union\n",
|
||||
"\n",
|
||||
"from langchain.agents import AgentExecutor, create_openai_functions_agent\n",
|
||||
"from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_core.runnables import Runnable\n",
|
||||
"from langchain_core.tools import BaseTool\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def create_agent(\n",
|
||||
" llm: ChatOpenAI,\n",
|
||||
" tools: list,\n",
|
||||
" system_prompt: str,\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n",
|
||||
" system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n",
|
||||
" \" Do not ask for clarification.\"\n",
|
||||
" \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n",
|
||||
" \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n",
|
||||
" prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" system_prompt,\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" agent = create_openai_functions_agent(llm, tools, prompt)\n",
|
||||
" executor = AgentExecutor(agent=agent, tools=tools)\n",
|
||||
" return executor\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def agent_node(state, agent, name):\n",
|
||||
" result = agent.invoke(state)\n",
|
||||
" return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def create_team_supervisor(\n",
|
||||
" llm: ChatOpenAI, system_prompt, members\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"An LLM-based router.\"\"\"\n",
|
||||
" options = [\"FINISH\"] + members\n",
|
||||
" function_def = {\n",
|
||||
" \"name\": \"route\",\n",
|
||||
" \"description\": \"Select the next role.\",\n",
|
||||
" \"parameters\": {\n",
|
||||
" \"title\": \"routeSchema\",\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\n",
|
||||
" \"next\": {\n",
|
||||
" \"title\": \"Next\",\n",
|
||||
" \"anyOf\": [\n",
|
||||
" {\"enum\": options},\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" \"required\": [\"next\"],\n",
|
||||
" },\n",
|
||||
" }\n",
|
||||
" prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\"system\", system_prompt),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"Given the conversation above, who should act next?\"\n",
|
||||
" \" Or should we FINISH? Select one of: {options}\",\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
" ).partial(options=str(options), team_members=\", \".join(members))\n",
|
||||
" return (\n",
|
||||
" prompt\n",
|
||||
" | llm.bind_functions(functions=[function_def], function_call=\"route\")\n",
|
||||
" | JsonOutputFunctionsParser()\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "00282b1f-bb4d-4ee7-9bae-e8e6f586f12e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define Agent Teams\n",
|
||||
"\n",
|
||||
"Now we can get to define our hierachical teams. \"Choose your player!\"\n",
|
||||
"\n",
|
||||
"### Research Team\n",
|
||||
"\n",
|
||||
"The research team will have a search agent and a web scraping \"research_agent\" as the two worker nodes. Let's create those, as well as the team supervisor."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "53db0c78-e357-48ba-ae5f-3fc04735a3b7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import functools\n",
|
||||
"import operator\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
|
||||
"from langchain_openai.chat_models import ChatOpenAI\n",
|
||||
"import functools\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Research team graph state\n",
|
||||
"class ResearchTeamState(TypedDict):\n",
|
||||
" # A message is added after each team member finishes\n",
|
||||
" messages: Annotated[List[BaseMessage], operator.add]\n",
|
||||
" # The team members are tracked so they are aware of\n",
|
||||
" # the others' skill-sets\n",
|
||||
" team_members: List[str]\n",
|
||||
" # Used to route work. The supervisor calls a function\n",
|
||||
" # that will update this every time it makes a decision\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
|
||||
"\n",
|
||||
"search_agent = create_agent(llm, [tavily_tool], \"You are a research assistant who can search for up-to-date info using the tavily search engine.\")\n",
|
||||
"search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n",
|
||||
"\n",
|
||||
"research_agent = create_agent(llm, [scrape_webpages], \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\")\n",
|
||||
"research_node = functools.partial(agent_node, agent=research_agent, name=\"Web Scraper\")\n",
|
||||
"\n",
|
||||
"supervisor_agent = create_team_supervisor(\n",
|
||||
" llm,\n",
|
||||
" \"You are a supervisor tasked with managing a conversation between the\"\n",
|
||||
" \" following workers: Search, Web Scraper. Given the following user request,\"\n",
|
||||
" \" respond with the worker to act next. Each worker will perform a\"\n",
|
||||
" \" task and respond with their results and status. When finished,\"\n",
|
||||
" \" respond with FINISH.\",\n",
|
||||
" [\"Search\", \"Web Scraper\"],\n",
|
||||
")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b01c6ee8-a461-4081-8a97-a3a06ec0f994",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now that we've created the necessary components, defining their interactions is easy. Add the nodes to the team graph, and define the edges, which determine the transition criteria."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"id": "1a7a1260-d9f6-4011-b2b1-13fab5126997",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"research_graph = StateGraph(ResearchTeamState)\n",
|
||||
"research_graph.add_node(\"Search\", search_node)\n",
|
||||
"research_graph.add_node(\"Web Scraper\", research_node)\n",
|
||||
"research_graph.add_node(\"supervisor\", supervisor_agent)\n",
|
||||
"\n",
|
||||
"# Define the control flow\n",
|
||||
"research_graph.add_edge(\"Search\", \"supervisor\")\n",
|
||||
"research_graph.add_edge(\"Web Scraper\", \"supervisor\")\n",
|
||||
"research_graph.add_conditional_edges(\n",
|
||||
" \"supervisor\",\n",
|
||||
" lambda x: x[\"next\"],\n",
|
||||
" {\n",
|
||||
" \"Search\": \"Search\",\n",
|
||||
" \"Web Scraper\": \"Web Scraper\",\n",
|
||||
" \"FINISH\": END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_graph.set_entry_point(\"supervisor\")\n",
|
||||
"chain = research_graph.compile()\n",
|
||||
"\n",
|
||||
"# The following functions interoperate between the top level graph state\n",
|
||||
"# and the state of the research sub-graph\n",
|
||||
"# this makes it so that the states of each graph don't get intermixed\n",
|
||||
"def enter_chain(message: str):\n",
|
||||
" results = {\n",
|
||||
" \"messages\": [HumanMessage(content=message)],\n",
|
||||
" }\n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"research_chain = (\n",
|
||||
" enter_chain\n",
|
||||
" | chain\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "63ee8f2c-fbde-427b-ba54-ae0c7ce5fbfb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can give this team work directly. Try it out below."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"id": "912b0604-a178-4246-a36f-2dedae606680",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'supervisor': {'next': 'Search'}}\n",
|
||||
"---\n",
|
||||
"{'Search': {'messages': [HumanMessage(content='Taylor Swift\\'s next tour, titled \"The Eras Tour,\" began on March 17, 2023, in Glendale, Arizona. The tour is scheduled to continue with various dates throughout 2023 and has added new U.S. dates for 2024. The tour will also go international this year and the next, with dates yet to be announced for the international leg. For specific dates and locations, fans can check the official announcements and ticketing websites.\\n\\nTo get the most updated information on the tour dates and locations, fans should refer to Taylor Swift\\'s official channels or trusted ticketing platforms.', name='Search')]}}\n",
|
||||
"---\n",
|
||||
"{'supervisor': {'next': 'FINISH'}}\n",
|
||||
"---\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for s in research_chain.stream(\n",
|
||||
" \"when is Taylor Swift's next tour?\",\n",
|
||||
" {\"recursion_limit\": 100}\n",
|
||||
" ):\n",
|
||||
" if \"__end__\" not in s:\n",
|
||||
" print(s)\n",
|
||||
" print(\"---\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "749b99ab-f6f0-4c5d-a90b-10102465d186",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Document Writing Team\n",
|
||||
"\n",
|
||||
"Create the document writing team below using a similar approach. This time, we will give each agent access to different file-writing tools.\n",
|
||||
"\n",
|
||||
"Note that we are giving file-system access to our agent here, which is not safe in all cases."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"id": "1bcdbf44-9481-430c-8429-fa142ed8a626",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -488,7 +506,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# Document writing team graph state\n",
|
||||
"class AuthoringState(TypedDict):\n",
|
||||
"class DocWritingState(TypedDict):\n",
|
||||
" # This tracks the team's conversation internally\n",
|
||||
" messages: Annotated[List[BaseMessage], operator.add]\n",
|
||||
" # This provides each worker with context on the others' skill sets\n",
|
||||
@@ -521,58 +539,143 @@
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Create the graph here:\n",
|
||||
"authoring_graph = StateGraph(AuthoringState)\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
|
||||
"create_worker_agent(\n",
|
||||
" authoring_graph,\n",
|
||||
" \"Author Docs\",\n",
|
||||
"\n",
|
||||
"doc_writer_agent = create_agent(\n",
|
||||
" llm,\n",
|
||||
" [write_document, edit_document, read_document],\n",
|
||||
" \"You are an expert writing a research document.\\n\"\n",
|
||||
" # The {current_files} value is populated automatically by the graph state\n",
|
||||
" \"Below are files currently in your directory:\\n{current_files}\",\n",
|
||||
" prelude=prelude,\n",
|
||||
")\n",
|
||||
"create_worker_agent(\n",
|
||||
" authoring_graph,\n",
|
||||
" \"Outline + Notetaker\",\n",
|
||||
"# Injects current directory working state before each call\n",
|
||||
"context_aware_doc_writer_agent = prelude | doc_writer_agent\n",
|
||||
"doc_writing_node = functools.partial(agent_node, agent=context_aware_doc_writer_agent, name=\"Doc Writer\")\n",
|
||||
"\n",
|
||||
"note_taking_agent = create_agent(\n",
|
||||
" llm,\n",
|
||||
" [create_outline, read_document],\n",
|
||||
" \"You are an expert senior researcher tasked with writing a paper outline and\"\n",
|
||||
" \" taking notes to craft a perfect paper.{current_files}\",\n",
|
||||
" prelude=prelude,\n",
|
||||
")\n",
|
||||
"create_worker_agent(\n",
|
||||
" authoring_graph,\n",
|
||||
" \"Generate Charts\",\n",
|
||||
"context_aware_note_taking_agent = prelude | note_taking_agent\n",
|
||||
"note_taking_node = functools.partial(agent_node, agent=context_aware_note_taking_agent, name=\"Note Taker\")\n",
|
||||
"\n",
|
||||
"chart_generating_agent = create_agent(\n",
|
||||
" llm,\n",
|
||||
" [read_document, python_repl],\n",
|
||||
" \"You are a data viz expert tasked with generating charts for a research project.\"\n",
|
||||
" \"{current_files}\",\n",
|
||||
")\n",
|
||||
"context_aware_chart_generating_agent = prelude | chart_generating_agent\n",
|
||||
"chart_generating_node = functools.partial(agent_node, agent=context_aware_note_taking_agent, name=\"Chart Generator\")\n",
|
||||
"\n",
|
||||
"supervisor_node = create_team_supervisor(\n",
|
||||
" authoring_graph,\n",
|
||||
"doc_writing_supervisor = create_team_supervisor(\n",
|
||||
" llm,\n",
|
||||
" \"You are a supervisor tasked with managing a conversation between the\"\n",
|
||||
" \" following workers: {team_members}. Given the following user request,\"\n",
|
||||
" \" respond with the worker to act next. Each worker will perform a\"\n",
|
||||
" \" task and respond with their results and status. When finished,\"\n",
|
||||
" \" respond with FINISH.\",\n",
|
||||
" [\"Doc Writer\", \"Note Taker\", \"Chart Generator\"]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "aee2cd9b-29aa-458e-903d-4e49179e5d59",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"With the objects themselves created, we can form the graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 32,
|
||||
"id": "9c5c644f-8966-4d2e-98d2-80d73520e9fe",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create the graph here:\n",
|
||||
"# Note that we have unrolled the loop for the sake of this doc\n",
|
||||
"authoring_graph = StateGraph(DocWritingState)\n",
|
||||
"authoring_graph.add_node(\"Doc Writer\", doc_writing_node)\n",
|
||||
"authoring_graph.add_node(\"Note Taker\", note_taking_node)\n",
|
||||
"authoring_graph.add_node(\"Chart Generator\", chart_generating_node)\n",
|
||||
"authoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n",
|
||||
"\n",
|
||||
"# Add the edges that always occur\n",
|
||||
"authoring_graph.add_edge(\"Doc Writer\", \"supervisor\")\n",
|
||||
"authoring_graph.add_edge(\"Note Taker\", \"supervisor\")\n",
|
||||
"authoring_graph.add_edge(\"Chart Generator\", \"supervisor\")\n",
|
||||
"\n",
|
||||
"# Add the edges where routing applies\n",
|
||||
"authoring_graph.add_conditional_edges(\n",
|
||||
" \"supervisor\",\n",
|
||||
" lambda x: x[\"next\"],\n",
|
||||
" {\n",
|
||||
" \"Doc Writer\": \"Doc Writer\",\n",
|
||||
" \"Note Taker\": \"Note Taker\",\n",
|
||||
" \"Chart Generator\": \"Chart Generator\",\n",
|
||||
" \"FINISH\": END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"authoring_graph.set_entry_point(supervisor_node)\n",
|
||||
"authoring_graph.set_entry_point(\"supervisor\")\n",
|
||||
"chain = research_graph.compile()\n",
|
||||
"\n",
|
||||
"# The following functions interoperate between the top level graph state\n",
|
||||
"# and the state of the research sub-graph\n",
|
||||
"# this makes it so that the states of each graph don't get intermixed\n",
|
||||
"def enter_chain(message: str, members: List[str]):\n",
|
||||
" results = {\n",
|
||||
" \"messages\": [HumanMessage(content=message)],\n",
|
||||
" \"team_members\": \", \".join(members)\n",
|
||||
" }\n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# We re-use the enter/exit functions to wrap the graph\n",
|
||||
"authoring_chain = (\n",
|
||||
" functools.partial(enter_chain, members=authoring_graph.nodes)\n",
|
||||
" | authoring_graph.compile()\n",
|
||||
" | return_final_response\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 33,
|
||||
"id": "9860fd46-c24d-40a5-a6ba-e8fddcd43369",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'supervisor': {'next': 'Note Taker'}}\n",
|
||||
"---\n",
|
||||
"{'Note Taker': {'messages': [HumanMessage(content='The poem \"Whispers of the Ancient Wind\" is now written and saved to disk as follows:\\n\\n1. **Introduction**\\n In the hush of early dawn, where shadows softly tread,\\n\\n2. **Setting the Scene**\\n The whispers of the ancient wind stir the slumbering bed.\\n\\n3. **Rising Action**\\n It speaks in tongues of yesteryears, a wisdom deep and wide,\\n Through rustling leaves and bending boughs, it shares what time can\\'t hide.\\n\\n4. **Climax**\\n The gale then rises, fierce and bold, a truth it must impart,\\n With howling force and chilling breath, it seeks the willing heart.\\n\\n5. **Falling Action**\\n Yet as the gusts begin to wane, the message becomes clear,\\n\\n6. **Resolution**\\n In every gust and gentle breeze, the past is always near.\\n\\n7. **Conclusion**\\n So listen close when breezes blow, and hear the silent din,\\n Of stories old and lessons told, in whispers of the wind.\\n\\nThis structure captures the narrative flow of the poem, moving from the calm introduction to the intense climax, and finally to the insightful conclusion.', name='Note Taker')]}}\n",
|
||||
"---\n",
|
||||
"{'supervisor': {'next': 'Doc Writer'}}\n",
|
||||
"---\n",
|
||||
"{'Doc Writer': {'messages': [HumanMessage(content='The poem \"Whispers of the Ancient Wind\" has been successfully written and saved to disk with the file name \"WhispersOfTheAncientWind.txt\".', name='Doc Writer')]}}\n",
|
||||
"---\n",
|
||||
"{'supervisor': {'next': 'FINISH'}}\n",
|
||||
"---\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for s in authoring_chain.stream(\n",
|
||||
" \"Write an outline for poem and then write the poem to disk.\",\n",
|
||||
" {\"recursion_limit\": 100}\n",
|
||||
" ):\n",
|
||||
" if \"__end__\" not in s:\n",
|
||||
" print(s)\n",
|
||||
" print(\"---\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f4b5b08d-9a9a-474a-94b4-f7aaa8ff19e6",
|
||||
@@ -587,7 +690,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 40,
|
||||
"id": "95ae7e52-92ed-41a3-88c4-21b6d7c8b041",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -596,69 +699,105 @@
|
||||
"from langchain_openai.chat_models import ChatOpenAI\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Research team graph\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[List[BaseMessage], operator.add]\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_last_message(state: State) -> str:\n",
|
||||
" return state[\"messages\"][-1].content\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def join_graph(response: dict):\n",
|
||||
" return {\"messages\": [response[\"final_response\"]]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"super_graph = StateGraph(State)\n",
|
||||
"super_graph.add_node(\"Research team\", get_last_message | research_chain | join_graph)\n",
|
||||
"super_graph.add_node(\n",
|
||||
" \"Paper writing team\", get_last_message | authoring_chain | join_graph\n",
|
||||
")\n",
|
||||
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
|
||||
"\n",
|
||||
"supervisor_node = create_team_supervisor(\n",
|
||||
" super_graph,\n",
|
||||
" llm,\n",
|
||||
" \"You are a supervisor tasked with managing a conversation between the\"\n",
|
||||
" \" following teams: {team_members}. Given the following user request,\"\n",
|
||||
" \" respond with the worker to act next. Each worker will perform a\"\n",
|
||||
" \" task and respond with their results and status. When finished,\"\n",
|
||||
" \" respond with FINISH.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"super_graph.set_entry_point(supervisor_node)\n",
|
||||
"super_graph = enter_chain | super_graph.compile()"
|
||||
" [\"Research team\", \"Paper writing team\"],\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"execution_count": 44,
|
||||
"id": "4880e573-612f-4d24-97c1-2079382a4a2f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Top-level graph state\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[List[BaseMessage], operator.add]\n",
|
||||
" next: str\n",
|
||||
"\n",
|
||||
"def get_last_message(state: State) -> str:\n",
|
||||
" return state[\"messages\"][-1].content\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def join_graph(response: dict):\n",
|
||||
" return {\"messages\": [response[\"messages\"][-1]]}\n",
|
||||
"\n",
|
||||
"# Define the graph.\n",
|
||||
"super_graph = StateGraph(State)\n",
|
||||
"# First add the nodes, which will do the work\n",
|
||||
"super_graph.add_node(\"Research team\", get_last_message | research_chain | join_graph)\n",
|
||||
"super_graph.add_node(\n",
|
||||
" \"Paper writing team\", get_last_message | authoring_chain | join_graph\n",
|
||||
")\n",
|
||||
"super_graph.add_node(\"supervisor\", supervisor_node)\n",
|
||||
"\n",
|
||||
"# Define the graph connections, which controls how the logic\n",
|
||||
"# propagates through the program\n",
|
||||
"super_graph.add_edge(\"Research team\", \"supervisor\")\n",
|
||||
"super_graph.add_edge(\"Paper writing team\", \"supervisor\")\n",
|
||||
"super_graph.add_conditional_edges(\n",
|
||||
" \"supervisor\",\n",
|
||||
" lambda x: x[\"next\"],\n",
|
||||
" {\n",
|
||||
" \"Paper writing team\": \"Paper writing team\",\n",
|
||||
" \"Research team\": \"Research team\",\n",
|
||||
" \"FINISH\": END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"super_graph.set_entry_point(\"supervisor\")\n",
|
||||
"super_graph = super_graph.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 47,
|
||||
"id": "6b8badbf-d728-44bd-a2a7-5b4e587c92fe",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"HumanMessage(content='The document titled \"North American Sturgeons: Overview\" has been successfully created and saved as `North_American_Sturgeons_Overview.txt`. This document includes key information on the distribution, habitat, and conservation status of various sturgeon species found in North America, as well as a simplified table summarizing these details.\\n\\nFor further reference or detailed information, you can access and read the document. If you require any additional information or updates to the document, please let me know.', name='Author Docs')"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'supervisor': {'next': 'Research team'}}\n",
|
||||
"---\n",
|
||||
"{'Research team': {'messages': [HumanMessage(content='# Research Report on North American Sturgeon\\n\\n## Overview\\nSturgeons are a group of fish that belong to the family Acipenseridae. There are 27 species of sturgeon found in rivers and seas throughout the world, with 9 species endemic to North America. These species are known for their longevity, late maturity, and distinctive physical characteristics, such as a heterocercal caudal fin. \\n\\n## North American Species\\nThe following are some of the sturgeon species found in North America:\\n\\n- **Atlantic Sturgeon (Acipenser oxyrinchus)**\\n - The largest of the three sturgeons found in New York State, capable of living 30 to 60 years, growing 6 to 14 feet in length, and weighing over 200 pounds.\\n- **Lake Sturgeon (Acipenser fulvescens)**\\n - It has fewer than a dozen large and stable populations in North America, with some expected to be evaluated for Endangered Species Act protection by 2024.\\n- **Pallid Sturgeon (Scaphirhynchus albus)**\\n- **White Sturgeon (Acipenser transmontanus)**\\n- **Green Sturgeon (Acipenser medirostris)**\\n\\n## Conservation Status\\nMany sturgeon species are now critically endangered due to overfishing, habitat loss, and pollution. Conservation efforts are aimed at restoring sturgeon populations to rivers and tributaries where they once spawned.\\n\\n## Chart: Sturgeon Species in North America\\n\\n| Species | Habitat | Size | Conservation Status |\\n|-------------------|--------------------------|---------------|----------------------|\\n| Atlantic Sturgeon | Eastern North America | 6-14 ft, 200+ lbs | Endangered |\\n| Lake Sturgeon | Great Lakes, Mississippi | Varies | Under Review (2024) |\\n| Pallid Sturgeon | Missouri River basin | Varies | Critically Endangered|\\n| White Sturgeon | Pacific Northwest | Varies | Not Listed |\\n| Green Sturgeon | West Coast | Varies | Threatened |\\n\\n## Conclusion\\nNorth American sturgeons are an integral part of the aquatic biodiversity and play a crucial role in the ecosystem. However, they face significant threats from human activities. Efforts to conserve and protect sturgeon species are vital for their survival and the health of their habitats.\\n\\n## References\\n- [American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)\\n- [Earthwave Society](https://www.earthwave.org/sturgeon)\\n- [New York Natural Heritage Program](https://guides.nynhp.org/atlantic-sturgeon/)\\n- [Center for Biological Diversity](https://biologicaldiversity.org/w/news/press-releases/lake-sturgeon-will-get-endangered-species-decision-in-2024-2021-09-15/)\\n- [Wikipedia](https://en.wikipedia.org/wiki/Sturgeon)\\n\\n*(Please note that the sizes mentioned are approximate and can vary. The conservation status is also subject to change as new assessments are made.)*', name='Search')]}}\n",
|
||||
"---\n",
|
||||
"{'supervisor': {'next': 'Paper writing team'}}\n",
|
||||
"---\n",
|
||||
"{'Paper writing team': {'messages': [HumanMessage(content='The research report on North American Sturgeon has been successfully written and saved under the file name \"NorthAmericanSturgeonResearchReport.txt\". This document offers a detailed examination of sturgeon species in North America, highlighting their conservation status and the importance of preserving these vital components of aquatic ecosystems. The report is now ready for review and any necessary revisions.', name='Doc Writer')]}}\n",
|
||||
"---\n",
|
||||
"{'supervisor': {'next': 'FINISH'}}\n",
|
||||
"---\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"results = super_graph.invoke(\n",
|
||||
" \"Write a brief research report on the North American sturgeon. Include a chart.\",\n",
|
||||
" {\"recursion_limit\": 150},\n",
|
||||
")\n",
|
||||
"results[\"messages\"][-1]"
|
||||
"for s in super_graph.stream(\n",
|
||||
" {\n",
|
||||
" \"messages\": [\n",
|
||||
" HumanMessage(content=\"Write a brief research report on the North American sturgeon. Include a chart.\")\n",
|
||||
" ],\n",
|
||||
" },\n",
|
||||
" {\"recursion_limit\": 150},\n",
|
||||
" ):\n",
|
||||
" if \"__end__\" not in s:\n",
|
||||
" print(s)\n",
|
||||
" print(\"---\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d6ffcc7f-7b78-4ca5-8e0a-7c0ac08300fc",
|
||||
"id": "8ea8bcec-c049-4f85-862d-9991ef07d793",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
@@ -680,7 +819,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
Reference in New Issue
Block a user