From b64f34e85f7606a6192a15a1a895961d0443089b Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 6 Feb 2024 09:57:00 -0800 Subject: [PATCH 1/3] add chatbot example --- examples/chatbots/prompt-generator.ipynb | 297 +++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 examples/chatbots/prompt-generator.ipynb diff --git a/examples/chatbots/prompt-generator.ipynb b/examples/chatbots/prompt-generator.ipynb new file mode 100644 index 000000000..80ff8a061 --- /dev/null +++ b/examples/chatbots/prompt-generator.ipynb @@ -0,0 +1,297 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c0aa20b8-fe59-4c91-a06f-32002f142a41", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import MessageGraph\n", + "from langchain_core.messages import SystemMessage\n", + "\n", + "template = \"\"\"Your job is to get information from a user about what type of prompt template they want to create.\n", + "\n", + "You should get the following information from them:\n", + "\n", + "- What the objective of the prompt is\n", + "- What variables will be passed into the prompt template\n", + "- Any constraints for what the output should NOT do\n", + "- Any requirements that the output MUST adhere to\n", + "\n", + "If you are not able to discerne this info, ask them to clarify! Do not attempt to wildly guess.\n", + "\n", + "After you are able to discerne all the information, call the relevant tool\"\"\"\n", + "def get_messages_info(messages):\n", + " return [SystemMessage(content=template)] + messages\n", + "\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(temperature=0)\n", + "\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from typing import List\n", + "\n", + "\n", + "class PromptInstructions(BaseModel):\n", + " \"\"\"Instructions on how to prompt the LLM.\"\"\"\n", + " objective: str\n", + " variables: List[str]\n", + " constraints: List[str]\n", + " requirements: List[str]\n", + "\n", + "llm_with_tool = llm.bind_tools([PromptInstructions])\n", + "\n", + "chain = get_messages_info | llm_with_tool\n", + "\n", + "def _is_tool_call(msg):\n", + " return hasattr(msg, \"additional_kwargs\") and 'tool_calls' in msg.additional_kwargs\n", + "\n", + "prompt_system = \"\"\"Based on the following requirements, write a good prompt template:\n", + "\n", + "{reqs}\"\"\"\n", + "def get_prompt_messages(messages):\n", + " tool_call = None\n", + " other_msgs = []\n", + " for m in messages:\n", + " if _is_tool_call(m):\n", + " tool_call = m.additional_kwargs['tool_calls'][0]['function']['arguments']\n", + " elif tool_call is not None:\n", + " other_msgs.append(m)\n", + " return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs\n", + " \n", + "\n", + "prompt_gen_chain = get_prompt_messages | llm\n", + "\n", + "def get_state(messages):\n", + " if _is_tool_call(messages[-1]):\n", + " return \"prompt\"\n", + " elif not isinstance(messages[-1], HumanMessage):\n", + " return END\n", + " for m in messages:\n", + " if _is_tool_call(m):\n", + " return \"prompt\"\n", + " return \"info\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "59d9d6b4-dce4-43cc-9a1a-61a7912ed5b8", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import MessageGraph, END\n", + "from langgraph.checkpoint.sqlite import SqliteSaver\n", + "\n", + "memory = SqliteSaver.from_conn_string(\":memory:\")\n", + "\n", + "nodes = {k:k for k in ['info', 'prompt', END]}\n", + "workflow = MessageGraph()\n", + "workflow.add_node(\"info\", chain)\n", + "workflow.add_node(\"prompt\", prompt_gen_chain)\n", + "workflow.add_conditional_edges(\"info\", get_state, nodes)\n", + "workflow.add_conditional_edges(\"prompt\", get_state, nodes)\n", + "workflow.set_entry_point(\"info\")\n", + "graph = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25793988-45a2-4e65-b33c-64e72aadb10e", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): hi!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Hello! How can I assist you today?'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): build me a prompt for extraction\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Sure! I can help you with that. Could you please provide me with more details about the prompt you want to create? Specifically, I need to know the objective of the prompt, the variables that will be passed into the prompt template, any constraints for what the output should not do, and any requirements that the output must adhere to.'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): i want to do extraction over a page\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Great! Could you please provide me with more details about the objective of the extraction? What specific information are you looking to extract from the page?'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): i want the user to specify that at run time\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content=\"Understood. So the objective of the prompt is to allow the user to specify the information they want to extract from a page at runtime. \\n\\nNow, let's move on to the variables. Are there any specific variables that you would like to pass into the prompt template? For example, the URL of the page or any other parameters that might be relevant for the extraction process.\"\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): the schema to extract, and the text to extract it from\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Got it. So the variables that will be passed into the prompt template are the schema to extract and the text to extract it from.\\n\\nNext, are there any constraints for what the output should not do? For example, should the output not include any sensitive information or should it not exceed a certain length?'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): it must be in json\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Understood. So a requirement for the output is that it must be in JSON format.\\n\\nLastly, are there any specific requirements that the output must adhere to? For example, should the output follow a specific structure or include certain fields?'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): must be json, must include the same fields as the schema specified\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'info':\n", + "---\n", + "content='Got it. So the requirements for the output are that it must be in JSON format and it must include the same fields as the schema specified.\\n\\nBased on the information you provided, I will now generate the prompt template for extraction. Please give me a moment.\\n\\n' additional_kwargs={'tool_calls': [{'id': 'call_6roy9dQoIrQZsHffR9kjAr0e', 'function': {'arguments': '{\\n \"objective\": \"Extract specific information from a page\",\\n \"variables\": [\"schema\", \"text\"],\\n \"constraints\": [\"Output should not include sensitive information\", \"Output should not exceed a certain length\"],\\n \"requirements\": [\"Output must be in JSON format\", \"Output must include the same fields as the specified schema\"]\\n}', 'name': 'PromptInstructions'}, 'type': 'function'}]}\n", + "\n", + "---\n", + "\n", + "Output from node 'prompt':\n", + "---\n", + "content='Extract specific information from a page and output the result in JSON format. The input page should contain the following fields: {{schema}}. The extracted information should be stored in the variable {{text}}. Ensure that the output does not include any sensitive information and does not exceed a certain length. Additionally, the output should include the same fields as the specified schema.'\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "import uuid\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", + "while True:\n", + " user = input('User (q/Q to quit): ')\n", + " if user in {'q', 'Q'}:\n", + " print('AI: Byebye')\n", + " break\n", + " for output in graph.stream([HumanMessage(content=user)], config=config):\n", + " if \"__end__\" in output:\n", + " continue\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")\n", + " history= output['__end__']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a276d20e-8a1b-4add-bf8d-83a8c803431d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b0b792f114347acc93e2cdf87c3bb721765cd43d Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 6 Feb 2024 11:30:31 -0800 Subject: [PATCH 2/3] cr --- examples/chatbots/imgs/prompt-generator.png | Bin 0 -> 25307 bytes examples/chatbots/prompt-generator.ipynb | 195 +++++++++++++++++--- 2 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 examples/chatbots/imgs/prompt-generator.png diff --git a/examples/chatbots/imgs/prompt-generator.png b/examples/chatbots/imgs/prompt-generator.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcc7083fc63e6ea99246553477a6a07e5d2a1f9 GIT binary patch literal 25307 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 V%<_n=C;l;n=0ja|r0Ua%{{bxW@1p Date: Wed, 7 Feb 2024 22:08:16 -0800 Subject: [PATCH 3/3] cr --- examples/chatbots/customer-support.ipynb | 829 ++++++++++++++++++ ...ynb => information-gather-prompting.ipynb} | 3 +- langgraph/graph/__init__.py | 4 +- langgraph/graph/graph.py | 8 + 4 files changed, 840 insertions(+), 4 deletions(-) create mode 100644 examples/chatbots/customer-support.ipynb rename examples/chatbots/{prompt-generator.ipynb => information-gather-prompting.ipynb} (99%) diff --git a/examples/chatbots/customer-support.ipynb b/examples/chatbots/customer-support.ipynb new file mode 100644 index 000000000..f1e7edaf3 --- /dev/null +++ b/examples/chatbots/customer-support.ipynb @@ -0,0 +1,829 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d9d1a28b-c2a1-4246-b1c2-c58d6938f798", + "metadata": {}, + "source": [ + "# Customer Support\n", + "\n", + "Here, we show an example of building a customer support chatbot.\n", + "\n", + "This customer support chatbot interacts with SQL database to answer questions.\n", + "We will use a mock SQL database to get started: the [Chinook](https://www.sqlitetutorial.net/sqlite-sample-database/) database.\n", + "This database is about sales from a music store: what songs and album exists, customer orders, things like that.\n", + "\n", + "This chatbot has two different states: \n", + "1. Music: the user can inquire about different songs and albums present in the store\n", + "2. Account: the user can ask questions about their account\n", + "\n", + "Under the hood, this is handled by two separate agents. \n", + "Each has a specific prompt and tools related to their objective. \n", + "There is also a generic agent who is responsible for routing between these two agents as needed." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "58c8fd46-843c-4bd6-a5f7-2a50676d1b53", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install -U scikit-learn" + ] + }, + { + "cell_type": "markdown", + "id": "9431e7f1-07fa-49d9-ac45-29613703dcc1", + "metadata": {}, + "source": [ + "## Load the data" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "61f7ef9c", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.utilities import SQLDatabase\n", + "\n", + "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "fd816e9f-fc94-476d-84eb-0e2a3c370c5e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The function `get_table_names` was deprecated in LangChain 0.0.1 and will be removed in 0.2.0. Use get_usable_table_name instead.\n", + " warn_deprecated(\n" + ] + }, + { + "data": { + "text/plain": [ + "['Album',\n", + " 'Artist',\n", + " 'Customer',\n", + " 'Employee',\n", + " 'Genre',\n", + " 'Invoice',\n", + " 'InvoiceLine',\n", + " 'MediaType',\n", + " 'Playlist',\n", + " 'PlaylistTrack',\n", + " 'Track']" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.get_table_names()" + ] + }, + { + "cell_type": "markdown", + "id": "1cf668e4-8cb4-4de1-bc5e-c90284bf74bc", + "metadata": {}, + "source": [ + "## Load an LLM\n", + "\n", + "We will load a language model to use.\n", + "For this demo we will use OpenAI." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d9ea4e80-30e6-4d46-b480-35f0be2fb055", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo-preview\")" + ] + }, + { + "cell_type": "markdown", + "id": "73907422-7e05-431e-b06d-256c9ec1f6f6", + "metadata": {}, + "source": [ + "## Load Other Modules\n", + "\n", + "Load other modules we will use.\n", + "\n", + "All of the tools our agents will use will be custom tools. As such, we will use the `@tool` decorator to create custom tools.\n", + "\n", + "We will pass in messages to the agent, so we load `HumanMessage` and `SystemMessage`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea958e9f-ab1f-49b5-bd85-16332055297c", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "from langchain_core.messages import HumanMessage, SystemMessage" + ] + }, + { + "cell_type": "markdown", + "id": "35271d4d-2a1c-41be-9359-a3a7c3fed3d9", + "metadata": {}, + "source": [ + "## Define the Customer Agent\n", + "\n", + "This agent is responsible for looking up customer information.\n", + "It will have a specific prompt as well a specific tool to look up information about that customer (after asking for their user id)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "975b039a", + "metadata": {}, + "outputs": [], + "source": [ + "# This tool is given to the agent to look up information about a customer\n", + "@tool\n", + "def get_customer_info(customer_id: int):\n", + " \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n", + " return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1d5fa446", + "metadata": {}, + "outputs": [], + "source": [ + "customer_prompt = \"\"\"Your job is to help a user update their profile.\n", + "\n", + "You only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n", + "\n", + "If you are unable to help the user, you can \"\"\"\n", + "\n", + "def get_customer_messages(messages):\n", + " return [SystemMessage(content=customer_prompt)] + messages\n", + "\n", + "customer_chain = get_customer_messages | model.bind_tools([get_customer_info])" + ] + }, + { + "cell_type": "markdown", + "id": "904a9485-3857-458e-8b9d-33bc33842bc9", + "metadata": {}, + "source": [ + "## Define the Music Agent\n", + "\n", + "This agent is responsible for figuring out information about music. To do that, we will create a prompt and various tools for looking up information about music\n", + "\n", + "First, we will create indexes for looking up artists and track names.\n", + "This will allow us to look up artists and tracks without having to spell their names exactly right." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a8604a3b-b484-4b2b-a914-4236cb98c524", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.vectorstores import SKLearnVectorStore\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "artists = db._execute(\"select * from Artist\")\n", + "songs = db._execute(\"select * from Track\")\n", + "artist_retriever = SKLearnVectorStore.from_texts(\n", + " [a['Name'] for a in artists],\n", + " OpenAIEmbeddings(), \n", + " metadatas=artists\n", + ").as_retriever()\n", + "song_retriever = SKLearnVectorStore.from_texts(\n", + " [a['Name'] for a in songs],\n", + " OpenAIEmbeddings(), \n", + " metadatas=songs\n", + ").as_retriever()" + ] + }, + { + "cell_type": "markdown", + "id": "ac7eb264-c572-4925-ad55-a1d52a18b1c0", + "metadata": {}, + "source": [ + "First, let's create a tool for getting albums by artist." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0a2a2b74", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def get_albums_by_artist(artist):\n", + " \"\"\"Get albums by an artist (or similar artists).\"\"\"\n", + " docs = artist_retriever.get_relevant_documents(artist)\n", + " artist_ids = \", \".join([str(d.metadata['ArtistId']) for d in docs])\n", + " return db.run(f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\", include_columns=True)" + ] + }, + { + "cell_type": "markdown", + "id": "45e85066-f2fc-490e-992d-cd66c9cd6486", + "metadata": {}, + "source": [ + "Next, lets create a tool for getting tracks by an artist" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "da533f50", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def get_tracks_by_artist(artist):\n", + " \"\"\"Get songs by an artist (or similar artists).\"\"\"\n", + " docs = artist_retriever.get_relevant_documents(artist)\n", + " artist_ids = \", \".join([str(d.metadata['ArtistId']) for d in docs])\n", + " return db.run(f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\", include_columns=True)" + ] + }, + { + "cell_type": "markdown", + "id": "bb0e50ab-b059-427c-924b-f8072d8db23c", + "metadata": {}, + "source": [ + "Finally, let's create a tool for looking up songs by their name." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b3c07010", + "metadata": {}, + "outputs": [], + "source": [ + "@tool\n", + "def check_for_songs(song_title):\n", + " \"\"\"Check if a song exists by its name.\"\"\"\n", + " return song_retriever.get_relevant_documents(song_title)" + ] + }, + { + "cell_type": "markdown", + "id": "88388ff8-38b5-4e4e-a24d-de8c3670bd2b", + "metadata": {}, + "source": [ + "Create the chain to call the relevant tools" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "72a14d5c", + "metadata": {}, + "outputs": [], + "source": [ + "song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n", + "\n", + "You only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n", + "\n", + "When looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\n", + "on simliar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n", + "def get_song_messages(messages):\n", + " return [SystemMessage(content=song_system_message)] + messages\n", + "\n", + "song_recc_chain = get_song_messages | model.bind_tools([get_albums_by_artist, get_tracks_by_artist, check_for_songs])" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cff15eb0-62c7-451d-a5f9-4576b24c879e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_85nJNoDaYBhgTLtl6opmSoSb', 'function': {'arguments': '{\"artist\":\"amy winehouse\"}', 'name': 'get_tracks_by_artist'}, 'type': 'function'}]})" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\n", + "song_recc_chain.invoke(msgs)" + ] + }, + { + "cell_type": "markdown", + "id": "0a42c293-0816-4f3c-b4a3-5b9f3a0665d1", + "metadata": {}, + "source": [ + "## Define the Generic Agent\n", + "\n", + "We now define a generic agent that is responsible for handling initial inquiries and routing to the right sub agent." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "73e74268", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import SystemMessage, HumanMessage, AIMessage\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "class Router(BaseModel):\n", + " \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n", + " choice: str = Field(description=\"should be one of: music, customer\")\n", + "\n", + "system_message = \"\"\"Your job is to help as a customer service representative for a music store.\n", + "\n", + "You should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n", + "\n", + "- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n", + "- Recomending music: if a customer wants to find some music or information about music. Call the router with `music`\n", + "\n", + "If the user is asking or wants to ask about updating or accessing their information, send them to that route.\n", + "If the user is asking or wants to ask about music, send them to that route.\n", + "Otherwise, respond.\"\"\"\n", + "def get_messages(messages):\n", + " return [SystemMessage(content=system_message)] + messages" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ddf27314", + "metadata": {}, + "outputs": [], + "source": [ + "chain = get_messages | model.bind_tools([Router])" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3c896f34", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_sRsolYD2ynqYbgGWqnNDfH4r', 'function': {'arguments': '{\"choice\":\"music\"}', 'name': 'Router'}, 'type': 'function'}]})" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\n", + "chain.invoke(msgs)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "40d86f59", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_b7iY3Kff2kV0VVQ6JgUrimd7', 'function': {'arguments': '{\"choice\":\"customer\"}', 'name': 'Router'}, 'type': 'function'}]})" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "msgs = [HumanMessage(content=\"hi! whats the email you have for me?\")]\n", + "chain.invoke(msgs)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "bd6ddd8b-7500-46a7-811d-3bcb937bda51", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import AIMessage\n", + "\n", + "def add_name(message, name):\n", + " _dict = message.dict()\n", + " _dict[\"name\"] = name\n", + " return AIMessage(**_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "27494de5-8345-4c23-bc0e-81e0dd5d47d8", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import END\n", + "import json\n", + "\n", + "def _get_last_ai_message(messages):\n", + " for m in messages[::-1]:\n", + " if isinstance(m, AIMessage):\n", + " return m\n", + " return None\n", + "\n", + "\n", + "def _is_tool_call(msg):\n", + " return hasattr(msg, \"additional_kwargs\") and 'tool_calls' in msg.additional_kwargs\n", + "\n", + "\n", + "def _route(messages):\n", + " last_message = messages[-1]\n", + " if isinstance(last_message, AIMessage):\n", + " if not _is_tool_call(last_message):\n", + " return END\n", + " else:\n", + " if last_message.name == \"general\":\n", + " tool_calls = last_message.additional_kwargs['tool_calls']\n", + " if len(tool_calls) > 1:\n", + " raise ValueError\n", + " tool_call = tool_calls[0]\n", + " return json.loads(tool_call['function']['arguments'])['choice']\n", + " else:\n", + " return \"tools\"\n", + " last_m = _get_last_ai_message(messages)\n", + " if last_m is None:\n", + " return \"general\"\n", + " if last_m.name == \"music\":\n", + " return \"music\"\n", + " elif last_m.name == \"customer\":\n", + " return \"customer\"\n", + " else:\n", + " return \"general\"" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "8aec704a-46fe-4fb3-bdee-11c3bbffc370", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "from langgraph.prebuilt import ToolExecutor, ToolInvocation\n", + "\n", + "tools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "4d5b75c6-73e0-4922-a765-a15be63f869e", + "metadata": {}, + "outputs": [], + "source": [ + "def _filter_out_routes(messages):\n", + " ms = []\n", + " for m in messages:\n", + " if _is_tool_call(m):\n", + " if m.name == \"general\":\n", + " continue\n", + " ms.append(m)\n", + " return ms" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "fd4dbf98-dbb3-411a-bad6-2bb334072aaf", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import partial\n", + "\n", + "general_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\n", + "music_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\n", + "customer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "9a1d7243", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import ToolMessage\n", + "\n", + "\n", + "async def call_tool(messages):\n", + " actions = []\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " for tool_call in last_message.additional_kwargs[\"tool_calls\"]:\n", + " function = tool_call[\"function\"]\n", + " function_name = function[\"name\"]\n", + " _tool_input = json.loads(function[\"arguments\"] or \"{}\")\n", + " # We construct an ToolInvocation from the function_call\n", + " actions.append(\n", + " ToolInvocation(\n", + " tool=function_name,\n", + " tool_input=_tool_input,\n", + " )\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " responses = await tool_executor.abatch(actions)\n", + " # We use the response to create a ToolMessage\n", + " tool_messages = [\n", + " ToolMessage(\n", + " tool_call_id=tool_call[\"id\"],\n", + " content=str(response),\n", + " additional_kwargs={\"name\": tool_call[\"function\"][\"name\"]},\n", + " )\n", + " for tool_call, response in zip(\n", + " last_message.additional_kwargs[\"tool_calls\"], responses\n", + " )\n", + " ]\n", + " return tool_messages" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "dcade924", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import MessageGraph\n", + "from langgraph.checkpoint.sqlite import SqliteSaver\n", + "\n", + "memory = SqliteSaver.from_conn_string(\":memory:\")\n", + "graph = MessageGraph()\n", + "nodes = {\"general\": \"general\", \"music\": \"music\", END: END, \"tools\": \"tools\", \"customer\": \"customer\"}\n", + "# Define a new graph\n", + "workflow = MessageGraph()\n", + "workflow.add_node(\"general\", general_node)\n", + "workflow.add_node(\"music\", music_node)\n", + "workflow.add_node(\"customer\", customer_node)\n", + "workflow.add_node(\"tools\", call_tool)\n", + "workflow.add_conditional_edges(\"general\", _route, nodes)\n", + "workflow.add_conditional_edges(\"tools\", _route, nodes)\n", + "workflow.add_conditional_edges(\"music\", _route, nodes)\n", + "workflow.add_conditional_edges(\"customer\", _route, nodes)\n", + "workflow.set_entry_route(_route, nodes)\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "ac65d6d2", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): hi!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'general':\n", + "---\n", + "content='Hello! How can I assist you today?' name='general'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): do you have any greenday songs?\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'general':\n", + "---\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_DEivnvJFmiaX8o55r5yhdQRM', 'function': {'arguments': '{\"choice\":\"music\"}', 'name': 'Router'}, 'type': 'function'}]} name='general'\n", + "\n", + "---\n", + "\n", + "Output from node 'music':\n", + "---\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_U4CNjjevWrB3XtELp2mYYeGN', 'function': {'arguments': '{\"artist\":\"Green Day\"}', 'name': 'get_tracks_by_artist'}, 'type': 'function'}]} name='music'\n", + "\n", + "---\n", + "\n", + "Output from node 'tools':\n", + "---\n", + "[ToolMessage(content='[{\\'SongName\\': \\'Maria\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Poprocks And Coke\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Longview\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Welcome To Paradise\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Basket Case\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'When I Come Around\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'She\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'J.A.R. (Jason Andrew Relva)\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Geek Stink Breath\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Brain Stew\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Jaded\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Walking Contradiction\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Stuck With Me\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \"Hitchin\\' A Ride\", \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Good Riddance (Time Of Your Life)\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Redundant\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Nice Guys Finish Last\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Minority\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Warning\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Waiting\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \"Macy\\'s Day Parade\", \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'American Idiot\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \"Jesus Of Suburbia / City Of The Damned / I Don\\'t Care / Dearly Beloved / Tales Of Another Broken Home\", \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Holiday\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Boulevard Of Broken Dreams\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Are We The Waiting\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'St. Jimmy\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Give Me Novacaine\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \"She\\'s A Rebel\", \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Extraordinary Girl\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Letterbomb\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Wake Me Up When September Ends\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \"Homecoming / The Death Of St. Jimmy / East 12th St. / Nobody Likes You / Rock And Roll Girlfriend / We\\'re Coming Home Again\", \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'Whatsername\\', \\'ArtistName\\': \\'Green Day\\'}, {\\'SongName\\': \\'In Your Honor\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'No Way Back\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Best Of You\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'DOA\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Hell\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'The Last Song\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Free Me\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Resolve\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'The Deepest Blues Are Black\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'End Over End\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Still\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'What If I Do?\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Miracle\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Another Round\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Friend Of A Friend\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Over And Out\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'On The Mend\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Virginia Moon\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Cold Day In The Sun\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Razor\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'All My Life\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Low\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Have It All\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Times Like These\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Disenchanted Lullaby\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Tired Of You\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Halo\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Lonely As You\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Overdrive\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Burn Away\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Come Back\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Doll\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Monkey Wrench\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Hey, Johnny Park!\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'My Poor Brain\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Wind Up\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Up In Arms\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'My Hero\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'See You\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Enough Space\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'February Stars\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Everlong\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Walking After You\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'New Way Home\\', \\'ArtistName\\': \\'Foo Fighters\\'}, {\\'SongName\\': \\'Speak To Me/Breathe\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'On The Run\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Time\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'The Great Gig In The Sky\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Money\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Us And Them\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Any Colour You Like\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Brain Damage\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Eclipse\\', \\'ArtistName\\': \\'Pink Floyd\\'}, {\\'SongName\\': \\'Lucky 13\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Aeroplane Flies High\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Because You Are\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Slow Dawn\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Believe\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'My Mistake\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Marquis In Spades\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \"Here\\'s To The Atom Bomb\", \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Sparrow\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Waiting\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Saturnine\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Rock On\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Set The Ray To Jerry\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Winterlong\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Soot & Stars\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Blissed & Gone\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Siva\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Rhinocerous\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Drown\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Cherub Rock\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Today\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Disarm\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Landslide\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Bullet With Butterfly Wings\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'1979\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Zero\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Tonight, Tonight\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Eye\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Ava Adore\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Perfect\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'The Everlasting Gaze\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Stand Inside Your Love\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'Real Love\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}, {\\'SongName\\': \\'[Untitled]\\', \\'ArtistName\\': \\'Smashing Pumpkins\\'}]', additional_kwargs={'name': 'get_tracks_by_artist'}, tool_call_id='call_U4CNjjevWrB3XtELp2mYYeGN')]\n", + "\n", + "---\n", + "\n", + "Output from node 'music':\n", + "---\n", + "content='Yes, we have a variety of Green Day songs available! Here are some of them:\\n\\n1. \"American Idiot\"\\n2. \"Basket Case\"\\n3. \"Boulevard of Broken Dreams\"\\n4. \"Good Riddance (Time of Your Life)\"\\n5. \"Holiday\"\\n6. \"Longview\"\\n7. \"Minority\"\\n8. \"Wake Me Up When September Ends\"\\n9. \"Welcome to Paradise\"\\n10. \"When I Come Around\"\\n\\nAnd many more! If you\\'re looking for a specific song or album by Green Day, feel free to ask!' name='music'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): q\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AI: Byebye\n" + ] + } + ], + "source": [ + "import uuid\n", + "from langchain_core.messages import HumanMessage\n", + "from langgraph.graph.graph import START\n", + "\n", + "history = []\n", + "while True:\n", + " user = input('User (q/Q to quit): ')\n", + " if user in {'q', 'Q'}:\n", + " print('AI: Byebye')\n", + " break\n", + " history.append(HumanMessage(content=user))\n", + " async for output in graph.astream(history):\n", + " if END in output or START in output:\n", + " continue\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")\n", + " history = output[END]" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "ccc495cd-9c2b-4b90-ba8b-88dd8f33966f", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): hi! whats the email you have on file?\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'general':\n", + "---\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_fA4APR8G3SIxHLN8Sfv2F6si', 'function': {'arguments': '{\"choice\":\"customer\"}', 'name': 'Router'}, 'type': 'function'}]} name='general'\n", + "\n", + "---\n", + "\n", + "Output from node 'customer':\n", + "---\n", + "content=\"To help you with that, I'll need your customer ID. Could you provide it, please?\" name='customer'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): 1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'customer':\n", + "---\n", + "content='' additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_pPUA6QaH2kQG9MRLjRn0PCDY', 'function': {'arguments': '{\"customer_id\":1}', 'name': 'get_customer_info'}, 'type': 'function'}]} name='customer'\n", + "\n", + "---\n", + "\n", + "Output from node 'tools':\n", + "---\n", + "[ToolMessage(content=\"[(1, 'Luís', 'Gonçalves', 'Embraer - Empresa Brasileira de Aeronáutica S.A.', 'Av. Brigadeiro Faria Lima, 2170', 'São José dos Campos', 'SP', 'Brazil', '12227-000', '+55 (12) 3923-5555', '+55 (12) 3923-5566', 'luisg@embraer.com.br', 3)]\", additional_kwargs={'name': 'get_customer_info'}, tool_call_id='call_pPUA6QaH2kQG9MRLjRn0PCDY')]\n", + "\n", + "---\n", + "\n", + "Output from node 'customer':\n", + "---\n", + "content='The email we have on file for you is luisg@embraer.com.br. Is there anything else I can assist you with?' name='customer'\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): q\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AI: Byebye\n" + ] + } + ], + "source": [ + "history = []\n", + "while True:\n", + " user = input('User (q/Q to quit): ')\n", + " if user in {'q', 'Q'}:\n", + " print('AI: Byebye')\n", + " break\n", + " history.append(HumanMessage(content=user))\n", + " async for output in graph.astream(history):\n", + " if END in output or START in output:\n", + " continue\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")\n", + " history = output[END]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chatbots/prompt-generator.ipynb b/examples/chatbots/information-gather-prompting.ipynb similarity index 99% rename from examples/chatbots/prompt-generator.ipynb rename to examples/chatbots/information-gather-prompting.ipynb index b89fa4247..ae21291ab 100644 --- a/examples/chatbots/prompt-generator.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -413,8 +413,7 @@ " print(f\"Output from node '{key}':\")\n", " print(\"---\")\n", " print(value)\n", - " print(\"\\n---\\n\")\n", - " history= output['__end__']" + " print(\"\\n---\\n\")" ] }, { diff --git a/langgraph/graph/__init__.py b/langgraph/graph/__init__.py index 8fac44cfa..d9276c4b1 100644 --- a/langgraph/graph/__init__.py +++ b/langgraph/graph/__init__.py @@ -1,5 +1,5 @@ -from langgraph.graph.graph import END, Graph +from langgraph.graph.graph import END, Graph, START from langgraph.graph.message import MessageGraph from langgraph.graph.state import StateGraph -__all__ = ["END", "Graph", "StateGraph", "MessageGraph"] +__all__ = ["END", "START", "Graph", "StateGraph", "MessageGraph"] diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index a0ac4a172..f10412f9c 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -13,6 +13,7 @@ from langgraph.checkpoint import BaseCheckpointSaver from langgraph.pregel import Channel, Pregel END = "__end__" +START = "START" class Branch(NamedTuple): @@ -34,6 +35,7 @@ class Graph: self.edges = set[tuple[str, str]]() self.branches: defaultdict[str, list[Branch]] = defaultdict(list) self.support_multiple_edges = False + self.entry_point = None def add_node(self, key: str, action: RunnableLike) -> None: if key in self.nodes: @@ -85,6 +87,12 @@ class Graph: raise ValueError(f"Need to add_node `{key}` first") self.entry_point = key + def set_entry_route(self, condition: Callable[..., str], + conditional_edge_mapping: Optional[Dict[str, str]] = None) -> None: + self.add_node(START, lambda x: None) + self.add_conditional_edges(START, condition, conditional_edge_mapping) + self.set_entry_point(START) + def set_finish_point(self, key: str) -> None: return self.add_edge(key, END)