From a8758661bc4533fd4719860b0ed0687cdf402f04 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:47:20 -0700 Subject: [PATCH] acesss tool calls directly (#1495) --- examples/storm/storm.ipynb | 772 +++++++++++++++++++++++++++-- examples/tutorials/sql-agent.ipynb | 73 +-- libs/cli/examples/graphs/storm.py | 2 +- 3 files changed, 773 insertions(+), 74 deletions(-) diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb index 67e56dbf7..42ba05faa 100644 --- a/examples/storm/storm.ipynb +++ b/examples/storm/storm.ipynb @@ -48,7 +48,10 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python" + "%%capture --no-stderr\n", + "%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n", + "# We use one or the other search engine below\n", + "%pip install -U duckduckgo tavily-python" ] }, { @@ -57,7 +60,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" + "# Uncomment if you want to draw the pretty graph diagrams.\n", + "# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n", + "# ! brew install graphviz\n", + "# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" ] }, { @@ -66,7 +72,21 @@ "metadata": {}, "outputs": [], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if os.environ.get(var):\n", + " return\n", + " os.environ[var] = getpass.getpass(var + \":\")\n", + "\n", + "\n", + "# Set for tracing\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -84,7 +104,12 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" + "from langchain_openai import ChatOpenAI\n", + "\n", + "fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", + "# Uncomment for a Fireworks model\n", + "# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n", + "long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" ] }, { @@ -112,7 +137,64 @@ } ], "source": [ - "from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)" + "from typing import List, Optional\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n", + " ),\n", + " (\"user\", \"{topic}\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "class Subsection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " description: str = Field(..., title=\"Content of the subsection\")\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n", + "\n", + "\n", + "class Section(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " description: str = Field(..., title=\"Content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n", + " for subsection in self.subsections or []\n", + " )\n", + " return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n", + "\n", + "\n", + "class Outline(BaseModel):\n", + " page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n", + " sections: List[Section] = Field(\n", + " default_factory=list,\n", + " title=\"Titles and descriptions for each section of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n", + " return f\"# {self.page_title}\\n\\n{sections}\".strip()\n", + "\n", + "\n", + "generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -145,7 +227,11 @@ } ], "source": [ - "example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)" + "example_topic = \"Impact of million-plus token context window language models on RAG\"\n", + "\n", + "initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n", + "\n", + "print(initial_outline.as_str)" ] }, { @@ -165,7 +251,25 @@ "metadata": {}, "outputs": [], "source": [ - "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)" + "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n", + " \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n", + "\n", + "Please list the as many subjects and urls as you can.\n", + "\n", + "Topic of interest: {topic}\n", + "\"\"\"\n", + ")\n", + "\n", + "\n", + "class RelatedSubjects(BaseModel):\n", + " topics: List[str] = Field(\n", + " description=\"Comprehensive list of related subjects as background research.\",\n", + " )\n", + "\n", + "\n", + "expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n", + " RelatedSubjects\n", + ")" ] }, { @@ -185,7 +289,8 @@ } ], "source": [ - "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects" + "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n", + "related_subjects" ] }, { @@ -204,7 +309,49 @@ "metadata": {}, "outputs": [], "source": [ - "class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)" + "class Editor(BaseModel):\n", + " affiliation: str = Field(\n", + " description=\"Primary affiliation of the editor.\",\n", + " )\n", + " name: str = Field(\n", + " description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n", + " )\n", + " role: str = Field(\n", + " description=\"Role of the editor in the context of the topic.\",\n", + " )\n", + " description: str = Field(\n", + " description=\"Description of the editor's focus, concerns, and motives.\",\n", + " )\n", + "\n", + " @property\n", + " def persona(self) -> str:\n", + " return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n", + "\n", + "\n", + "class Perspectives(BaseModel):\n", + " editors: List[Editor] = Field(\n", + " description=\"Comprehensive list of editors with their roles and affiliations.\",\n", + " # Add a pydantic validation/restriction to be at most M editors\n", + " )\n", + "\n", + "\n", + "gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n", + " You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n", + "\n", + " Wiki page outlines of related topics for inspiration:\n", + " {examples}\"\"\",\n", + " ),\n", + " (\"user\", \"Topic of interest: {topic}\"),\n", + " ]\n", + ")\n", + "\n", + "gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Perspectives)" ] }, { @@ -213,7 +360,37 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" + "from langchain_community.retrievers import WikipediaRetriever\n", + "from langchain_core.runnables import RunnableLambda\n", + "from langchain_core.runnables import chain as as_runnable\n", + "\n", + "wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n", + "\n", + "\n", + "def format_doc(doc, max_length=1000):\n", + " related = \"- \".join(doc.metadata[\"categories\"])\n", + " return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n", + " :max_length\n", + " ]\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n", + "\n", + "\n", + "@as_runnable\n", + "async def survey_subjects(topic: str):\n", + " related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n", + " retrieved_docs = await wikipedia_retriever.abatch(\n", + " related_subjects.topics, return_exceptions=True\n", + " )\n", + " all_docs = []\n", + " for docs in retrieved_docs:\n", + " if isinstance(docs, BaseException):\n", + " continue\n", + " all_docs.extend(docs)\n", + " formatted = format_docs(all_docs)\n", + " return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" ] }, { @@ -280,7 +457,40 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]" + "from typing import Annotated\n", + "\n", + "from langchain_core.messages import AnyMessage\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "\n", + "def add_messages(left, right):\n", + " if not isinstance(left, list):\n", + " left = [left]\n", + " if not isinstance(right, list):\n", + " right = [right]\n", + " return left + right\n", + "\n", + "\n", + "def update_references(references, new_references):\n", + " if not references:\n", + " references = {}\n", + " references.update(new_references)\n", + " return references\n", + "\n", + "\n", + "def update_editor(editor, new_editor):\n", + " # Can only set at the outset\n", + " if not editor:\n", + " return new_editor\n", + " return editor\n", + "\n", + "\n", + "class InterviewState(TypedDict):\n", + " messages: Annotated[List[AnyMessage], add_messages]\n", + " references: Annotated[Optional[dict], update_references]\n", + " editor: Annotated[Optional[Editor], update_editor]" ] }, { @@ -298,7 +508,56 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}" + "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", + "from langchain_core.prompts import MessagesPlaceholder\n", + "\n", + "gen_qn_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n", + "Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n", + "Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n", + "\n", + "When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n", + "Please only ask one question at a time and don't ask what you have asked before.\\\n", + "Your questions should be related to the topic you want to write.\n", + "Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n", + "\n", + "Stay true to your specific perspective:\n", + "\n", + "{persona}\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "\n", + "def tag_with_name(ai_message: AIMessage, name: str):\n", + " ai_message.name = name\n", + " return ai_message\n", + "\n", + "\n", + "def swap_roles(state: InterviewState, name: str):\n", + " converted = []\n", + " for message in state[\"messages\"]:\n", + " if isinstance(message, AIMessage) and message.name != name:\n", + " message = HumanMessage(**message.dict(exclude={\"type\"}))\n", + " converted.append(message)\n", + " return {\"messages\": converted}\n", + "\n", + "\n", + "@as_runnable\n", + "async def generate_question(state: InterviewState):\n", + " editor = state[\"editor\"]\n", + " gn_chain = (\n", + " RunnableLambda(swap_roles).bind(name=editor.name)\n", + " | gen_qn_prompt.partial(persona=editor.persona)\n", + " | fast_llm\n", + " | RunnableLambda(tag_with_name).bind(name=editor.name)\n", + " )\n", + " result = await gn_chain.ainvoke(state)\n", + " return {\"messages\": [result]}" ] }, { @@ -318,7 +577,17 @@ } ], "source": [ - "messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content" + "messages = [\n", + " HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n", + "]\n", + "question = await generate_question.ainvoke(\n", + " {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": messages,\n", + " }\n", + ")\n", + "\n", + "question[\"messages\"][0].content" ] }, { @@ -336,7 +605,24 @@ "metadata": {}, "outputs": [], "source": [ - "class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)" + "class Queries(BaseModel):\n", + " queries: List[str] = Field(\n", + " description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n", + " )\n", + "\n", + "\n", + "gen_queries_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Queries, include_raw=True)" ] }, { @@ -357,7 +643,10 @@ } ], "source": [ - "queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries" + "queries = await gen_queries_chain.ainvoke(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "queries[\"parsed\"].queries" ] }, { @@ -366,7 +655,38 @@ "metadata": {}, "outputs": [], "source": [ - "class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")" + "class AnswerWithCitations(BaseModel):\n", + " answer: str = Field(\n", + " description=\"Comprehensive answer to the user's question with citations.\",\n", + " )\n", + " cited_urls: List[str] = Field(\n", + " description=\"List of urls cited in the answer.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n", + " f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n", + " )\n", + "\n", + "\n", + "gen_answer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n", + " to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n", + "\n", + "Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n", + "Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n", + " AnswerWithCitations, include_raw=True\n", + ").with_config(run_name=\"GenerateAnswer\")" ] }, { @@ -375,7 +695,29 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" + "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n", + "from langchain_core.tools import tool\n", + "\n", + "'''\n", + "# Tavily is typically a better search engine, but your free queries are limited\n", + "search_engine = TavilySearchResults(max_results=4)\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = tavily_search.invoke(query)\n", + " return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n", + "'''\n", + "\n", + "# DDG\n", + "search_engine = DuckDuckGoSearchAPIWrapper()\n", + "\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n", + " return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" ] }, { @@ -384,7 +726,43 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}" + "import json\n", + "\n", + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "\n", + "async def gen_answer(\n", + " state: InterviewState,\n", + " config: Optional[RunnableConfig] = None,\n", + " name: str = \"Subject_Matter_Expert\",\n", + " max_str_len: int = 15000,\n", + "):\n", + " swapped_state = swap_roles(state, name) # Convert all other AI messages\n", + " queries = await gen_queries_chain.ainvoke(swapped_state)\n", + " query_results = await search_engine.abatch(\n", + " queries[\"parsed\"].queries, config, return_exceptions=True\n", + " )\n", + " successful_results = [\n", + " res for res in query_results if not isinstance(res, Exception)\n", + " ]\n", + " all_query_results = {\n", + " res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n", + " }\n", + " # We could be more precise about handling max token length if we wanted to here\n", + " dumped = json.dumps(all_query_results)[:max_str_len]\n", + " ai_message: AIMessage = queries[\"raw\"]\n", + " tool_call = queries[\"raw\"].tool_calls[0]\n", + " tool_id = tool_call[\"id\"]\n", + " tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n", + " swapped_state[\"messages\"].extend([ai_message, tool_message])\n", + " # Only update the shared state with the final answer to avoid\n", + " # polluting the dialogue history with intermediate messages\n", + " generated = await gen_answer_chain.ainvoke(swapped_state)\n", + " cited_urls = set(generated[\"parsed\"].cited_urls)\n", + " # Save the retrieved information to a the shared state for future reference\n", + " cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n", + " formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n", + " return {\"messages\": [formatted_message], \"references\": cited_references}" ] }, { @@ -404,7 +782,10 @@ } ], "source": [ - "example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content" + "example_answer = await gen_answer(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "example_answer[\"messages\"][-1].content" ] }, { @@ -423,7 +804,31 @@ "metadata": {}, "outputs": [], "source": [ - "max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" + "max_num_turns = 5\n", + "\n", + "\n", + "def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n", + " messages = state[\"messages\"]\n", + " num_responses = len(\n", + " [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n", + " )\n", + " if num_responses >= max_num_turns:\n", + " return END\n", + " last_question = messages[-2]\n", + " if last_question.content.endswith(\"Thank you so much for your help!\"):\n", + " return END\n", + " return \"ask_question\"\n", + "\n", + "\n", + "builder = StateGraph(InterviewState)\n", + "\n", + "builder.add_node(\"ask_question\", generate_question)\n", + "builder.add_node(\"answer_question\", gen_answer)\n", + "builder.add_conditional_edges(\"answer_question\", route_messages)\n", + "builder.add_edge(\"ask_question\", \"answer_question\")\n", + "\n", + "builder.add_edge(START, \"ask_question\")\n", + "interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" ] }, { @@ -444,7 +849,11 @@ } ], "source": [ - "from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())" + "from IPython.display import Image\n", + "\n", + "# Feel free to comment out if you have\n", + "# not installed pygraphviz\n", + "Image(interview_graph.get_graph().draw_png())" ] }, { @@ -474,7 +883,23 @@ } ], "source": [ - "final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step" + "final_step = None\n", + "\n", + "initial_state = {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {example_topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + "}\n", + "async for step in interview_graph.astream(initial_state):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name][\"messages\"])[:300])\n", + " if END in step:\n", + " final_step = step" ] }, { @@ -501,7 +926,29 @@ "metadata": {}, "outputs": [], "source": [ - "refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)" + "refine_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n", + "You need to make sure that the outline is comprehensive and specific. \\\n", + "Topic you are writing about: {topic} \n", + "\n", + "Old outline:\n", + "\n", + "{old_outline}\"\"\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "# Using turbo preview since the context can get quite long\n", + "refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -510,7 +957,15 @@ "metadata": {}, "outputs": [], "source": [ - "refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)" + "refined_outline = refine_outline_chain.invoke(\n", + " {\n", + " \"topic\": example_topic,\n", + " \"old_outline\": initial_outline.as_str,\n", + " \"conversations\": \"\\n\\n\".join(\n", + " f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n", + " ),\n", + " }\n", + ")" ] }, { @@ -595,7 +1050,23 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)" + "from langchain_community.vectorstores import SKLearnVectorStore\n", + "from langchain_core.documents import Document\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n", + "reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in final_state[\"references\"].items()\n", + "]\n", + "# This really doesn't need to be a vectorstore for this size of data.\n", + "# It could just be a numpy matrix. Or you could store documents\n", + "# across requests if you want.\n", + "vectorstore = SKLearnVectorStore.from_documents(\n", + " reference_docs,\n", + " embedding=embeddings,\n", + ")\n", + "retriever = vectorstore.as_retriever(k=10)" ] }, { @@ -636,7 +1107,67 @@ "metadata": {}, "outputs": [], "source": [ - "class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)" + "class SubSection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " content: str = Field(\n", + " ...,\n", + " title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n", + "\n", + "\n", + "class WikiSection(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " content: str = Field(..., title=\"Full content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + " citations: List[str] = Field(default_factory=list)\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " subsection.as_str for subsection in self.subsections or []\n", + " )\n", + " citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n", + " return (\n", + " f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n", + " + f\"\\n\\n{citations}\".strip()\n", + " )\n", + "\n", + "\n", + "section_writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n", + " \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n", + " ),\n", + " (\"user\", \"Write the full WikiSection for the {section} section.\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "async def retrieve(inputs: dict):\n", + " docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n", + " formatted = \"\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )\n", + " return {\"docs\": formatted, **inputs}\n", + "\n", + "\n", + "section_writer = (\n", + " retrieve\n", + " | section_writer_prompt\n", + " | long_context_llm.with_structured_output(WikiSection)\n", + ")" ] }, { @@ -663,7 +1194,14 @@ } ], "source": [ - "section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)" + "section = await section_writer.ainvoke(\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": refined_outline.sections[1].section_title,\n", + " \"topic\": example_topic,\n", + " }\n", + ")\n", + "print(section.as_str)" ] }, { @@ -681,7 +1219,24 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()" + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n", + " \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n", + " \" avoiding duplicates in the footer. Include URLs in the footer.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "writer = writer_prompt | long_context_llm | StrOutputParser()" ] }, { @@ -774,7 +1329,8 @@ } ], "source": [ - "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")" + "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n", + " print(tok, end=\"\")" ] }, { @@ -801,7 +1357,14 @@ "metadata": {}, "outputs": [], "source": [ - "class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str" + "class ResearchState(TypedDict):\n", + " topic: str\n", + " outline: Outline\n", + " editors: List[Editor]\n", + " interview_results: List[InterviewState]\n", + " # The final sections output\n", + " sections: List[WikiSection]\n", + " article: str" ] }, { @@ -810,7 +1373,109 @@ "metadata": {}, "outputs": [], "source": [ - "import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }" + "import asyncio\n", + "\n", + "\n", + "async def initialize_research(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " coros = (\n", + " generate_outline_direct.ainvoke({\"topic\": topic}),\n", + " survey_subjects.ainvoke(topic),\n", + " )\n", + " results = await asyncio.gather(*coros)\n", + " return {\n", + " **state,\n", + " \"outline\": results[0],\n", + " \"editors\": results[1].editors,\n", + " }\n", + "\n", + "\n", + "async def conduct_interviews(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " initial_states = [\n", + " {\n", + " \"editor\": editor,\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + " }\n", + " for editor in state[\"editors\"]\n", + " ]\n", + " # We call in to the sub-graph here to parallelize the interviews\n", + " interview_results = await interview_graph.abatch(initial_states)\n", + "\n", + " return {\n", + " **state,\n", + " \"interview_results\": interview_results,\n", + " }\n", + "\n", + "\n", + "def format_conversation(interview_state):\n", + " messages = interview_state[\"messages\"]\n", + " convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n", + " return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n", + "\n", + "\n", + "async def refine_outline(state: ResearchState):\n", + " convos = \"\\n\\n\".join(\n", + " [\n", + " format_conversation(interview_state)\n", + " for interview_state in state[\"interview_results\"]\n", + " ]\n", + " )\n", + "\n", + " updated_outline = await refine_outline_chain.ainvoke(\n", + " {\n", + " \"topic\": state[\"topic\"],\n", + " \"old_outline\": state[\"outline\"].as_str,\n", + " \"conversations\": convos,\n", + " }\n", + " )\n", + " return {**state, \"outline\": updated_outline}\n", + "\n", + "\n", + "async def index_references(state: ResearchState):\n", + " all_docs = []\n", + " for interview_state in state[\"interview_results\"]:\n", + " reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in interview_state[\"references\"].items()\n", + " ]\n", + " all_docs.extend(reference_docs)\n", + " await vectorstore.aadd_documents(all_docs)\n", + " return state\n", + "\n", + "\n", + "async def write_sections(state: ResearchState):\n", + " outline = state[\"outline\"]\n", + " sections = await section_writer.abatch(\n", + " [\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": section.section_title,\n", + " \"topic\": state[\"topic\"],\n", + " }\n", + " for section in outline.sections\n", + " ]\n", + " )\n", + " return {\n", + " **state,\n", + " \"sections\": sections,\n", + " }\n", + "\n", + "\n", + "async def write_article(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " sections = state[\"sections\"]\n", + " draft = \"\\n\\n\".join([section.as_str for section in sections])\n", + " article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n", + " return {\n", + " **state,\n", + " \"article\": article,\n", + " }" ] }, { @@ -826,7 +1491,27 @@ "metadata": {}, "outputs": [], "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())" + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "builder_of_storm = StateGraph(ResearchState)\n", + "\n", + "nodes = [\n", + " (\"init_research\", initialize_research),\n", + " (\"conduct_interviews\", conduct_interviews),\n", + " (\"refine_outline\", refine_outline),\n", + " (\"index_references\", index_references),\n", + " (\"write_sections\", write_sections),\n", + " (\"write_article\", write_article),\n", + "]\n", + "for i in range(len(nodes)):\n", + " name, node = nodes[i]\n", + " builder_of_storm.add_node(name, node)\n", + " if i > 0:\n", + " builder_of_storm.add_edge(nodes[i - 1][0], name)\n", + "\n", + "builder_of_storm.add_edge(START, nodes[0][0])\n", + "builder_of_storm.add_edge(nodes[-1][0], END)\n", + "storm = builder_of_storm.compile(checkpointer=MemorySaver())" ] }, { @@ -877,7 +1562,16 @@ } ], "source": [ - "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])" + "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n", + "async for step in storm.astream(\n", + " {\n", + " \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n", + " },\n", + " config,\n", + "):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name])[:300])" ] }, { @@ -886,7 +1580,8 @@ "metadata": {}, "outputs": [], "source": [ - "checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]" + "checkpoint = storm.get_state(config)\n", + "article = checkpoint.values[\"article\"]" ] }, { @@ -967,7 +1662,10 @@ } ], "source": [ - "from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))" + "from IPython.display import Markdown\n", + "\n", + "# We will down-header the sections to create less confusion in this notebook\n", + "Markdown(article.replace(\"\\n#\", \"\\n##\"))" ] }, { @@ -975,9 +1673,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "" - ] + "source": [] } ], "metadata": { diff --git a/examples/tutorials/sql-agent.ipynb b/examples/tutorials/sql-agent.ipynb index fb1354ef4..3a7f077fb 100644 --- a/examples/tutorials/sql-agent.ipynb +++ b/examples/tutorials/sql-agent.ipynb @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 1, "id": "64b0bf1b14c2e902", "metadata": { "ExecuteTime": { @@ -170,7 +170,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "id": "1f1e1f4f86ed54", "metadata": { "ExecuteTime": { @@ -197,7 +197,7 @@ "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" ] }, - "execution_count": 20, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -228,7 +228,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 3, "id": "deae8460e4cf72b1", "metadata": { "ExecuteTime": { @@ -295,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 4, "id": "452d049a3d2a4406", "metadata": { "ExecuteTime": { @@ -360,7 +360,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 5, "id": "f7eb708ecb4c7cfc", "metadata": { "ExecuteTime": { @@ -416,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 6, "id": "293017e8f05ac2b3", "metadata": { "ExecuteTime": { @@ -432,10 +432,10 @@ { "data": { "text/plain": [ - "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5zdRt3uWwY23FSYmKZT7crGF', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 222, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_319be4768e', 'finish_reason': 'stop', 'logprobs': None}, id='run-a062c91e-084e-4a91-bba8-fdbb957e2a5c-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_5zdRt3uWwY23FSYmKZT7crGF'}], usage_metadata={'input_tokens': 222, 'output_tokens': 20, 'total_tokens': 242})" + "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 221, 'total_tokens': 241}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-dd7873ef-d2f7-4769-a5c0-e6776ec2c515-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 221, 'output_tokens': 20, 'total_tokens': 241})" ] }, - "execution_count": 24, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -485,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 7, "id": "90d04ceea7b6b010", "metadata": { "ExecuteTime": { @@ -675,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 14, "id": "4f200d1813897000", "metadata": { "ExecuteTime": { @@ -690,7 +690,7 @@ "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -727,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 15, "id": "85958809-03c5-4e52-97cc-e7c0ae986f60", "metadata": {}, "outputs": [ @@ -737,29 +737,42 @@ "'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'" ] }, - "execution_count": 53, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import json\n", - "\n", "messages = app.invoke(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", ")\n", - "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - "]\n", - "json.loads(json_str)[\"final_answer\"]" + "json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"][\"final_answer\"]\n", + "json_str" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "3bf7709f-500c-4f28-bb85-dda317286c63", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'first_tool_call': {'messages': [AIMessage(content='', tool_calls=[{'name': 'sql_db_list_tables', 'args': {}, 'id': 'tool_abcd123', 'type': 'tool_call'}])]}}\n", + "{'list_tables_tool': {'messages': [ToolMessage(content='Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track', name='sql_db_list_tables', tool_call_id='tool_abcd123')]}}\n", + "{'model_get_schema': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'function': {'arguments': '{\"table_names\":\"Employee, Invoice\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 177, 'total_tokens': 195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c91a5aad-fc05-4881-87f9-0662d703c3c8-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Employee, Invoice'}, 'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'type': 'tool_call'}], usage_metadata={'input_tokens': 177, 'output_tokens': 18, 'total_tokens': 195})]}}\n", + "{'get_schema_tool': {'messages': [ToolMessage(content='\\nCREATE TABLE \"Employee\" (\\n\\t\"EmployeeId\" INTEGER NOT NULL, \\n\\t\"LastName\" NVARCHAR(20) NOT NULL, \\n\\t\"FirstName\" NVARCHAR(20) NOT NULL, \\n\\t\"Title\" NVARCHAR(30), \\n\\t\"ReportsTo\" INTEGER, \\n\\t\"BirthDate\" DATETIME, \\n\\t\"HireDate\" DATETIME, \\n\\t\"Address\" NVARCHAR(70), \\n\\t\"City\" NVARCHAR(40), \\n\\t\"State\" NVARCHAR(40), \\n\\t\"Country\" NVARCHAR(40), \\n\\t\"PostalCode\" NVARCHAR(10), \\n\\t\"Phone\" NVARCHAR(24), \\n\\t\"Fax\" NVARCHAR(24), \\n\\t\"Email\" NVARCHAR(60), \\n\\tPRIMARY KEY (\"EmployeeId\"), \\n\\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\\n)\\n\\n/*\\n3 rows from Employee table:\\nEmployeeId\\tLastName\\tFirstName\\tTitle\\tReportsTo\\tBirthDate\\tHireDate\\tAddress\\tCity\\tState\\tCountry\\tPostalCode\\tPhone\\tFax\\tEmail\\n1\\tAdams\\tAndrew\\tGeneral Manager\\tNone\\t1962-02-18 00:00:00\\t2002-08-14 00:00:00\\t11120 Jasper Ave NW\\tEdmonton\\tAB\\tCanada\\tT5K 2N1\\t+1 (780) 428-9482\\t+1 (780) 428-3457\\tandrew@chinookcorp.com\\n2\\tEdwards\\tNancy\\tSales Manager\\t1\\t1958-12-08 00:00:00\\t2002-05-01 00:00:00\\t825 8 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 2T3\\t+1 (403) 262-3443\\t+1 (403) 262-3322\\tnancy@chinookcorp.com\\n3\\tPeacock\\tJane\\tSales Support Agent\\t2\\t1973-08-29 00:00:00\\t2002-04-01 00:00:00\\t1111 6 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 5M5\\t+1 (403) 262-3443\\t+1 (403) 262-6712\\tjane@chinookcorp.com\\n*/\\n\\n\\nCREATE TABLE \"Invoice\" (\\n\\t\"InvoiceId\" INTEGER NOT NULL, \\n\\t\"CustomerId\" INTEGER NOT NULL, \\n\\t\"InvoiceDate\" DATETIME NOT NULL, \\n\\t\"BillingAddress\" NVARCHAR(70), \\n\\t\"BillingCity\" NVARCHAR(40), \\n\\t\"BillingState\" NVARCHAR(40), \\n\\t\"BillingCountry\" NVARCHAR(40), \\n\\t\"BillingPostalCode\" NVARCHAR(10), \\n\\t\"Total\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\"InvoiceId\"), \\n\\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\\n)\\n\\n/*\\n3 rows from Invoice table:\\nInvoiceId\\tCustomerId\\tInvoiceDate\\tBillingAddress\\tBillingCity\\tBillingState\\tBillingCountry\\tBillingPostalCode\\tTotal\\n1\\t2\\t2009-01-01 00:00:00\\tTheodor-Heuss-Straße 34\\tStuttgart\\tNone\\tGermany\\t70174\\t1.98\\n2\\t4\\t2009-01-02 00:00:00\\tUllevålsveien 14\\tOslo\\tNone\\tNorway\\t0171\\t3.96\\n3\\t8\\t2009-01-03 00:00:00\\tGrétrystraat 63\\tBrussels\\tNone\\tBelgium\\t1000\\t5.94\\n*/', name='sql_db_schema', tool_call_id='call_z1tyC7cEAawi5oIQn731Uknp')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'function': {'arguments': '{\"table_names\":\"Customer\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 1179, 'total_tokens': 1195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-19e02169-5e1e-40d9-90a2-384336ca5069-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Customer'}, 'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1179, 'output_tokens': 16, 'total_tokens': 1195}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='de5d25f5-b891-4e47-8282-d04dc9b93e9e', tool_call_id='call_ErWLktUfxKsHGNGr74m72yYD')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'function': {'arguments': '{\"table_names\":[\"Customer\"]}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 1245, 'total_tokens': 1262}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2c5f800f-43dc-4224-847b-49b5079efd2a-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': ['Customer']}, 'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1245, 'output_tokens': 17, 'total_tokens': 1262}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='6c962a35-fc24-4f27-86f0-6ec05256d478', tool_call_id='call_TFaA52SbhgEqm3ElEAd4HCsn')]}}\n", + "{'query_gen': {'messages': [AIMessage(content=\"To determine which sales agent made the most in sales in 2009, we need to join the `Invoice`, `Customer`, and `Employee` tables. Here is the query to find the top sales agent:\\n\\n```sql\\nSELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\\n```\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 125, 'prompt_tokens': 1312, 'total_tokens': 1437}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3aa7262c27', 'finish_reason': 'stop', 'logprobs': None}, id='run-6cacd10d-d3aa-49ae-b9d7-8cc209fc4ccc-0', usage_metadata={'input_tokens': 1312, 'output_tokens': 125, 'total_tokens': 1437})]}}\n", + "{'correct_query': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'function': {'arguments': '{\"query\":\"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\\\nFROM Invoice i\\\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\\\nWHERE strftime(\\'%Y\\', i.InvoiceDate) = \\'2009\\'\\\\nGROUP BY e.EmployeeId\\\\nORDER BY TotalSales DESC\\\\nLIMIT 1;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 90, 'prompt_tokens': 337, 'total_tokens': 427}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-71067e75-80f6-4356-8239-518e466b3526-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': \"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\"}, 'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'type': 'tool_call'}], usage_metadata={'input_tokens': 337, 'output_tokens': 90, 'total_tokens': 427})]}}\n", + "{'execute_query': {'messages': [ToolMessage(content=\"[('Steve', 'Johnson', 164.34)]\", name='db_query_tool', tool_call_id='call_FwCE2c7WORU7lKHdSWqMv0ON')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'function': {'arguments': '{\"final_answer\":\"The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.\"}', 'name': 'SubmitFinalAnswer'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 41, 'prompt_tokens': 1553, 'total_tokens': 1594}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_cb7cc8e106', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2ec7bf3a-2a16-47bd-aa9c-b7d6dc531c1b-0', tool_calls=[{'name': 'SubmitFinalAnswer', 'args': {'final_answer': 'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'}, 'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1553, 'output_tokens': 41, 'total_tokens': 1594})]}}\n" + ] + } + ], "source": [ "for event in app.stream(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", @@ -806,10 +819,8 @@ " \"\"\"Use this for answer evaluation\"\"\"\n", " msg = {\"messages\": (\"user\", example[\"input\"])}\n", " messages = app.invoke(msg)\n", - " json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - " ]\n", - " response = json.loads(json_str)[\"final_answer\"]\n", + " json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"]\n", + " response = json_str[\"final_answer\"]\n", " return {\"response\": response}" ] }, @@ -1053,14 +1064,6 @@ "\n", "We will explore ways to resolve this using LangGraph in future cookbooks!" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0681b6e0-196e-440c-ab16-1a530411719e", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1079,7 +1082,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/libs/cli/examples/graphs/storm.py b/libs/cli/examples/graphs/storm.py index e5ef2346e..a1317bc51 100644 --- a/libs/cli/examples/graphs/storm.py +++ b/libs/cli/examples/graphs/storm.py @@ -339,7 +339,7 @@ async def gen_answer( # We could be more precise about handling max token length if we wanted to here dumped = json.dumps(all_query_results)[:max_str_len] ai_message: AIMessage = queries["raw"] - tool_call = queries["raw"].additional_kwargs["tool_calls"][0] + tool_call = queries["raw"].tool_calls[0] tool_id = tool_call["id"] tool_message = ToolMessage(tool_call_id=tool_id, content=dumped) swapped_state["messages"].extend([ai_message, tool_message])