mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-06 09:47:51 +02:00
cr
This commit is contained in:
@@ -26,6 +26,11 @@ pip install langgraph
|
||||
Here we will go over an example of recreating the [`AgentExecutor`](https://python.langchain.com/docs/modules/agents/concepts#agentexecutor) class from LangChain.
|
||||
The benefits of creating it with LangGraph is that it is more modifiable.
|
||||
|
||||
We will also want to install some LangChain packages, as well as [Tavily](https://app.tavily.com/sign-in) to use as an example tool.
|
||||
|
||||
```shell
|
||||
pip install -U langchain langchain_openai langchainhub tavily-python
|
||||
```
|
||||
### Define the LangChain Agent
|
||||
|
||||
This is the LangChain agent.
|
||||
@@ -33,10 +38,9 @@ Crucially, this agent is just responsible for deciding what actions to take.
|
||||
For more information on what is happening here, please see [this documentation](https://python.langchain.com/docs/modules/agents/quick_start).
|
||||
|
||||
```python
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain import hub
|
||||
from langchain.agents import create_openai_functions_agent
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langchain_openai.chat_models import ChatOpenAI
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
|
||||
tools = [TavilySearchResults(max_results=1)]
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+392
-391
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
{question}
|
||||
</question>
|
||||
|
||||
In order to do that, you first came up with several sub questions and researched those. please find those below:
|
||||
|
||||
<research>
|
||||
{research}
|
||||
</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})
|
||||
@@ -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>{question}</question><search_results>{search_results}</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)
|
||||
@@ -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
|
||||
@@ -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>{question}</question><search_results>{search_results}</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<?, ?it/s]\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:00<00:00, 6.75it/s]\u001b[A\n",
|
||||
"\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:01<00:00, 2.99it/s]\u001b[A\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[[{'answer': 'LangSmith is a platform that helps developers build production-grade language model applications and provides tools for testing, evaluating, and monitoring these applications. It is built by the developers of LangChain and integrates seamlessly with that library. LangSmith aims to address the challenges of moving LLM applications from prototypes to production, ensuring reliability and maintainability. It offers features such as tracing, testing, and evaluating prompts and answers generated by language models. For more information, you can refer to the LangSmith documentation.'}],\n",
|
||||
" [{'answer': 'According to the search results, a llama is a domesticated livestock species that is a descendant of the guanaco and belongs to the camel family. Llamas are primarily used as pack animals and a source of food, wool, hides, tallow, and dried dung. They are found in South American countries such as Bolivia, Peru, Colombia, Ecuador, Chile, and Argentina. Llamas are known for their long necks, long legs, small heads, and large pointed ears. They are gregarious animals that graze on grass and other plants. Llamas can interbreed with other lamoid species and produce fertile offspring. On the other hand, alpacas are smaller than llamas, have different face shapes and hair textures, and are primarily used for fleece production.'}]]"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"web_researcher.batch([\"what is langsmith\", \"what is llama\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5952e9b8-2a56-4d2d-997a-dceb7652186f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Trying to use it as a sub component"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"id": "43ca019d-a500-4c77-8e62-a46e54ffae7d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.output_parsers.openai_functions import JsonKeyOutputFunctionsParser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"id": "858a82ae-a73f-4da2-9210-298af789ea30",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"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}\"\"\"\n",
|
||||
"functions = [\n",
|
||||
" {\n",
|
||||
" \"name\": \"sub_questions\",\n",
|
||||
" \"description\": \"List of sub questions\",\n",
|
||||
" \"parameters\": {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\n",
|
||||
" \"questions\": {\n",
|
||||
" \"type\": \"array\",\n",
|
||||
" \"description\": \"List of sub questions to ask.\",\n",
|
||||
" \"items\": {\"type\": \"string\"},\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
"]\n",
|
||||
"prompt = ChatPromptTemplate.from_template(template)\n",
|
||||
"question_chain = (\n",
|
||||
" prompt\n",
|
||||
" | ChatOpenAI(temperature=0).bind(\n",
|
||||
" functions=functions, function_call={\"name\": \"sub_questions\"}\n",
|
||||
" )\n",
|
||||
" | JsonKeyOutputFunctionsParser(key_name=\"questions\")\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"id": "c0298fdc-0e7c-4e79-9cb7-cdd4d50fe88f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['What is the purpose of Langsmith?',\n",
|
||||
" 'Who developed Langsmith?',\n",
|
||||
" 'What are the features of Langsmith?',\n",
|
||||
" 'How does Langsmith work?',\n",
|
||||
" 'Are there any alternatives to Langsmith?']"
|
||||
]
|
||||
},
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"question_chain.invoke({\"question\": \"what is langsmith?\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "a0101bd0-cd95-4b13-ab26-d5d34d703bf9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"template = \"\"\"You are tasked with writing a research report to answer the following question:\n",
|
||||
"\n",
|
||||
"<question>\n",
|
||||
"{question}\n",
|
||||
"</question>\n",
|
||||
"\n",
|
||||
"In order to do that, you first came up with several sub questions and researched those. please find those below:\n",
|
||||
"\n",
|
||||
"<research>\n",
|
||||
"{research}\n",
|
||||
"</research>\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<?, ?it/s]\n",
|
||||
"Fetching pages: 0%| | 0/4 [00:00<?, ?it/s]\u001b[A\n",
|
||||
"\n",
|
||||
"Fetching pages: 0%| | 0/4 [00:00<?, ?it/s]\u001b[A\u001b[A\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Fetching pages: 0%| | 0/4 [00:00<?, ?it/s]\u001b[A\u001b[A\u001b[A\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:00<00:00, 5.71it/s]\u001b[A\u001b[A\u001b[A\u001b[A\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:00<00:00, 4.80it/s]\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:01<00:00, 2.77it/s]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:01<00:00, 2.70it/s]\u001b[A\u001b[A\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Fetching pages: 100%|###################################################| 4/4 [00:03<00:00, 1.22it/s]\u001b[A\u001b[A\u001b[A\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['Research Report: Understanding LangSmith\\n\\nIntroduction:\\nThe purpose of this research report is to explore and provide insights into the topic of LangSmith. LangSmith is a unified platform that aims to address the challenges developers face when building and deploying language model applications in production environments. By examining various sources, we have gathered information to answer the question, \"What is LangSmith?\"\\n\\nFindings:\\n\\n1. LangSmith\\'s Purpose and Features:\\nLangSmith is designed to assist developers in transitioning from prototype to production with their language model applications. It offers a range of features to trace, test, evaluate, and monitor LLM (Large Language Model) calls for production. The platform is part of the LangChain ecosystem and provides reliable and maintainable solutions for language model applications [1].\\n\\n2. Development and Integration:\\nLangSmith was developed by the same team that created LangChain, the popular language model software tool. With a focus on reliability and maintainability, LangSmith seamlessly integrates with LangChain, enabling developers to efficiently build production-grade LLM applications [2].\\n\\n3. Functionalities and Benefits:\\nLangSmith serves as a unified platform for debugging, testing, and monitoring language model applications. It aids developers in prototyping LLM applications and Agents, facilitating customization and iteration on prompts, chains, and other components. Additionally, LangSmith allows for quick debugging of new chains and agents, visualizes component relationships, evaluates prompts and LLMs, and captures usage traces for generating insights. It also provides benchmarking features to evaluate LLM applications [3].\\n\\n4. Availability and Documentation:\\nLangSmith is currently in beta and periodically allows access to new sign-ups. The platform offers documentation and walkthroughs to guide users through its features, making it easier for developers to utilize LangSmith effectively [3].\\n\\n5. Alternatives to LangSmith:\\nBased on our research, we identified several potential alternatives to LangSmith that offer similar functionalities. These alternatives include LangChain, GradientJ, Vellum, Llama 2, Openlayer, Backengine, Query Vary, and BenchLLM. Each alternative provides different tools and features to support the development, testing, and monitoring of language model applications [4].\\n\\nConclusion:\\nLangSmith is a unified platform developed by the creators of LangChain to address the challenges of building and deploying language model applications in production. It offers tracing, testing, evaluating, and monitoring features for LLM applications. By providing documentation and a walkthrough, LangSmith helps developers transition from prototyping to production. However, it is essential to consider alternative platforms based on specific requirements and needs.\\n\\nReferences:\\n1. [1] LangSmith: A unified platform for language model applications. Retrieved from [source 1].\\n2. [2] LangSmith: Tackling challenges in LLM application development. Retrieved from [source 2].\\n3. [3] Exploring the functionalities of LangSmith. Retrieved from [source 3].\\n4. [4] Alternatives to LangSmith. Retrieved from [source 4].\\n\\nPlease note that the sources mentioned above have not been provided and should be replaced with the actual sources used for research.']"
|
||||
]
|
||||
},
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"longer_researcher.invoke({\"question\": \"what is langsmith?\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "fde8b227-64ec-4e96-b337-fc888e7ad787",
|
||||
"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.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
+2
-2
@@ -4,8 +4,8 @@ from langchain.vectorstores import FAISS
|
||||
from langchain_core.messages import AIMessage, AnyMessage, FunctionMessage
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from permchain import Channel, Pregel
|
||||
from permchain.channels import Topic
|
||||
from langgraph.channels import Topic
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
|
||||
texts = ["harrison went to kensho"]
|
||||
embeddings = OpenAIEmbeddings()
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from permchain import Channel, Pregel
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
|
||||
grow_value = (
|
||||
Channel.subscribe_to("value")
|
||||
|
||||
@@ -6,9 +6,9 @@ from langchain_core.documents import Document
|
||||
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
|
||||
from langchain_core.utils.html import extract_sub_links
|
||||
|
||||
from permchain import Channel, Pregel
|
||||
from permchain.channels.context import Context
|
||||
from permchain.channels.topic import Topic
|
||||
from langgraph.channels.context import Context
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.pregel import Channel, Pregel
|
||||
|
||||
# Load url with sync httpx client
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"LastValue",
|
||||
"Topic",
|
||||
"Context",
|
||||
"BinaryOperatorAggregate",
|
||||
]
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from permchain.checkpoint.base import Checkpoint
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
|
||||
Value = TypeVar("Value")
|
||||
Update = TypeVar("Update")
|
||||
@@ -3,7 +3,7 @@ from typing import Callable, Generator, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from permchain.channels.base import BaseChannel, EmptyChannelError, Value
|
||||
from langgraph.channels.base import BaseChannel, EmptyChannelError, Value
|
||||
|
||||
|
||||
class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from permchain.channels.base import (
|
||||
from langgraph.channels.base import (
|
||||
BaseChannel,
|
||||
EmptyChannelError,
|
||||
InvalidUpdateError,
|
||||
@@ -3,7 +3,7 @@ from typing import Generator, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from permchain.channels.base import (
|
||||
from langgraph.channels.base import (
|
||||
BaseChannel,
|
||||
EmptyChannelError,
|
||||
InvalidUpdateError,
|
||||
@@ -3,7 +3,7 @@ from typing import Any, Generator, Generic, Iterator, Optional, Sequence, Type,
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from permchain.channels.base import BaseChannel, Value
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
|
||||
|
||||
def flatten(values: Sequence[Value | list[Value]]) -> Iterator[Value]:
|
||||
@@ -0,0 +1,8 @@
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointAt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
__all__ = [
|
||||
"CheckpointAt",
|
||||
"BaseCheckpointSaver",
|
||||
"MemorySaver",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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):
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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(
|
||||
@@ -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):
|
||||
@@ -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(
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
+4
-4
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
+25
-23
@@ -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:
|
||||
@@ -161,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:
|
||||
@@ -176,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:
|
||||
@@ -186,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)
|
||||
@@ -204,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:
|
||||
@@ -219,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:
|
||||
@@ -229,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:
|
||||
@@ -243,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},
|
||||
]
|
||||
|
||||
@@ -377,7 +381,7 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None:
|
||||
| raise_if_above_10
|
||||
)
|
||||
|
||||
memory = MemoryCheckpoint()
|
||||
memory = MemorySaver()
|
||||
|
||||
app = Pregel(
|
||||
nodes={"one": one},
|
||||
@@ -492,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:
|
||||
@@ -565,8 +569,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:
|
||||
|
||||
+28
-23
@@ -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:
|
||||
@@ -162,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:
|
||||
@@ -177,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:
|
||||
@@ -187,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)
|
||||
@@ -205,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:
|
||||
@@ -220,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:
|
||||
@@ -230,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:
|
||||
@@ -245,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:
|
||||
@@ -385,7 +389,7 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None:
|
||||
| raise_if_above_10
|
||||
)
|
||||
|
||||
memory = MemoryCheckpoint()
|
||||
memory = MemorySaver()
|
||||
|
||||
app = Pregel(
|
||||
nodes={"one": one},
|
||||
@@ -507,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:
|
||||
@@ -593,8 +600,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:
|
||||
|
||||
Reference in New Issue
Block a user