This commit is contained in:
Harrison Chase
2024-01-07 20:23:16 -08:00
39 changed files with 537 additions and 1602 deletions
+6 -2
View File
@@ -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)]
+4 -4
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -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
View File
@@ -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
}
-384
View File
@@ -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
}
-97
View File
@@ -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)
-38
View File
@@ -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
-498
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
from permchain import Channel, Pregel
from langgraph.pregel import Channel, Pregel
grow_value = (
Channel.subscribe_to("value")
+3 -3
View File
@@ -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
+11
View File
@@ -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]:
+8
View File
@@ -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]
-12
View File
@@ -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",
]
-11
View File
@@ -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
View File
@@ -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"
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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: