From e441990f923c5cd42927465005ad74e115659d70 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sun, 7 Jan 2024 19:39:10 -0800 Subject: [PATCH 1/2] Rename --- LICENSE | 2 +- README.md | 10 +- examples/combine_docs.ipynb | 8 +- examples/draft-revise-loop.py | 2 +- examples/langgraph.ipynb | 783 +++++++++--------- examples/old/example.ipynb | 384 --------- examples/old/research/researcher.py | 97 --- .../research/single_question_researcher.py | 66 -- examples/old/research/webscraper.py | 38 - examples/old/web-research.ipynb | 498 ----------- examples/rag.py | 4 +- examples/readme.py | 2 +- examples/recursive-web-loader.py | 6 +- .../checkpoint => langgraph}/__init__.py | 0 langgraph/channels/__init__.py | 11 + {permchain => langgraph}/channels/base.py | 2 +- {permchain => langgraph}/channels/binop.py | 2 +- {permchain => langgraph}/channels/context.py | 2 +- .../channels/last_value.py | 2 +- {permchain => langgraph}/channels/topic.py | 2 +- langgraph/checkpoint/__init__.py | 8 + {permchain => langgraph}/checkpoint/base.py | 4 +- {permchain => langgraph}/checkpoint/memory.py | 4 +- {permchain => langgraph}/constants.py | 0 .../langgraph => langgraph/graph}/__init__.py | 2 +- {permchain => langgraph}/pregel/__init__.py | 26 +- {permchain => langgraph}/pregel/debug.py | 2 +- {permchain => langgraph}/pregel/io.py | 4 +- {permchain => langgraph}/pregel/log.py | 0 {permchain => langgraph}/pregel/read.py | 4 +- {permchain => langgraph}/pregel/reserved.py | 0 {permchain => langgraph}/pregel/validate.py | 8 +- {permchain => langgraph}/pregel/write.py | 2 +- {permchain => langgraph}/utils.py | 0 permchain/__init__.py | 12 - permchain/channels/__init__.py | 11 - pyproject.toml | 8 +- tests/test_channels.py | 10 +- tests/test_pregel.py | 21 +- tests/test_pregel_async.py | 21 +- 40 files changed, 490 insertions(+), 1578 deletions(-) delete mode 100644 examples/old/example.ipynb delete mode 100644 examples/old/research/researcher.py delete mode 100644 examples/old/research/single_question_researcher.py delete mode 100644 examples/old/research/webscraper.py delete mode 100644 examples/old/web-research.ipynb rename {permchain/checkpoint => langgraph}/__init__.py (100%) create mode 100644 langgraph/channels/__init__.py rename {permchain => langgraph}/channels/base.py (98%) rename {permchain => langgraph}/channels/binop.py (96%) rename {permchain => langgraph}/channels/context.py (98%) rename {permchain => langgraph}/channels/last_value.py (97%) rename {permchain => langgraph}/channels/topic.py (97%) create mode 100644 langgraph/checkpoint/__init__.py rename {permchain => langgraph}/checkpoint/base.py (95%) rename {permchain => langgraph}/checkpoint/memory.py (88%) rename {permchain => langgraph}/constants.py (100%) rename {permchain/langgraph => langgraph/graph}/__init__.py (98%) rename {permchain => langgraph}/pregel/__init__.py (97%) rename {permchain => langgraph}/pregel/debug.py (94%) rename {permchain => langgraph}/pregel/io.py (93%) rename {permchain => langgraph}/pregel/log.py (100%) rename {permchain => langgraph}/pregel/read.py (98%) rename {permchain => langgraph}/pregel/reserved.py (100%) rename {permchain => langgraph}/pregel/validate.py (89%) rename {permchain => langgraph}/pregel/write.py (97%) rename {permchain => langgraph}/utils.py (100%) delete mode 100644 permchain/__init__.py delete mode 100644 permchain/channels/__init__.py diff --git a/LICENSE b/LICENSE index 280546a23..f1a3c99dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -# PermChain License +# LangGraph License By using the software, you agree to all of the terms and conditions below. diff --git a/README.md b/README.md index e74f0b270..bd39acfa5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# `permchain` +# `langgraph` ## Get started -`pip install permchain` +`pip install langgraph` ## Overview -PermChain is an alpha-stage library for building stateful, multi-actor applications with LLMs. It extends the [LangChain Expression Language](https://python.langchain.com/docs/expression_language/) with the ability to coordinate multiple chains (or actors) across multiple steps of computation. It is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). +LangGraph is an alpha-stage library for building stateful, multi-actor applications with LLMs. It extends the [LangChain Expression Language](https://python.langchain.com/docs/expression_language/) with the ability to coordinate multiple chains (or actors) across multiple steps of computation. It is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). Some of the use cases are: @@ -20,7 +20,7 @@ Some of the use cases are: ### Channels -Channels are used to communicate between chains. Each channel has a value type, an update type, and an update function – which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. PermChain provides a number of built-in channels: +Channels are used to communicate between chains. Each channel has a value type, an update type, and an update function – which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels: #### Basic channels: LastValue and Topic @@ -49,7 +49,7 @@ Repeat until no chains are planned for execution, or a maximum number of steps i ## Example ```python -from permchain import Channel, Pregel +from langgraph import Channel, Pregel grow_value = ( Channel.subscribe_to("value") diff --git a/examples/combine_docs.ipynb b/examples/combine_docs.ipynb index 7ad00124e..d981c2444 100644 --- a/examples/combine_docs.ipynb +++ b/examples/combine_docs.ipynb @@ -7,7 +7,7 @@ "source": [ "# Combine Docs\n", "\n", - "PermChain is a great choice for implementating workflows that involve operating over longer documents because of its recursive nature" + "LangGraph is a great choice for implementating workflows that involve operating over longer documents because of its recursive nature" ] }, { @@ -20,13 +20,13 @@ "from langchain.chat_models.openai import ChatOpenAI\n", "from langchain.prompts import ChatPromptTemplate, PromptTemplate\n", "from langchain.schema.output_parser import StrOutputParser\n", - "from langchain.schema.runnable import Runnable, RunnablePassthrough\n", + "from langchain.schema.runnable import Runnable\n", "from langchain.schema.output_parser import StrOutputParser\n", "from langchain.schema.document import Document\n", "from langchain.schema import format_document\n", "\n", - "from permchain import Channel, Pregel\n", - "from permchain.channels import LastValue, Topic" + "from langgraph.pregel import Channel, Pregel\n", + "from langgraph.channels import Topic" ] }, { diff --git a/examples/draft-revise-loop.py b/examples/draft-revise-loop.py index eb90e2872..fae95a14e 100644 --- a/examples/draft-revise-loop.py +++ b/examples/draft-revise-loop.py @@ -5,7 +5,7 @@ from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import SystemMessagePromptTemplate -from permchain import Channel, Pregel +from langgraph.pregel import Channel, Pregel # prompts diff --git a/examples/langgraph.ipynb b/examples/langgraph.ipynb index 8b3dca25a..34fd6c3f4 100644 --- a/examples/langgraph.ipynb +++ b/examples/langgraph.ipynb @@ -1,399 +1,400 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "396e20d9-8684-40ea-a46a-e3dfa36ed5a6", - "metadata": {}, - "source": [ - "## Existing Agent Executor" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d642e6af-217a-4414-a78c-509b44155eca", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.chat_models import ChatOpenAI\n", - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.chat_models import ChatOpenAI\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.runnables import RunnablePassthrough\n", - "from permchain.langgraph import Graph, END\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", - "\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n", - "\n", - "from langchain_core.agents import AgentFinish\n", - "# Define decision-making logic\n", - "def should_continue(data):\n", - " # Logic to decide whether to continue in the loop or exit\n", - " if isinstance(data['agent_outcome'], AgentFinish):\n", - " return \"exit\"\n", - " else:\n", - " return \"continue\"\n", - " \n", - "def execute_tools(data):\n", - " agent_action = data.pop('agent_outcome')\n", - " observation = {t.name: t for t in tools}[agent_action.tool].invoke(agent_action.tool_input)\n", - " data['intermediate_steps'].append((agent_action, observation))\n", - " return data\n", - " \n", - " \n", - "\n", - "# Define agents\n", - "agent = RunnablePassthrough.assign(\n", - " agent_outcome = agent_runnable\n", - ")\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = Graph()\n", - "\n", - "workflow.add_node(\"agent\", agent)\n", - "workflow.add_node(\"tools\", execute_tools)\n", - "\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " {\n", - " \"continue\": \"tools\",\n", - " \"exit\": END\n", - " }\n", - ")\n", - "\n", - "workflow.add_edge('tools', 'agent')\n", - "\n", - "chain = workflow.compile()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c46bd262-9605-4449-9391-f6b6e0fe440e", - "metadata": {}, - "outputs": [ + "cells": [ { - "data": { - "text/plain": [ - "{'input': 'what is the weather in sf',\n", - " 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\":\"weather in San Francisco\"}'}})]),\n", - " [{'url': 'https://www.weather25.com/north-america/usa/california/san-francisco',\n", - " 'content': 'will give you an idea of weather trends in San Francisco. For example, the weather in San Francisco in January 2024. San Francisco 14 day weather The weather today in San Francisco San Francisco weather report The weather in San Francisco, United States San Francisco weather by months San Francisco weather the weather in San Francisco including humidity, wind, chance of rain and more on the San Francisco current weather01 January 02 February 03 March 04 April 05 May 06 June 07 July 08 August 09 September 10 October 11 November 12 December. ... For example, the weather in San Francisco in January 2024. These trends can be helpful when planning trips to San Francisco or preparing for the weather in advance. There are many factors to consider when looking at the ...'}])],\n", - " 'agent_outcome': AgentFinish(return_values={'output': 'For the current weather in San Francisco, you can visit the following website: [San Francisco Weather](https://www.weather25.com/north-america/usa/california/san-francisco). This will provide you with the latest weather updates including humidity, wind, chance of rain, and more.'}, log='For the current weather in San Francisco, you can visit the following website: [San Francisco Weather](https://www.weather25.com/north-america/usa/california/san-francisco). This will provide you with the latest weather updates including humidity, wind, chance of rain, and more.')}" + "cell_type": "markdown", + "id": "396e20d9-8684-40ea-a46a-e3dfa36ed5a6", + "metadata": {}, + "source": [ + "## Existing Agent Executor" ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chain.invoke({\"input\": \"what is the weather in sf\", \"intermediate_steps\": []})" - ] - }, - { - "cell_type": "markdown", - "id": "592c3886-71d1-4539-80dd-111e55cc3a85", - "metadata": {}, - "source": [ - "## Reflexion Agent" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "f6f96e81-4a20-4599-a625-8d18df6fa76d", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.agents import AgentExecutor, BaseMultiActionAgent, Tool\n", - "from langchain.schema import AgentAction, AgentFinish\n", - "from langchain_core.language_models.chat_models import BaseChatModel\n", - "from langchain.chains import LLMChain\n", - "\n", - "from langchain.globals import set_llm_cache\n", - "\n", - "from dotenv import load_dotenv\n", - "\n", - "from pydantic import BaseModel\n", - "\n", - "from langchain.chat_models import ChatOpenAI\n", - "from langchain.cache import SQLiteCache\n", - "\n", - "from langchain_core.output_parsers import BaseOutputParser\n", - "\n", - "from langchain.prompts.chat import ChatPromptTemplate\n", - "from langchain.callbacks import get_openai_callback\n", - "from langchain.tools.tavily_search import TavilySearchResults\n", - "from langchain.utilities.tavily_search import TavilySearchAPIWrapper\n", - "from langchain.pydantic_v1 import BaseModel\n", - "import os\n", - "\n", - "from langchain.agents import AgentType, initialize_agent, load_tools\n", - "\n", - "set_llm_cache(SQLiteCache(database_path=\".langchain.db\"))\n", - "\n", - "\n", - "llm = ChatOpenAI(\n", - " temperature=0.0,\n", - " max_tokens=2000,\n", - " max_retries=100,\n", - " model=\"gpt-4-1106-preview\",\n", - ")\n", - "\n", - "search = TavilySearchAPIWrapper()\n", - "tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\n", - "\n", - "NEXT_STEP_TEMPLATE = \"\"\"You are expert researcher trying answer a question ~250 words. You are asked to answer the following question: {question}\n", - "\n", - "The way you are going to answer the question is as follows:\n", - "\n", - "1. Revise your previous answer using the new information.\n", - " - You should use the previous critique to add important information to your answer.\n", - " _ You MUST include numerical citations in your revised answer to ensure it can be verified.\n", - " - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n", - " - [1] https://example.com\n", - " - [2] https://example.com\n", - " - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n", - "2. Reflect and critique your answer. Specifically, you should:\n", - " - Think about what is missing from your answer.\n", - " - Think about what is superfluous in your answer.\n", - " - Think about what search query you should use next to improve your answer.\n", - " Give your answer in exactly 2 parts. The first should address what is missing from your answer. The second should address what could be removed from your answer. Your should be VERY harsh as we really want to improve the answer.\n", - "3. Give the search query you came up with to improve your answer.\n", - "\n", - "Previous steps: \n", - "\n", - "{previous_steps}\n", - "\n", - "===\n", - "\n", - "Format your answer as follows:\n", - "\n", - "Revised answer: [give your revised answer based on the previous critique and new information from the search engine then the \"References\" section]\n", - "Critique: [give your harsh critique of your revised answer in 2 parts: what is missing and what is superfluous]\n", - "Search query: [give the new search query you came up with to enter into the search engine to improve your answer. If you have more than one, make sure they are comma separated and in quotes]\n", - "\n", - "SAY NOTHING else please.\"\"\"\n", - "\n", - "INITIAL_ANSWER_TEMPLATE = \"\"\"You are expert researcher trying answer a question ~250 words. You are asked to answer the following question: {question}\n", - "\n", - "The way you are going to answer the question is as follows:\n", - "\n", - "1. Give a detailed in ~250 words.\n", - "2. Reflect and critique your answer. Specifically, you should:\n", - " - Think about what is missing from your answer.\n", - " - Think about what is superfluous in your answer.\n", - " - Think about what search query you should use next to improve your answer.\n", - " Give your answer in exactly 2 parts. The first should address what is missing from your answer. The second should address what could be removed from your answer. Your should be VERY harsh as we really want to improve the answer.\n", - "3. Give the search query you came up with to improve your answer.\n", - "\n", - "===\n", - "\n", - "Format your answer as follows:\n", - "\n", - "Answer: [give your initial answer]\n", - "Critique: [give your harsh critique of your answer in 2 parts: what is missing and what is superfluous]\n", - "Search query: [give the search query you came up with to improve your answer. If you have more than one, make sure they are comma separated and in quotes]\n", - "\n", - "SAY NOTHING else please.\"\"\"\n", - "\n", - "\n", - "class ReflexionStep(BaseModel):\n", - " \"\"\"A single step in the reflexion process.\"\"\"\n", - "\n", - " answer: str\n", - " critique: str\n", - " search_query: str\n", - "\n", - " def __str__(self):\n", - " return f\"Answer: {self.answer}\\nCritique: {self.critique}\\nSearch query: {self.search_query}\"\n", - "\n", - "def _parse_reflexion_step(output: str) -> tuple[str, str, str]:\n", - " # find answer using .split()\n", - " if (\"Answer:\" not in output and \"Revised answer:\" not in output) or not \"Critique:\" in output or not \"Search query:\" in output:\n", - " raise ValueError(f\"The output is not formatted correctly. Output: {output}\")\n", - " if \"Answer:\" in output:\n", - " answer = output.split(\"Answer:\")[1].split(\"Critique:\")[0].strip()\n", - " else:\n", - " answer = output.split(\"Revised answer:\")[1].split(\"Critique:\")[0].strip()\n", - " critique = output.split(\"Critique:\")[1].split(\"Search query:\")[0].strip()\n", - " search_query = output.split(\"Search query:\")[1].strip()\n", - " return answer, critique, search_query\n", - "\n", - "class ReflexionStepParser(BaseOutputParser[ReflexionStep]):\n", - " \"\"\"Parser for the reflexion step.\"\"\"\n", - "\n", - " def parse(self, output: str) -> ReflexionStep:\n", - " \"\"\"Parse the output.\"\"\"\n", - " # try to find answer or initial answer\n", - " answer, critique, search_query = _parse_reflexion_step(output)\n", - " return ReflexionStep(\n", - " answer=answer, critique=critique, search_query=search_query\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7708fa95-547b-4bea-b126-3656de7d5873", - "metadata": {}, - "outputs": [], - "source": [ - "initial_chain = RunnablePassthrough.assign(\n", - " agent_outcome = ChatPromptTemplate.from_template(INITIAL_ANSWER_TEMPLATE) | llm | ReflexionStepParser() | (lambda x: AgentAction(\n", - " tool=\"tavily_search_results_json\",\n", - " tool_input=x.search_query,\n", - " log=str(x),\n", - " ))\n", - ")\n", - "\n", - "def prep_next(inputs):\n", - " intermediate_steps = inputs[\"intermediate_steps\"]\n", - " previous_steps = list[str]()\n", - "\n", - " for i, (action, observation) in enumerate(intermediate_steps, start=1):\n", - " last_step_str = f\"\"\"Step {i}:\n", - "\n", - "{action.log}\n", - "\n", - "Search output for \"{action.tool_input}\":\n", - "\n", - "{observation}\"\"\"\n", - " previous_steps.append(last_step_str)\n", - "\n", - " previous_steps_str = \"\\n\\n\".join(previous_steps)\n", - " inputs[\"previous_steps\"] = previous_steps_str\n", - " return inputs\n", - " \n", - "next_chain = RunnablePassthrough.assign(\n", - " agent_outcome = prep_next | ChatPromptTemplate.from_template(NEXT_STEP_TEMPLATE) | llm | ReflexionStepParser() | (lambda x: AgentAction(\n", - " tool=\"tavily_search_results_json\",\n", - " tool_input=x.search_query,\n", - " log=str(x),\n", - " ))\n", - ")\n", - "\n", - "def finish(inputs):\n", - " intermediate_steps = inputs[\"intermediate_steps\"]\n", - " last_action, _ = intermediate_steps[-1]\n", - " last_step_str = last_action.log\n", - " # extract answer\n", - " answer, _, _ = _parse_reflexion_step(last_step_str)\n", - "\n", - " first_action, _ = intermediate_steps[0]\n", - " first_step_str = first_action.log\n", - " # extract answer\n", - " initial_answer, _, _ = _parse_reflexion_step(first_step_str)\n", - "\n", - " return AgentFinish(\n", - " log=\"Reached max steps.\",\n", - " return_values={\"output\": answer, \"initial_answer\": initial_answer},\n", - " )\n", - "\n", - "\n", - "def execute_tools(data):\n", - " agent_action = data.pop('agent_outcome')\n", - " observation = {t.name: t for t in tools}[agent_action.tool].invoke(agent_action.tool_input)\n", - " data['intermediate_steps'].append((agent_action, observation))\n", - " return data\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "d6cdd1cd-e480-4dd7-99b4-9018eb243b4d", - "metadata": {}, - "outputs": [ + }, { - "data": { - "text/plain": [ - "AgentFinish(return_values={'output': \"The current weather in San Francisco can be accessed through various weather reporting services, which provide real-time temperature, humidity, wind, and chances of precipitation [1]. Historically, San Francisco experiences a mild, Mediterranean climate with average temperatures ranging from the low 50s to the mid-60s Fahrenheit. The city's unique topography creates microclimates, leading to significant weather variations across different neighborhoods. San Francisco's summers are notably cooler compared to other Californian cities, largely due to the cold California Current and persistent fog, especially in June and July. Winters are mild and the wettest months span from November to March, with an annual rainfall average of approximately 23 inches. Wind is a prominent feature, with spring being particularly windy. For historical weather extremes and average wind speeds, additional specific data can be sought from climatological records.\\n\\nReferences:\\n[1] https://www.weather25.com/north-america/usa/california/san-francisco\", 'initial_answer': \"The weather in San Francisco (SF) is characterized by a mild, Mediterranean-like climate with wet winters and dry summers. The city's unique topography and coastal location result in microclimates, where weather conditions can vary significantly from one neighborhood to another. Average temperatures typically range from the low 50s to the mid-60s Fahrenheit throughout the year. Summers in San Francisco are often cooler than in other parts of California due to the cold California Current offshore and the presence of fog, particularly in June and July. The fog usually burns off by the afternoon, leading to clearer skies and slightly warmer temperatures. Winters are mild and moist, with the majority of the city's rainfall occurring between November and March. Rainfall averages around 23 inches annually. Wind is also a notable feature of San Francisco's weather, with spring being the windiest season. Despite the general patterns, it's always advisable to dress in layers due to the potential for rapid weather changes.\"}, log='Reached max steps.')" + "cell_type": "code", + "execution_count": 1, + "id": "d642e6af-217a-4414-a78c-509b44155eca", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain.chat_models import ChatOpenAI\n", + "from langchain_community.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "from langgraph.graph import END, Graph\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]\n", + "\n", + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "\n", + "# Choose the LLM that will drive the agent\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n", + "\n", + "from langchain_core.agents import AgentFinish\n", + "# Define decision-making logic\n", + "def should_continue(data):\n", + " # Logic to decide whether to continue in the loop or exit\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"exit\"\n", + " else:\n", + " return \"continue\"\n", + " \n", + "def execute_tools(data):\n", + " agent_action = data.pop('agent_outcome')\n", + " observation = {t.name: t for t in tools}[agent_action.tool].invoke(agent_action.tool_input)\n", + " data['intermediate_steps'].append((agent_action, observation))\n", + " return data\n", + " \n", + " \n", + "\n", + "# Define agents\n", + "agent = RunnablePassthrough.assign(\n", + " agent_outcome = agent_runnable\n", + ")\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = Graph()\n", + "\n", + "workflow.add_node(\"agent\", agent)\n", + "workflow.add_node(\"tools\", execute_tools)\n", + "\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + " {\n", + " \"continue\": \"tools\",\n", + " \"exit\": END\n", + " }\n", + ")\n", + "\n", + "workflow.add_edge('tools', 'agent')\n", + "\n", + "chain = workflow.compile()" ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c46bd262-9605-4449-9391-f6b6e0fe440e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input': 'what is the weather in sf',\n", + " 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\":\"weather in San Francisco\"}'}})]),\n", + " [{'url': 'https://www.weather25.com/north-america/usa/california/san-francisco',\n", + " 'content': 'will give you an idea of weather trends in San Francisco. For example, the weather in San Francisco in January 2024. San Francisco 14 day weather The weather today in San Francisco San Francisco weather report The weather in San Francisco, United States San Francisco weather by months San Francisco weather the weather in San Francisco including humidity, wind, chance of rain and more on the San Francisco current weather01 January 02 February 03 March 04 April 05 May 06 June 07 July 08 August 09 September 10 October 11 November 12 December. ... For example, the weather in San Francisco in January 2024. These trends can be helpful when planning trips to San Francisco or preparing for the weather in advance. There are many factors to consider when looking at the ...'}])],\n", + " 'agent_outcome': AgentFinish(return_values={'output': 'For the current weather in San Francisco, you can visit the following website: [San Francisco Weather](https://www.weather25.com/north-america/usa/california/san-francisco). This will provide you with the latest weather updates including humidity, wind, chance of rain, and more.'}, log='For the current weather in San Francisco, you can visit the following website: [San Francisco Weather](https://www.weather25.com/north-america/usa/california/san-francisco). This will provide you with the latest weather updates including humidity, wind, chance of rain, and more.')}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke({\"input\": \"what is the weather in sf\", \"intermediate_steps\": []})" + ] + }, + { + "cell_type": "markdown", + "id": "592c3886-71d1-4539-80dd-111e55cc3a85", + "metadata": {}, + "source": [ + "## Reflexion Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f6f96e81-4a20-4599-a625-8d18df6fa76d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import AgentExecutor, BaseMultiActionAgent, Tool\n", + "from langchain.schema import AgentAction, AgentFinish\n", + "from langchain_core.language_models.chat_models import BaseChatModel\n", + "from langchain.chains import LLMChain\n", + "\n", + "from langchain.globals import set_llm_cache\n", + "\n", + "from dotenv import load_dotenv\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "from langchain.chat_models import ChatOpenAI\n", + "from langchain.cache import SQLiteCache\n", + "\n", + "from langchain_core.output_parsers import BaseOutputParser\n", + "\n", + "from langchain.prompts.chat import ChatPromptTemplate\n", + "from langchain.callbacks import get_openai_callback\n", + "from langchain.tools.tavily_search import TavilySearchResults\n", + "from langchain.utilities.tavily_search import TavilySearchAPIWrapper\n", + "from langchain.pydantic_v1 import BaseModel\n", + "import os\n", + "\n", + "from langchain.agents import AgentType, initialize_agent, load_tools\n", + "\n", + "set_llm_cache(SQLiteCache(database_path=\".langchain.db\"))\n", + "\n", + "\n", + "llm = ChatOpenAI(\n", + " temperature=0.0,\n", + " max_tokens=2000,\n", + " max_retries=100,\n", + " model=\"gpt-4-1106-preview\",\n", + ")\n", + "\n", + "search = TavilySearchAPIWrapper()\n", + "tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\n", + "\n", + "NEXT_STEP_TEMPLATE = \"\"\"You are expert researcher trying answer a question ~250 words. You are asked to answer the following question: {question}\n", + "\n", + "The way you are going to answer the question is as follows:\n", + "\n", + "1. Revise your previous answer using the new information.\n", + " - You should use the previous critique to add important information to your answer.\n", + " _ You MUST include numerical citations in your revised answer to ensure it can be verified.\n", + " - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n", + " - [1] https://example.com\n", + " - [2] https://example.com\n", + " - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n", + "2. Reflect and critique your answer. Specifically, you should:\n", + " - Think about what is missing from your answer.\n", + " - Think about what is superfluous in your answer.\n", + " - Think about what search query you should use next to improve your answer.\n", + " Give your answer in exactly 2 parts. The first should address what is missing from your answer. The second should address what could be removed from your answer. Your should be VERY harsh as we really want to improve the answer.\n", + "3. Give the search query you came up with to improve your answer.\n", + "\n", + "Previous steps: \n", + "\n", + "{previous_steps}\n", + "\n", + "===\n", + "\n", + "Format your answer as follows:\n", + "\n", + "Revised answer: [give your revised answer based on the previous critique and new information from the search engine then the \"References\" section]\n", + "Critique: [give your harsh critique of your revised answer in 2 parts: what is missing and what is superfluous]\n", + "Search query: [give the new search query you came up with to enter into the search engine to improve your answer. If you have more than one, make sure they are comma separated and in quotes]\n", + "\n", + "SAY NOTHING else please.\"\"\"\n", + "\n", + "INITIAL_ANSWER_TEMPLATE = \"\"\"You are expert researcher trying answer a question ~250 words. You are asked to answer the following question: {question}\n", + "\n", + "The way you are going to answer the question is as follows:\n", + "\n", + "1. Give a detailed in ~250 words.\n", + "2. Reflect and critique your answer. Specifically, you should:\n", + " - Think about what is missing from your answer.\n", + " - Think about what is superfluous in your answer.\n", + " - Think about what search query you should use next to improve your answer.\n", + " Give your answer in exactly 2 parts. The first should address what is missing from your answer. The second should address what could be removed from your answer. Your should be VERY harsh as we really want to improve the answer.\n", + "3. Give the search query you came up with to improve your answer.\n", + "\n", + "===\n", + "\n", + "Format your answer as follows:\n", + "\n", + "Answer: [give your initial answer]\n", + "Critique: [give your harsh critique of your answer in 2 parts: what is missing and what is superfluous]\n", + "Search query: [give the search query you came up with to improve your answer. If you have more than one, make sure they are comma separated and in quotes]\n", + "\n", + "SAY NOTHING else please.\"\"\"\n", + "\n", + "\n", + "class ReflexionStep(BaseModel):\n", + " \"\"\"A single step in the reflexion process.\"\"\"\n", + "\n", + " answer: str\n", + " critique: str\n", + " search_query: str\n", + "\n", + " def __str__(self):\n", + " return f\"Answer: {self.answer}\\nCritique: {self.critique}\\nSearch query: {self.search_query}\"\n", + "\n", + "def _parse_reflexion_step(output: str) -> tuple[str, str, str]:\n", + " # find answer using .split()\n", + " if (\"Answer:\" not in output and \"Revised answer:\" not in output) or not \"Critique:\" in output or not \"Search query:\" in output:\n", + " raise ValueError(f\"The output is not formatted correctly. Output: {output}\")\n", + " if \"Answer:\" in output:\n", + " answer = output.split(\"Answer:\")[1].split(\"Critique:\")[0].strip()\n", + " else:\n", + " answer = output.split(\"Revised answer:\")[1].split(\"Critique:\")[0].strip()\n", + " critique = output.split(\"Critique:\")[1].split(\"Search query:\")[0].strip()\n", + " search_query = output.split(\"Search query:\")[1].strip()\n", + " return answer, critique, search_query\n", + "\n", + "class ReflexionStepParser(BaseOutputParser[ReflexionStep]):\n", + " \"\"\"Parser for the reflexion step.\"\"\"\n", + "\n", + " def parse(self, output: str) -> ReflexionStep:\n", + " \"\"\"Parse the output.\"\"\"\n", + " # try to find answer or initial answer\n", + " answer, critique, search_query = _parse_reflexion_step(output)\n", + " return ReflexionStep(\n", + " answer=answer, critique=critique, search_query=search_query\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7708fa95-547b-4bea-b126-3656de7d5873", + "metadata": {}, + "outputs": [], + "source": [ + "initial_chain = RunnablePassthrough.assign(\n", + " agent_outcome = ChatPromptTemplate.from_template(INITIAL_ANSWER_TEMPLATE) | llm | ReflexionStepParser() | (lambda x: AgentAction(\n", + " tool=\"tavily_search_results_json\",\n", + " tool_input=x.search_query,\n", + " log=str(x),\n", + " ))\n", + ")\n", + "\n", + "def prep_next(inputs):\n", + " intermediate_steps = inputs[\"intermediate_steps\"]\n", + " previous_steps = list[str]()\n", + "\n", + " for i, (action, observation) in enumerate(intermediate_steps, start=1):\n", + " last_step_str = f\"\"\"Step {i}:\n", + "\n", + "{action.log}\n", + "\n", + "Search output for \"{action.tool_input}\":\n", + "\n", + "{observation}\"\"\"\n", + " previous_steps.append(last_step_str)\n", + "\n", + " previous_steps_str = \"\\n\\n\".join(previous_steps)\n", + " inputs[\"previous_steps\"] = previous_steps_str\n", + " return inputs\n", + " \n", + "next_chain = RunnablePassthrough.assign(\n", + " agent_outcome = prep_next | ChatPromptTemplate.from_template(NEXT_STEP_TEMPLATE) | llm | ReflexionStepParser() | (lambda x: AgentAction(\n", + " tool=\"tavily_search_results_json\",\n", + " tool_input=x.search_query,\n", + " log=str(x),\n", + " ))\n", + ")\n", + "\n", + "def finish(inputs):\n", + " intermediate_steps = inputs[\"intermediate_steps\"]\n", + " last_action, _ = intermediate_steps[-1]\n", + " last_step_str = last_action.log\n", + " # extract answer\n", + " answer, _, _ = _parse_reflexion_step(last_step_str)\n", + "\n", + " first_action, _ = intermediate_steps[0]\n", + " first_step_str = first_action.log\n", + " # extract answer\n", + " initial_answer, _, _ = _parse_reflexion_step(first_step_str)\n", + "\n", + " return AgentFinish(\n", + " log=\"Reached max steps.\",\n", + " return_values={\"output\": answer, \"initial_answer\": initial_answer},\n", + " )\n", + "\n", + "\n", + "def execute_tools(data):\n", + " agent_action = data.pop('agent_outcome')\n", + " observation = {t.name: t for t in tools}[agent_action.tool].invoke(agent_action.tool_input)\n", + " data['intermediate_steps'].append((agent_action, observation))\n", + " return data\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d6cdd1cd-e480-4dd7-99b4-9018eb243b4d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentFinish(return_values={'output': \"The current weather in San Francisco can be accessed through various weather reporting services, which provide real-time temperature, humidity, wind, and chances of precipitation [1]. Historically, San Francisco experiences a mild, Mediterranean climate with average temperatures ranging from the low 50s to the mid-60s Fahrenheit. The city's unique topography creates microclimates, leading to significant weather variations across different neighborhoods. San Francisco's summers are notably cooler compared to other Californian cities, largely due to the cold California Current and persistent fog, especially in June and July. Winters are mild and the wettest months span from November to March, with an annual rainfall average of approximately 23 inches. Wind is a prominent feature, with spring being particularly windy. For historical weather extremes and average wind speeds, additional specific data can be sought from climatological records.\\n\\nReferences:\\n[1] https://www.weather25.com/north-america/usa/california/san-francisco\", 'initial_answer': \"The weather in San Francisco (SF) is characterized by a mild, Mediterranean-like climate with wet winters and dry summers. The city's unique topography and coastal location result in microclimates, where weather conditions can vary significantly from one neighborhood to another. Average temperatures typically range from the low 50s to the mid-60s Fahrenheit throughout the year. Summers in San Francisco are often cooler than in other parts of California due to the cold California Current offshore and the presence of fog, particularly in June and July. The fog usually burns off by the afternoon, leading to clearer skies and slightly warmer temperatures. Winters are mild and moist, with the majority of the city's rainfall occurring between November and March. Rainfall averages around 23 inches annually. Wind is also a notable feature of San Francisco's weather, with spring being the windiest season. Despite the general patterns, it's always advisable to dress in layers due to the potential for rapid weather changes.\"}, log='Reached max steps.')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "workflow = Graph()\n", + "\n", + "# add actors\n", + "workflow.add_node(\"initial\", initial_chain)\n", + "workflow.add_node(\"next\", next_chain)\n", + "workflow.add_node(\"finish\", finish)\n", + "workflow.add_node(\"tools\", execute_tools)\n", + "\n", + "# Enter with initial actor, then loop through tools -> next steps until finished\n", + "workflow.set_entry_point('initial')\n", + "\n", + "workflow.add_edge('initial', 'tools')\n", + "workflow.add_conditional_edges(\n", + " 'tools',\n", + " lambda x: \"exit\" if len(x['intermediate_steps']) >= 2 else \"continue\",\n", + " {\n", + " \"continue\": 'next',\n", + " \"exit\": 'finish'\n", + " }\n", + ")\n", + "workflow.add_edge('next', 'tools')\n", + "workflow.set_finish_point('finish')\n", + "\n", + "chain = workflow.compile()\n", + "\n", + "chain.invoke({\"question\": \"what is the weather in sf\", \"intermediate_steps\": []})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9babf196-b1fd-492d-9197-96a674f5e81d", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58ce0d58-fb00-4dc1-a12b-8fc015474611", + "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.5" } - ], - "source": [ - "workflow = Graph()\n", - "\n", - "# add actors\n", - "workflow.add_node(\"initial\", initial_chain)\n", - "workflow.add_node(\"next\", next_chain)\n", - "workflow.add_node(\"finish\", finish)\n", - "workflow.add_node(\"tools\", execute_tools)\n", - "\n", - "# Enter with initial actor, then loop through tools -> next steps until finished\n", - "workflow.set_entry_point('initial')\n", - "\n", - "workflow.add_edge('initial', 'tools')\n", - "workflow.add_conditional_edges(\n", - " 'tools',\n", - " lambda x: \"exit\" if len(x['intermediate_steps']) >= 2 else \"continue\",\n", - " {\n", - " \"continue\": 'next',\n", - " \"exit\": 'finish'\n", - " }\n", - ")\n", - "workflow.add_edge('next', 'tools')\n", - "workflow.set_finish_point('finish')\n", - "\n", - "chain = workflow.compile()\n", - "\n", - "chain.invoke({\"question\": \"what is the weather in sf\", \"intermediate_steps\": []})" - ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "9babf196-b1fd-492d-9197-96a674f5e81d", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "58ce0d58-fb00-4dc1-a12b-8fc015474611", - "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.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/examples/old/example.ipynb b/examples/old/example.ipynb deleted file mode 100644 index 223fd90ee..000000000 --- a/examples/old/example.ipynb +++ /dev/null @@ -1,384 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "fc5e376f-eff5-4546-956b-a257250d0a74", - "metadata": {}, - "source": [ - "# Simple Example\n", - "\n", - "This is a simple example to get familiar with how to use permchain. permchain is a pub-sub framework which makes it easy to coordinate multiple LLM actors (whether these be agents or single LLM calls). This notebook goes over a simple example of three actors:\n", - "\n", - "- a writer, responsible for writing the first draft\n", - "- a editor, responsible for critiquing a written draft\n", - "- a reviser, responsible for taking a draft and associated critiques and editing it\n", - "\n", - "We will first define these actors individually, and then we will show how to coordinate them such that for a given input the writer will write a draft, and then the editor and reviser will go back and forth until the editor thinks its good enough." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "a9a1a8db-794a-442b-93fc-3d612aa93845", - "metadata": {}, - "outputs": [], - "source": [ - "from operator import itemgetter\n", - "\n", - "from langchain.chat_models.openai import ChatOpenAI\n", - "from langchain.prompts import SystemMessagePromptTemplate\n", - "from langchain.schema.output_parser import StrOutputParser\n", - "from langchain.runnables.openai_functions import OpenAIFunctionsRouter\n", - "\n", - "from permchain.connection_inmemory import InMemoryPubSubConnection\n", - "from permchain.pubsub import PubSub\n", - "from permchain.topic import Topic" - ] - }, - { - "cell_type": "markdown", - "id": "73b3d3f8-0408-4dbc-ab1f-4384ad6011fa", - "metadata": {}, - "source": [ - "## Drafter" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4e11ecdb-2b74-4f1e-8b8b-91a0d0e7547c", - "metadata": {}, - "outputs": [], - "source": [ - "drafter_prompt = (\n", - " SystemMessagePromptTemplate.from_template(\n", - " \"You are an expert on turtles, who likes to write in pirate-speak. You have been tasked by your editor with drafting a 100-word article answering the following question.\"\n", - " )\n", - " + \"Question:\\n\\n{question}\"\n", - ")\n", - "drafter_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", - "drafter = drafter_prompt | drafter_llm | StrOutputParser()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "85df70b5-4d3c-47b1-b401-036f513965b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Arrr, me hearties! What be art, ye ask? Art be a fine treasure crafted by the hands of a creative soul. It be a form o' expression, a way to share the beauty and wonders o' the world. It be a splash o' colors on a canvas, a melody playin' in yer ear, or a tale spun with words. Art be a look into the depths o' the human spirit, a glimpse into the mysteries o' life. So, me mateys, let yer hearts be filled with art, for it be the treasure that brings joy and meaning to our pirate lives!\"" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "drafter.invoke({\"question\": \"what is art?\"})" - ] - }, - { - "cell_type": "markdown", - "id": "a4d554fd-3cfb-4705-bafc-8523d3cd79ff", - "metadata": {}, - "source": [ - "## Critiquer" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "5371da31-1fd1-46dd-afaa-6727cc6a3d57", - "metadata": {}, - "outputs": [], - "source": [ - "editor_prompt = (\n", - " SystemMessagePromptTemplate.from_template(\n", - " \"You are an editor. You have been tasked with editing the following draft, which was written by a non-expert. Please accept the draft if it is good enough to publish, or send it for revision, along with your notes to guide the revision.\"\n", - " )\n", - " + \"Draft:\\n\\n{draft}\"\n", - ")\n", - "editor_llm = ChatOpenAI(model=\"gpt-4\")\n", - "functions = [\n", - " {\n", - " \"name\": \"revise\",\n", - " \"description\": \"Sends the draft for revision\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"notes\": {\n", - " \"type\": \"string\",\n", - " \"description\": \"The editor's notes to guide the revision.\",\n", - " },\n", - " },\n", - " },\n", - " },\n", - " {\n", - " \"name\": \"accept\",\n", - " \"description\": \"Accepts the draft\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\"ready\": {\"const\": True}},\n", - " },\n", - " },\n", - "]\n", - "editor = editor_prompt | editor_llm.bind(functions=functions)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8e2f7fa4-eef5-440e-bf51-ee236dc421e6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage(content='', additional_kwargs={'function_call': {'name': 'revise', 'arguments': '{\\n \"notes\": \"The current draft is too short and lacks any context or detailed information. Please provide a more comprehensive and detailed draft for review.\"\\n}'}}, example=False)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "editor.invoke({\"draft\": \"hi!\"})" - ] - }, - { - "cell_type": "markdown", - "id": "9dfbf1b6-b074-4fe6-acbb-80742d943dc3", - "metadata": {}, - "source": [ - "## Reviser" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "e9dcbbc9-2bc2-4a15-9002-1acb2692f943", - "metadata": {}, - "outputs": [], - "source": [ - "reviser_prompt = (\n", - " SystemMessagePromptTemplate.from_template(\n", - " \"You are an expert on turtles. You have been tasked by your editor with revising the following draft, which was written by a non-expert. You may follow the editor's notes or not, as you see fit.\"\n", - " )\n", - " + \"Draft:\\n\\n{draft}\"\n", - " + \"Editor's notes:\\n\\n{notes}\"\n", - ")\n", - "reviser_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", - "reviser = reviser_prompt | reviser_llm | StrOutputParser()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "9f200a4c-628b-495b-a9ec-c647f8dcdcf0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Revised draft:\\n\\nHello there!'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "reviser.invoke({\"draft\": \"hi!\", \"notes\": \"too short\"})" - ] - }, - { - "cell_type": "markdown", - "id": "5089152c-8073-4811-b7e2-9ee2fc2397b9", - "metadata": {}, - "source": [ - "## Hooking it all up\n", - "\n", - "We can now hook it all up. This means:\n", - "\n", - "1. Each chain should subscribe to some events. This can be the `input` event, or they can listen for pushes to an inbox\n", - "2. Each chain should do something with the output. This can involving returning a final answer, or pushing to an inbox" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "82f6c33f-1553-4d92-bad9-0b499dafcf6c", - "metadata": {}, - "outputs": [], - "source": [ - "# create topics\n", - "editor_inbox = Topic(\"editor_inbox\")\n", - "reviser_inbox = Topic(\"reviser_inbox\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "af624684-c3b4-4283-aecb-d57d1b2316f9", - "metadata": {}, - "outputs": [], - "source": [ - "draft_chain = (\n", - " # Listed in inputs\n", - " Topic.IN.subscribe()\n", - " | {\"draft\": drafter}\n", - " # The draft always goes to the editors inbox\n", - " | editor_inbox.publish()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "3deabd3d-2995-4565-a6b2-38e39171143f", - "metadata": {}, - "outputs": [], - "source": [ - "editor_chain = (\n", - " # Listen for events in the editors inbox\n", - " editor_inbox.subscribe()\n", - " | editor\n", - " # Depending on the output, different things should happen\n", - " | OpenAIFunctionsRouter(\n", - " {\n", - " # If revise is chosen, we send a push to the revisor's inbox\n", - " \"revise\": (\n", - " {\n", - " \"notes\": itemgetter(\"notes\"),\n", - " \"draft\": editor_inbox.current() | itemgetter(\"draft\"),\n", - " \"question\": Topic.IN.current() | itemgetter(\"question\"),\n", - " }\n", - " | reviser_inbox.publish()\n", - " ),\n", - " # If accepted, then we return\n", - " \"accept\": editor_inbox.current() | Topic.OUT.publish(),\n", - " },\n", - " )\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "f3aaf437-95d6-4e1f-a756-15b5132ac9f7", - "metadata": {}, - "outputs": [], - "source": [ - "reviser_chain = (\n", - " # Listen for events in the reviser's inbox\n", - " reviser_inbox.subscribe()\n", - " | {\"draft\": reviser}\n", - " # Publish to the editors inbox\n", - " | editor_inbox.publish()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "314b75ee-837d-419c-81a7-ea3dec97203b", - "metadata": {}, - "outputs": [], - "source": [ - "web_researcher = PubSub(\n", - " processes=(draft_chain, editor_chain, reviser_chain),\n", - " connection=InMemoryPubSubConnection(),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "83087e85-b526-4730-b51e-9654dd0b8f69", - "metadata": {}, - "outputs": [], - "source": [ - "import langchain\n", - "\n", - "langchain.verbose = True" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "0371a5ac-7194-4cf9-9dd2-56cb3d1f46d6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'draft': 'Turtles have specific dietary preferences that vary depending on their species. Sea turtles, for example, primarily consume seaweed, jellyfish, and occasionally fish. On the other hand, land turtles, such as tortoises, mainly graze on grass, flowers, and leafy greens. Some turtles even enjoy fruits like berries and melons in addition to their plant-based diet. Insects also make for a crunchy treat that some turtles may indulge in. Therefore, whether they inhabit land or sea, turtles have a diverse range of food options to keep their bodies nourished and satisfied.'}]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "web_researcher.invoke({\"question\": \"What food do turtles eat?\"})" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "6bc365e5-06d9-49d9-8278-c3b1138ea73c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[[{'draft': 'Turtles are fascinating creatures with a diverse appetite. Depending on their species and habitat, turtles consume a variety of foods. Some turtles primarily eat plants such as seaweed, grass, and algae. For instance, the green sea turtle is known to graze on seagrass beds and algae. Other turtles, like the snapping turtle, have a more carnivorous diet, feasting on insects, fish, and small crustaceans. Additionally, there are land-dwelling turtles that enjoy fruits and vegetables in their diet. For example, the box turtle has been observed eating berries and leafy greens. With such a varied diet, turtles keep their bellies full and maintain their overall health.'}],\n", - " [{'draft': 'Revised draft:\\n\\nHello, readers! You may be wondering where bears live. Well, bears are known to inhabit a wide range of lands, from the icy regions of the Arctic to the lush forests of the jungles. They can be found in North America, Europe, Asia, and even some parts of South America. Bears are highly adaptable creatures, capable of surviving in various habitats, including mountains, tundra, and even deserts. They create dens for hibernation and seek shelter in caves, trees, or dense vegetation. So, be observant, friends, as bears may be encountered in unexpected places!'}]]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "[\n", - " *web_researcher.batch(\n", - " [\n", - " {\"question\": \"What food do turtles eat?\"},\n", - " {\"question\": \"Where do bears live?\"},\n", - " ]\n", - " )\n", - "]" - ] - } - ], - "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.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/old/research/researcher.py b/examples/old/research/researcher.py deleted file mode 100644 index ea1252468..000000000 --- a/examples/old/research/researcher.py +++ /dev/null @@ -1,97 +0,0 @@ -from operator import itemgetter - -import requests -from fastapi import FastAPI -from langchain.chat_models import ChatOpenAI -from langchain.output_parsers.openai_functions import JsonKeyOutputFunctionsParser -from langchain.prompts import ChatPromptTemplate -from langchain.schema.output_parser import StrOutputParser - -from permchain.connection_inmemory import InMemoryPubSubConnection -from permchain.pubsub import PubSub -from permchain.topic import Topic - -template = """Write between 2 and 5 sub questions that serve as google search queries to search online that form an objective opinion from the following: {question}""" -functions = [ - { - "name": "sub_questions", - "description": "List of sub questions", - "parameters": { - "type": "object", - "properties": { - "questions": { - "type": "array", - "description": "List of sub questions to ask.", - "items": {"type": "string"}, - }, - }, - }, - }, -] -prompt = ChatPromptTemplate.from_template(template) -question_chain = ( - prompt - | ChatOpenAI(temperature=0).bind( - functions=functions, function_call={"name": "sub_questions"} - ) - | JsonKeyOutputFunctionsParser(key_name="questions") -) - -template = """You are tasked with writing a research report to answer the following question: - - -{question} - - -In order to do that, you first came up with several sub questions and researched those. please find those below: - - -{research} - - -Now, write your final report answering the original question!""" -prompt = ChatPromptTemplate.from_template(template) -report_chain = prompt | ChatOpenAI() | StrOutputParser() - -research_inbox = Topic("research") -writer_inbox = Topic("writer_inbox") - - -def web_researcher(questions): - response = requests.post( - "http://127.0.0.1:8081/batch", json={"questions": questions} - ) - return response.json() - - -subquestion_actor = ( - # Listed in inputs - Topic.IN.subscribe() - | question_chain - # The draft always goes to the editors inbox - | research_inbox.publish() -) -research_actor = ( - research_inbox.subscribe() - | { - "research": lambda x: web_researcher(x), - # "research": (lambda x: [web_researcher(i) for i in x]), - "question": Topic.IN.current() | itemgetter("question"), - } - | writer_inbox.publish() -) -write_actor = ( - writer_inbox.subscribe() | {"response": report_chain} | Topic.OUT.publish() -) - -longer_researcher = PubSub( - processes=(subquestion_actor, research_actor, write_actor), - connection=InMemoryPubSubConnection(), -) - -app = FastAPI() - - -@app.get("/report") -def read_item(question: str): - return longer_researcher.invoke({"question": question}) diff --git a/examples/old/research/single_question_researcher.py b/examples/old/research/single_question_researcher.py deleted file mode 100644 index 456e854ed..000000000 --- a/examples/old/research/single_question_researcher.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import List - -import requests -from fastapi import FastAPI -from langchain.chat_models import ChatAnthropic, ChatOpenAI -from langchain.prompts import ChatPromptTemplate -from langchain.schema.output_parser import StrOutputParser -from pydantic import BaseModel - -from permchain.connection_inmemory import InMemoryPubSubConnection -from permchain.pubsub import PubSub -from permchain.topic import Topic - -prompt = ChatPromptTemplate.from_template( - "Answer the user's question given the search results\n\n{question}{search_results}" -) - -summarizer_chain = ( - prompt - | ChatOpenAI(max_retries=0).with_fallbacks( - [ChatOpenAI(model="gpt-3.5-turbo-16k"), ChatAnthropic(model="claude-2")] - ) - | StrOutputParser() -) - - -def retrieve_documents(query): - response = requests.get("http://127.0.0.1:8080/query", params={"query": query}) - return response.json() - - -summarizer_inbox = Topic("summarizer") - -search_actor = ( - Topic.IN.subscribe() - | { - "search_results": retrieve_documents, - "question": Topic.IN.current(), - } - | summarizer_inbox.publish() -) - -summ_actor = ( - summarizer_inbox.subscribe() | {"answer": summarizer_chain} | Topic.OUT.publish() -) - -web_researcher = PubSub( - processes=(search_actor, summ_actor), - connection=InMemoryPubSubConnection(), -) - -app = FastAPI() - - -class Data(BaseModel): - questions: List[str] - - -@app.get("/invoke") -def read_item(question: str): - return web_researcher.invoke(question) - - -@app.post("/batch") -def batch(data: Data): - return web_researcher.batch(data.questions) diff --git a/examples/old/research/webscraper.py b/examples/old/research/webscraper.py deleted file mode 100644 index 35c19108f..000000000 --- a/examples/old/research/webscraper.py +++ /dev/null @@ -1,38 +0,0 @@ -# main.py - -from duckduckgo_search import DDGS -from fastapi import FastAPI -from langchain.document_loaders import AsyncHtmlLoader -from langchain.document_transformers import Html2TextTransformer - -ddgs = DDGS() - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/query") -def read_item(query: str): - query = query.strip().strip('"') - search_results = ddgs.text(query) - urls_to_look = [] - for res in search_results: - if res.get("href", None): - urls_to_look.append(res["href"]) - if len(urls_to_look) >= 4: - break - - # Relevant urls - # Load, split, and add new urls to vectorstore - if urls_to_look: - loader = AsyncHtmlLoader(urls_to_look) - html2text = Html2TextTransformer() - docs = loader.load() - docs = list(html2text.transform_documents(docs)) - else: - docs = [] - return docs diff --git a/examples/old/web-research.ipynb b/examples/old/web-research.ipynb deleted file mode 100644 index b552a38d8..000000000 --- a/examples/old/web-research.ipynb +++ /dev/null @@ -1,498 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "cd80bc40-f10d-4ab3-826d-6cd0636d11e0", - "metadata": {}, - "outputs": [], - "source": [ - "from operator import itemgetter\n", - "\n", - "from langchain.chat_models import ChatOpenAI, ChatAnthropic\n", - "from langchain.prompts import SystemMessagePromptTemplate, ChatPromptTemplate\n", - "from langchain.schema.output_parser import StrOutputParser\n", - "from langchain.runnables.openai_functions import OpenAIFunctionsRouter\n", - "\n", - "from permchain.connection_inmemory import InMemoryPubSubConnection\n", - "from permchain.pubsub import PubSub\n", - "from permchain.topic import Topic" - ] - }, - { - "cell_type": "markdown", - "id": "54c553be-9ed1-452c-a4ab-828f34dbb3ce", - "metadata": {}, - "source": [ - "## Content Fetcher" - ] - }, - { - "cell_type": "markdown", - "id": "56a6788d-b67c-4331-af8d-7741a66f03af", - "metadata": {}, - "source": [ - "First, we are going to define our content fetcher. This is responsible for taking a search query an getting relevant web pages." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a9c32e92-6f19-4cf1-8b87-1b756de0e263", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.utilities import GoogleSearchAPIWrapper\n", - "from langchain.document_loaders import AsyncHtmlLoader\n", - "from langchain.document_transformers import Html2TextTransformer" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "89d4a96a-2f59-491f-81fd-4a5d755c0081", - "metadata": {}, - "outputs": [], - "source": [ - "from duckduckgo_search import DDGS\n", - "\n", - "ddgs = DDGS()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "dcdc3812-0cdc-4677-bf9d-f9d7d5cac7e2", - "metadata": {}, - "outputs": [], - "source": [ - "def retrieve_documents(query):\n", - " query = query.strip().strip('\"')\n", - " search_results = ddgs.text(query)\n", - " urls_to_look = []\n", - " for res in search_results:\n", - " if res.get(\"href\", None):\n", - " urls_to_look.append(res[\"href\"])\n", - " if len(urls_to_look) >= 4:\n", - " break\n", - "\n", - " # Relevant urls\n", - " # Load, split, and add new urls to vectorstore\n", - " if urls_to_look:\n", - " loader = AsyncHtmlLoader(urls_to_look)\n", - " html2text = Html2TextTransformer()\n", - " docs = loader.load()\n", - " docs = list(html2text.transform_documents(docs))\n", - " else:\n", - " docs = []\n", - " return docs" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "45bb7661-35db-4a67-a62e-9c791c9de359", - "metadata": {}, - "outputs": [], - "source": [ - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d087d2b4-d1fa-4f63-81de-d3d4e1dc5b1b", - "metadata": {}, - "outputs": [], - "source": [ - "# docs = retrieve_documents(\"langchain\")" - ] - }, - { - "cell_type": "markdown", - "id": "73e0b79f-f6bd-4f68-9433-645ee1eea81e", - "metadata": {}, - "source": [ - "## Summarizer\n", - "We will now come up with an actor to summarize the results given a query and some search results" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "901e1f8d-c973-4998-a731-5dab0c147b8c", - "metadata": {}, - "outputs": [], - "source": [ - "prompt = ChatPromptTemplate.from_template(\n", - " \"Answer the user's question given the search results\\n\\n{question}{search_results}\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "915bec33-d210-4471-b051-859ecba608be", - "metadata": {}, - "outputs": [], - "source": [ - "summarizer_chain = (\n", - " prompt\n", - " | ChatOpenAI(max_retries=0).with_fallbacks(\n", - " [ChatOpenAI(model=\"gpt-3.5-turbo-16k\"), ChatAnthropic(model=\"claude-2\")]\n", - " )\n", - " | StrOutputParser()\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "6bf403ec-39a4-4061-9401-06850ffe3761", - "metadata": {}, - "source": [ - "## All together now!" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "edc0def4-d184-4438-9099-e6604ba9ff28", - "metadata": {}, - "outputs": [], - "source": [ - "summarizer_inbox = Topic(\"summarizer\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "23f89611-7b0f-4f01-b9a7-119124e3341d", - "metadata": {}, - "outputs": [], - "source": [ - "search_actor = (\n", - " Topic.IN.subscribe()\n", - " | {\n", - " \"search_results\": retrieve_documents,\n", - " \"question\": Topic.IN.current(),\n", - " }\n", - " | summarizer_inbox.publish()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "2d4a6b51-bd93-47c2-a301-9593a47df7d4", - "metadata": {}, - "outputs": [], - "source": [ - "summ_actor = (\n", - " summarizer_inbox.subscribe() | {\"answer\": summarizer_chain} | Topic.OUT.publish()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "11d3b066-7f95-4ad9-82d0-ba64bbf3e3fa", - "metadata": {}, - "outputs": [], - "source": [ - "web_researcher = PubSub(\n", - " processes=(search_actor, summ_actor),\n", - " connection=InMemoryPubSubConnection(),\n", - ").with_config(run_name=\"WebResearcher\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "bc022d51-69f0-4da9-8025-70afdc3cc6a8", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Fetching pages: 100%|###################################################| 4/4 [00:01<00:00, 2.19it/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'answer': 'LangSmith is a platform built by LangChain to help developers build production-grade language model (LLM) applications. It enables developers to trace and evaluate their LLM applications and intelligent agents, ensuring reliability and maintainability in the production environment. LangSmith integrates seamlessly with LangChain and provides features such as tracing runs, testing, and evaluating prompts or answers generated by LLM applications. It aims to facilitate the development lifecycle, maintenance, and improvement of AI models. For more information, you can refer to the LangSmith documentation.'}]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "web_researcher.invoke(\"What is langsmith?\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "000f4f24-15ba-476f-8a33-d023081b18d2", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Fetching pages: 0%| | 0/4 [00:00\n", - "{question}\n", - "\n", - "\n", - "In order to do that, you first came up with several sub questions and researched those. please find those below:\n", - "\n", - "\n", - "{research}\n", - "\n", - "\n", - "Now, write your final report answering the original question!\"\"\"\n", - "prompt = ChatPromptTemplate.from_template(template)\n", - "report_chain = prompt | ChatOpenAI() | StrOutputParser()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "d7715e0c-23c0-4985-97a7-bf5b151cf734", - "metadata": {}, - "outputs": [], - "source": [ - "research_inbox = Topic(\"research\")\n", - "writer_inbox = Topic(\"writer_inbox\")" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "f274027a-c9f0-4efa-b798-1921b9b376d9", - "metadata": {}, - "outputs": [], - "source": [ - "subquestion_actor = (\n", - " # Listed in inputs\n", - " Topic.IN.subscribe()\n", - " | question_chain\n", - " # The draft always goes to the editors inbox\n", - " | research_inbox.publish()\n", - ")\n", - "research_actor = (\n", - " research_inbox.subscribe()\n", - " | {\n", - " \"research\": web_researcher.map(),\n", - " # \"research\": lambda x: [web_researcher.invoke({\"question\": i}) for i in x],\n", - " \"question\": Topic.IN.current() | itemgetter(\"question\"),\n", - " }\n", - " | writer_inbox.publish()\n", - ")\n", - "write_actor = writer_inbox.subscribe() | report_chain | Topic.OUT.publish()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "789b636d-23ce-44fb-a8e3-fdfbfabe77ff", - "metadata": {}, - "outputs": [], - "source": [ - "longer_researcher = PubSub(\n", - " processes=(subquestion_actor, research_actor, write_actor),\n", - " connection=InMemoryPubSubConnection(),\n", - ").with_config(run_name=\"LongResearcher\")" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "8f713a31-5f60-4d90-9b95-3f42d8fbbddb", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Fetching pages: 0%| | 0/4 [00:00 Iterator[Value]: diff --git a/langgraph/checkpoint/__init__.py b/langgraph/checkpoint/__init__.py new file mode 100644 index 000000000..7a52e0c6f --- /dev/null +++ b/langgraph/checkpoint/__init__.py @@ -0,0 +1,8 @@ +from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointAt +from langgraph.checkpoint.memory import MemorySaver + +__all__ = [ + "CheckpointAt", + "BaseCheckpointSaver", + "MemorySaver", +] diff --git a/permchain/checkpoint/base.py b/langgraph/checkpoint/base.py similarity index 95% rename from permchain/checkpoint/base.py rename to langgraph/checkpoint/base.py index b43cea377..562aba529 100644 --- a/permchain/checkpoint/base.py +++ b/langgraph/checkpoint/base.py @@ -9,7 +9,7 @@ from langchain_core.pydantic_v1 import Field from langchain_core.runnables import RunnableConfig from langchain_core.runnables.utils import ConfigurableFieldSpec -from permchain.utils import StrEnum +from langgraph.utils import StrEnum class Checkpoint(TypedDict): @@ -35,7 +35,7 @@ class CheckpointAt(StrEnum): END_OF_RUN = "end_of_run" -class BaseCheckpointAdapter(Serializable, ABC): +class BaseCheckpointSaver(Serializable, ABC): at: CheckpointAt = CheckpointAt.END_OF_RUN @property diff --git a/permchain/checkpoint/memory.py b/langgraph/checkpoint/memory.py similarity index 88% rename from permchain/checkpoint/memory.py rename to langgraph/checkpoint/memory.py index 7b150203f..41a145397 100644 --- a/permchain/checkpoint/memory.py +++ b/langgraph/checkpoint/memory.py @@ -2,10 +2,10 @@ from langchain_core.pydantic_v1 import Field from langchain_core.runnables import RunnableConfig from langchain_core.runnables.utils import ConfigurableFieldSpec -from permchain.checkpoint.base import BaseCheckpointAdapter, Checkpoint +from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint -class MemoryCheckpoint(BaseCheckpointAdapter): +class MemorySaver(BaseCheckpointSaver): storage: dict[str, Checkpoint] = Field(default_factory=dict) @property diff --git a/permchain/constants.py b/langgraph/constants.py similarity index 100% rename from permchain/constants.py rename to langgraph/constants.py diff --git a/permchain/langgraph/__init__.py b/langgraph/graph/__init__.py similarity index 98% rename from permchain/langgraph/__init__.py rename to langgraph/graph/__init__.py index 1b5fcde3a..a0447cf7f 100644 --- a/permchain/langgraph/__init__.py +++ b/langgraph/graph/__init__.py @@ -9,7 +9,7 @@ from langchain_core.runnables.base import ( coerce_to_runnable, ) -from permchain.pregel import Channel, Pregel +from langgraph.pregel import Channel, Pregel class Branch(NamedTuple): diff --git a/permchain/pregel/__init__.py b/langgraph/pregel/__init__.py similarity index 97% rename from permchain/pregel/__init__.py rename to langgraph/pregel/__init__.py index bfd61bb56..20b8cc604 100644 --- a/permchain/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -41,29 +41,29 @@ from langchain_core.runnables.utils import ( ) from langchain_core.tracers.log_stream import LogStreamCallbackHandler -from permchain.channels.base import ( +from langgraph.channels.base import ( AsyncChannelsManager, BaseChannel, ChannelsManager, EmptyChannelError, create_checkpoint, ) -from permchain.channels.last_value import LastValue -from permchain.checkpoint.base import ( - BaseCheckpointAdapter, +from langgraph.channels.last_value import LastValue +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, Checkpoint, CheckpointAt, CheckpointView, empty_checkpoint, ) -from permchain.constants import CONFIG_KEY_READ, CONFIG_KEY_SEND -from permchain.pregel.debug import print_checkpoint, print_step_start -from permchain.pregel.io import map_input, map_output -from permchain.pregel.log import logger -from permchain.pregel.read import ChannelBatch, ChannelInvoke -from permchain.pregel.reserved import ReservedChannels -from permchain.pregel.validate import validate_graph -from permchain.pregel.write import ChannelWrite +from langgraph.constants import CONFIG_KEY_READ, CONFIG_KEY_SEND +from langgraph.pregel.debug import print_checkpoint, print_step_start +from langgraph.pregel.io import map_input, map_output +from langgraph.pregel.log import logger +from langgraph.pregel.read import ChannelBatch, ChannelInvoke +from langgraph.pregel.reserved import ReservedChannels +from langgraph.pregel.validate import validate_graph +from langgraph.pregel.write import ChannelWrite WriteValue = Union[ Runnable[Input, Output], @@ -157,7 +157,7 @@ class Pregel(RunnableSerializable[dict[str, Any] | Any, dict[str, Any] | Any]): debug: bool = Field(default_factory=get_debug) - saver: Optional[BaseCheckpointAdapter] = None + saver: Optional[BaseCheckpointSaver] = None class Config: arbitrary_types_allowed = True diff --git a/permchain/pregel/debug.py b/langgraph/pregel/debug.py similarity index 94% rename from permchain/pregel/debug.py rename to langgraph/pregel/debug.py index c1e3cc2be..d6a8a2828 100644 --- a/permchain/pregel/debug.py +++ b/langgraph/pregel/debug.py @@ -4,7 +4,7 @@ from typing import Any, Iterator, Mapping from langchain_core.runnables import Runnable from langchain_core.utils.input import get_bolded_text, get_colored_text -from permchain.channels.base import BaseChannel, EmptyChannelError +from langgraph.channels.base import BaseChannel, EmptyChannelError def print_step_start(step: int, next_tasks: list[tuple[Runnable, Any, str]]) -> None: diff --git a/permchain/pregel/io.py b/langgraph/pregel/io.py similarity index 93% rename from permchain/pregel/io.py rename to langgraph/pregel/io.py index d49193b49..ff30b5d87 100644 --- a/permchain/pregel/io.py +++ b/langgraph/pregel/io.py @@ -1,7 +1,7 @@ from typing import Any, Iterator, Mapping, Sequence -from permchain.channels.base import BaseChannel -from permchain.pregel.log import logger +from langgraph.channels.base import BaseChannel +from langgraph.pregel.log import logger def map_input( diff --git a/permchain/pregel/log.py b/langgraph/pregel/log.py similarity index 100% rename from permchain/pregel/log.py rename to langgraph/pregel/log.py diff --git a/permchain/pregel/read.py b/langgraph/pregel/read.py similarity index 98% rename from permchain/pregel/read.py rename to langgraph/pregel/read.py index f5184f821..923219ccb 100644 --- a/permchain/pregel/read.py +++ b/langgraph/pregel/read.py @@ -18,8 +18,8 @@ from langchain_core.runnables.base import ( ) from langchain_core.runnables.utils import ConfigurableFieldSpec -from permchain.channels.base import BaseChannel -from permchain.constants import CONFIG_KEY_READ +from langgraph.channels.base import BaseChannel +from langgraph.constants import CONFIG_KEY_READ class ChannelRead(RunnableLambda): diff --git a/permchain/pregel/reserved.py b/langgraph/pregel/reserved.py similarity index 100% rename from permchain/pregel/reserved.py rename to langgraph/pregel/reserved.py diff --git a/permchain/pregel/validate.py b/langgraph/pregel/validate.py similarity index 89% rename from permchain/pregel/validate.py rename to langgraph/pregel/validate.py index 66a5c06b4..813c3089f 100644 --- a/permchain/pregel/validate.py +++ b/langgraph/pregel/validate.py @@ -1,9 +1,9 @@ from typing import Any, Mapping, Sequence -from permchain.channels.base import BaseChannel -from permchain.channels.last_value import LastValue -from permchain.pregel.read import ChannelBatch, ChannelInvoke -from permchain.pregel.reserved import ReservedChannels +from langgraph.channels.base import BaseChannel +from langgraph.channels.last_value import LastValue +from langgraph.pregel.read import ChannelBatch, ChannelInvoke +from langgraph.pregel.reserved import ReservedChannels def validate_graph( diff --git a/permchain/pregel/write.py b/langgraph/pregel/write.py similarity index 97% rename from permchain/pregel/write.py rename to langgraph/pregel/write.py index 7135e5a76..e4be13e2e 100644 --- a/permchain/pregel/write.py +++ b/langgraph/pregel/write.py @@ -9,7 +9,7 @@ from langchain_core.runnables import ( ) from langchain_core.runnables.utils import ConfigurableFieldSpec -from permchain.constants import CONFIG_KEY_SEND +from langgraph.constants import CONFIG_KEY_SEND TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None] diff --git a/permchain/utils.py b/langgraph/utils.py similarity index 100% rename from permchain/utils.py rename to langgraph/utils.py diff --git a/permchain/__init__.py b/permchain/__init__.py deleted file mode 100644 index 24c2199d6..000000000 --- a/permchain/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from permchain.checkpoint.base import BaseCheckpointAdapter, CheckpointAt -from permchain.langgraph import Graph -from permchain.pregel import Channel, Pregel, ReservedChannels - -__all__ = [ - "Channel", - "Pregel", - "ReservedChannels", - "BaseCheckpointAdapter", - "CheckpointAt", - "Graph", -] diff --git a/permchain/channels/__init__.py b/permchain/channels/__init__.py deleted file mode 100644 index cc4de04bb..000000000 --- a/permchain/channels/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from permchain.channels.binop import BinaryOperatorAggregate -from permchain.channels.context import Context -from permchain.channels.last_value import LastValue -from permchain.channels.topic import Topic - -__all__ = [ - "LastValue", - "Topic", - "Context", - "BinaryOperatorAggregate", -] diff --git a/pyproject.toml b/pyproject.toml index 3357125af..942d50b35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.poetry] -name = "permchain" +name = "langgraph" version = "0.0.8" -description = "permchain" +description = "langgraph" authors = [] -license = "MIT" +license = "LangGraph License" readme = "README.md" -repository = "https://www.github.com/langchain-ai/permchain" +repository = "https://www.github.com/langchain-ai/langgraph" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" diff --git a/tests/test_channels.py b/tests/test_channels.py index e183ece2e..51b70eb04 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -6,11 +6,11 @@ import httpx import pytest from pytest_mock import MockerFixture -from permchain.channels.base import EmptyChannelError, InvalidUpdateError -from permchain.channels.binop import BinaryOperatorAggregate -from permchain.channels.context import Context -from permchain.channels.last_value import LastValue -from permchain.channels.topic import Topic +from langgraph.channels.base import EmptyChannelError, InvalidUpdateError +from langgraph.channels.binop import BinaryOperatorAggregate +from langgraph.channels.context import Context +from langgraph.channels.last_value import LastValue +from langgraph.channels.topic import Topic def test_last_value() -> None: diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 30a8f891a..30b9bd11e 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -8,14 +8,15 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture -from permchain import Channel, Graph, Pregel -from permchain.channels.base import InvalidUpdateError -from permchain.channels.binop import BinaryOperatorAggregate -from permchain.channels.context import Context -from permchain.channels.last_value import LastValue -from permchain.channels.topic import Topic -from permchain.checkpoint.memory import MemoryCheckpoint -from permchain.pregel.reserved import ReservedChannels +from langgraph.channels.base import InvalidUpdateError +from langgraph.channels.binop import BinaryOperatorAggregate +from langgraph.channels.context import Context +from langgraph.channels.last_value import LastValue +from langgraph.channels.topic import Topic +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, Graph +from langgraph.pregel import Channel, Pregel +from langgraph.pregel.reserved import ReservedChannels def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: @@ -377,7 +378,7 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemoryCheckpoint() + memory = MemorySaver() app = Pregel( nodes={"one": one}, @@ -565,8 +566,6 @@ def test_conditional_graph() -> None: from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough - from permchain.langgraph import END - # Assemble the tools @tool() def search_api(query: str) -> str: diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 082b8569a..e65835931 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -7,14 +7,15 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture -from permchain import Channel, Graph, Pregel -from permchain.channels.base import InvalidUpdateError -from permchain.channels.binop import BinaryOperatorAggregate -from permchain.channels.context import Context -from permchain.channels.last_value import LastValue -from permchain.channels.topic import Topic -from permchain.checkpoint.memory import MemoryCheckpoint -from permchain.pregel.reserved import ReservedChannels +from langgraph.channels.base import InvalidUpdateError +from langgraph.channels.binop import BinaryOperatorAggregate +from langgraph.channels.context import Context +from langgraph.channels.last_value import LastValue +from langgraph.channels.topic import Topic +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, Graph +from langgraph.pregel import Channel, Pregel +from langgraph.pregel.reserved import ReservedChannels async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: @@ -385,7 +386,7 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemoryCheckpoint() + memory = MemorySaver() app = Pregel( nodes={"one": one}, @@ -593,8 +594,6 @@ async def test_conditional_graph() -> None: from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnablePassthrough - from permchain.langgraph import END - # Assemble the tools @tool() def search_api(query: str) -> str: From 5799b80261c83db3a74c2a152fed314b24a9dff0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sun, 7 Jan 2024 19:53:28 -0800 Subject: [PATCH 2/2] .stream() defaults to yielding output from all channels --- langgraph/pregel/__init__.py | 18 ++++++++++++++---- tests/test_pregel.py | 27 +++++++++++++++------------ tests/test_pregel_async.py | 30 ++++++++++++++++++------------ 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 20b8cc604..33a0e49e3 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -222,7 +222,7 @@ class Pregel(RunnableSerializable[dict[str, Any] | Any, dict[str, Any] | Any]): if config["recursion_limit"] < 1: raise ValueError("recursion_limit must be at least 1") # assign defaults - output = output if output is not None else self.output + output = output if output is not None else [chan for chan in self.channels] # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one @@ -339,7 +339,7 @@ class Pregel(RunnableSerializable[dict[str, Any] | Any, dict[str, Any] | Any]): None, ) # assign defaults - output = output if output is not None else self.output + output = output if output is not None else [chan for chan in self.channels] # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one @@ -448,7 +448,12 @@ class Pregel(RunnableSerializable[dict[str, Any] | Any, dict[str, Any] | Any]): **kwargs: Any, ) -> dict[str, Any] | Any: latest: dict[str, Any] | Any = None - for chunk in self.stream(input, config, output=output, **kwargs): + for chunk in self.stream( + input, + config, + output=output if output is not None else self.output, + **kwargs, + ): latest = chunk return latest @@ -498,7 +503,12 @@ class Pregel(RunnableSerializable[dict[str, Any] | Any, dict[str, Any] | Any]): **kwargs: Any, ) -> dict[str, Any] | Any: latest: dict[str, Any] | Any = None - async for chunk in self.astream(input, config, output=output, **kwargs): + async for chunk in self.astream( + input, + config, + output=output if output is not None else self.output, + **kwargs, + ): latest = chunk return latest diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 30b9bd11e..47c4ba321 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -162,14 +162,14 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "inbox": 3, "input": 2, } - assert output is None + assert output == {"inbox": 3} elif view.step == 2: assert view.values == { "output": 4, "inbox": 3, "input": 2, } - assert output == 4 + assert output == {"output": 4} for output, view in app.step(2): if view.step == 1: @@ -177,7 +177,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "inbox": 3, "input": 2, } - assert output is None + assert output == {"inbox": 3} # modify inbox value view.values["inbox"] = 5 elif view.step == 2: @@ -187,7 +187,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "input": 2, } # output is different now - assert output == 6 + assert output == {"output": 6} graph = Graph() graph.add_node("add_one", add_one) @@ -205,14 +205,14 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "add_one": 2, "add_one_more": 3, } - assert output is None + assert output == {"add_one_more": 3} elif view.step == 2: assert view.values == { "add_one": 2, "add_one_more": 3, "__end__": 4, } - assert output == 4 + assert output == {"__end__": 4} for output, view in gapp.step(2): if view.step == 1: @@ -220,7 +220,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "add_one": 2, "add_one_more": 3, } - assert output is None + assert output == {"add_one_more": 3} # modify inbox value view.values["add_one_more"] = 5 elif view.step == 2: @@ -230,7 +230,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "__end__": 6, } # output is different now - assert output == 6 + assert output == {"__end__": 6} def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: @@ -244,9 +244,12 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: input=["input", "inbox"], ) - assert [*app.stream({"input": 2, "inbox": 12})] == [13, 4] # [12 + 1, 2 + 1 + 1] - assert [*app.stream({"input": 2, "inbox": 12}, output=["output"])] == [ - {"output": 13}, + assert [*app.stream({"input": 2, "inbox": 12}, output="output")] == [ + 13, + 4, + ] # [12 + 1, 2 + 1 + 1] + assert [*app.stream({"input": 2, "inbox": 12})] == [ + {"inbox": [3], "output": 13}, {"output": 4}, ] @@ -493,7 +496,7 @@ def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> None: app = Pregel(nodes={"one": one, "two": two}) - assert [c for c in app.stream(2)] == [3, 4] + assert [c for c in app.stream(2)] == [{"between": 3, "output": 3}, {"output": 4}] def test_invoke_two_processes_no_out(mocker: MockerFixture) -> None: diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index e65835931..57b538398 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -163,14 +163,14 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "inbox": 3, "input": 2, } - assert output is None + assert output == {"inbox": 3} elif view.step == 2: assert view.values == { "output": 4, "inbox": 3, "input": 2, } - assert output == 4 + assert output == {"output": 4} async for output, view in app.astep(2): if view.step == 1: @@ -178,7 +178,7 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "inbox": 3, "input": 2, } - assert output is None + assert output == {"inbox": 3} # modify inbox value view.values["inbox"] = 5 elif view.step == 2: @@ -188,7 +188,7 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "input": 2, } # output is different now - assert output == 6 + assert output == {"output": 6} graph = Graph() graph.add_node("add_one", add_one) @@ -206,14 +206,14 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "add_one": 2, "add_one_more": 3, } - assert output is None + assert output == {"add_one_more": 3} elif view.step == 2: assert view.values == { "add_one": 2, "add_one_more": 3, "__end__": 4, } - assert output == 4 + assert output == {"__end__": 4} async for output, view in gapp.astep(2): if view.step == 1: @@ -221,7 +221,7 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "add_one": 2, "add_one_more": 3, } - assert output is None + assert output == {"add_one_more": 3} # modify inbox value view.values["add_one_more"] = 5 elif view.step == 2: @@ -231,7 +231,7 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "__end__": 6, } # output is different now - assert output == 6 + assert output == {"__end__": 6} async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: @@ -246,10 +246,13 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: ) # [12 + 1, 2 + 1 + 1] - assert [c async for c in pubsub.astream({"input": 2, "inbox": 12})] == [13, 4] assert [ - c async for c in pubsub.astream({"input": 2, "inbox": 12}, output=["output"]) - ] == [{"output": 13}, {"output": 4}] + c async for c in pubsub.astream({"input": 2, "inbox": 12}, output="output") + ] == [13, 4] + assert [c async for c in pubsub.astream({"input": 2, "inbox": 12})] == [ + {"inbox": [3], "output": 13}, + {"output": 4}, + ] async def test_batch_two_processes_in_out() -> None: @@ -508,7 +511,10 @@ async def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> Non app = Pregel(nodes={"one": one, "two": two}) # Then invoke pubsub - assert [c async for c in app.astream(2)] == [3, 4] + assert [c async for c in app.astream(2)] == [ + {"between": 3, "output": 3}, + {"output": 4}, + ] async def test_invoke_two_processes_no_out(mocker: MockerFixture) -> None: