Lesson 4: Persistence and Streaming¶
In [1]:
Copied!
from dotenv import load_dotenv
_ = load_dotenv()
from dotenv import load_dotenv _ = load_dotenv()
In [2]:
Copied!
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage from langchain_openai import ChatOpenAI from langchain_community.tools.tavily_search import TavilySearchResults
In [3]:
Copied!
tool = TavilySearchResults(max_results=2)
tool = TavilySearchResults(max_results=2)
In [4]:
Copied!
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
class AgentState(TypedDict): messages: Annotated[list[AnyMessage], operator.add]
In [5]:
Copied!
from langgraph.checkpoint.sqlite import SqliteSaver
memory = SqliteSaver.from_conn_string(":memory:")
from langgraph.checkpoint.sqlite import SqliteSaver memory = SqliteSaver.from_conn_string(":memory:")
In [6]:
Copied!
class Agent:
def __init__(self, model, tools, checkpointer, system=""):
self.system = system
graph = StateGraph(AgentState)
graph.add_node("llm", self.call_openai)
graph.add_node("action", self.take_action)
graph.add_conditional_edges("llm", self.exists_action, {True: "action", False: END})
graph.add_edge("action", "llm")
graph.set_entry_point("llm")
self.graph = graph.compile(checkpointer=checkpointer)
self.tools = {t.name: t for t in tools}
self.model = model.bind_tools(tools)
def call_openai(self, state: AgentState):
messages = state['messages']
if self.system:
messages = [SystemMessage(content=self.system)] + messages
message = self.model.invoke(messages)
return {'messages': [message]}
def exists_action(self, state: AgentState):
result = state['messages'][-1]
return len(result.tool_calls) > 0
def take_action(self, state: AgentState):
tool_calls = state['messages'][-1].tool_calls
results = []
for t in tool_calls:
print(f"Calling: {t}")
result = self.tools[t['name']].invoke(t['args'])
results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
print("Back to the model!")
return {'messages': results}
class Agent: def __init__(self, model, tools, checkpointer, system=""): self.system = system graph = StateGraph(AgentState) graph.add_node("llm", self.call_openai) graph.add_node("action", self.take_action) graph.add_conditional_edges("llm", self.exists_action, {True: "action", False: END}) graph.add_edge("action", "llm") graph.set_entry_point("llm") self.graph = graph.compile(checkpointer=checkpointer) self.tools = {t.name: t for t in tools} self.model = model.bind_tools(tools) def call_openai(self, state: AgentState): messages = state['messages'] if self.system: messages = [SystemMessage(content=self.system)] + messages message = self.model.invoke(messages) return {'messages': [message]} def exists_action(self, state: AgentState): result = state['messages'][-1] return len(result.tool_calls) > 0 def take_action(self, state: AgentState): tool_calls = state['messages'][-1].tool_calls results = [] for t in tool_calls: print(f"Calling: {t}") result = self.tools[t['name']].invoke(t['args']) results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result))) print("Back to the model!") return {'messages': results}
In [7]:
Copied!
prompt = """You are a smart research assistant. Use the search engine to look up information. \
You are allowed to make multiple calls (either together or in sequence). \
Only look up information when you are sure of what you want. \
If you need to look up some information before asking a follow up question, you are allowed to do that!
"""
model = ChatOpenAI(model="gpt-4o")
abot = Agent(model, [tool], system=prompt, checkpointer=memory)
prompt = """You are a smart research assistant. Use the search engine to look up information. \ You are allowed to make multiple calls (either together or in sequence). \ Only look up information when you are sure of what you want. \ If you need to look up some information before asking a follow up question, you are allowed to do that! """ model = ChatOpenAI(model="gpt-4o") abot = Agent(model, [tool], system=prompt, checkpointer=memory)
In [8]:
Copied!
messages = [HumanMessage(content="What is the weather in sf?")]
messages = [HumanMessage(content="What is the weather in sf?")]
In [9]:
Copied!
thread = {"configurable": {"thread_id": "1"}}
thread = {"configurable": {"thread_id": "1"}}
In [10]:
Copied!
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v['messages'])
for event in abot.graph.stream({"messages": messages}, thread): for v in event.values(): print(v['messages'])
[AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_AQa7oNKz1QqPrnk6zasAyVJe', 'function': {'arguments': '{"query":"current weather in San Francisco"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 151, 'total_tokens': 173}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-599e082a-65d4-4074-a91b-610ed453d2e1-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_AQa7oNKz1QqPrnk6zasAyVJe'}])] Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_AQa7oNKz1QqPrnk6zasAyVJe'} Back to the model! [ToolMessage(content='[{\'url\': \'https://www.wunderground.com/hourly/us/ca/san-francisco/KCASANFR2002/date/2024-6-20\', \'content\': \'Current Weather for Popular Cities . San Francisco, CA warning 53 ° F Clear; Manhattan, NY 66 ° F Partly Cloudy; Schiller Park, IL (60176) 61 ° F Fair; Boston, MA 67 ° F Cloudy; Houston, TX ...\'}, {\'url\': \'https://www.weatherapi.com/\', \'content\': "{\'location\': {\'name\': \'San Francisco\', \'region\': \'California\', \'country\': \'United States of America\', \'lat\': 37.78, \'lon\': -122.42, \'tz_id\': \'America/Los_Angeles\', \'localtime_epoch\': 1718875883, \'localtime\': \'2024-06-20 2:31\'}, \'current\': {\'last_updated_epoch\': 1718875800, \'last_updated\': \'2024-06-20 02:30\', \'temp_c\': 12.2, \'temp_f\': 54.0, \'is_day\': 0, \'condition\': {\'text\': \'Partly cloudy\', \'icon\': \'//cdn.weatherapi.com/weather/64x64/night/116.png\', \'code\': 1003}, \'wind_mph\': 6.9, \'wind_kph\': 11.2, \'wind_degree\': 280, \'wind_dir\': \'W\', \'pressure_mb\': 1013.0, \'pressure_in\': 29.92, \'precip_mm\': 0.0, \'precip_in\': 0.0, \'humidity\': 86, \'cloud\': 75, \'feelslike_c\': 11.1, \'feelslike_f\': 52.0, \'windchill_c\': 10.0, \'windchill_f\': 50.1, \'heatindex_c\': 11.5, \'heatindex_f\': 52.8, \'dewpoint_c\': 9.7, \'dewpoint_f\': 49.5, \'vis_km\': 16.0, \'vis_miles\': 9.0, \'uv\': 1.0, \'gust_mph\': 9.8, \'gust_kph\': 15.8}}"}]', name='tavily_search_results_json', tool_call_id='call_AQa7oNKz1QqPrnk6zasAyVJe')] [AIMessage(content='The current weather in San Francisco is 54°F (12.2°C) with partly cloudy conditions. The wind is blowing from the west at 6.9 mph (11.2 kph), and the humidity is at 86%. The visibility is 16 kilometers, and the UV index is 1.', response_metadata={'token_usage': {'completion_tokens': 65, 'prompt_tokens': 689, 'total_tokens': 754}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_9cb5d38cf7', 'finish_reason': 'stop', 'logprobs': None}, id='run-d87071d2-1776-4689-97e6-9351575db0d9-0')]
Using the thread_id we can acces the conversatioin in any time:
In [11]:
Copied!
messages = [HumanMessage(content="What about in la?")]
thread = {"configurable": {"thread_id": "1"}}
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
messages = [HumanMessage(content="What about in la?")] thread = {"configurable": {"thread_id": "1"}} for event in abot.graph.stream({"messages": messages}, thread): for v in event.values(): print(v)
{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_eCRPIoM4YT5Oi19NHIgJrIsO', 'function': {'arguments': '{"query":"current weather in Los Angeles"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 766, 'total_tokens': 788}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_5e6c71d4a8', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-e7e04b6a-bb58-4e09-9ddf-18ce65719250-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in Los Angeles'}, 'id': 'call_eCRPIoM4YT5Oi19NHIgJrIsO'}])]} Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'current weather in Los Angeles'}, 'id': 'call_eCRPIoM4YT5Oi19NHIgJrIsO'} Back to the model! {'messages': [ToolMessage(content="[{'url': 'https://www.weathertab.com/en/c/e/06/united-states/california/los-angeles/', 'content': 'Avg High Temps 75 to 85 °F. Avg Low Temps 50 to 60 °F. Explore comprehensive June 2024 weather forecasts for Los Angeles, including daily high and low temperatures, precipitation risks, and monthly temperature trends. Featuring detailed day-by-day forecasts, dynamic graphs of daily rain probabilities, and temperature trends to help you plan ...'}, {'url': 'https://forecast.weather.gov/zipcity.php?inputstring=Los Angeles,CA', 'content': 'Current conditions at LOS ANGELES DOWNTOWN (FHMC1) Lat: 34.06778°NLon: 118.24167°WElev: 413.0ft.'}]", name='tavily_search_results_json', tool_call_id='call_eCRPIoM4YT5Oi19NHIgJrIsO')]} {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_kMhq28EoaucoODMREw8vcBXK', 'function': {'arguments': '{"query":"current weather in Los Angeles"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 972, 'total_tokens': 994}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-360773b5-d521-4f0b-ac49-0c705be64a20-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in Los Angeles'}, 'id': 'call_kMhq28EoaucoODMREw8vcBXK'}])]} Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'current weather in Los Angeles'}, 'id': 'call_kMhq28EoaucoODMREw8vcBXK'} Back to the model! {'messages': [ToolMessage(content='[{\'url\': \'https://www.weathertab.com/en/c/e/06/united-states/california/los-angeles/\', \'content\': \'Avg High Temps 75 to 85 °F. Avg Low Temps 50 to 60 °F. Explore comprehensive June 2024 weather forecasts for Los Angeles, including daily high and low temperatures, precipitation risks, and monthly temperature trends. Featuring detailed day-by-day forecasts, dynamic graphs of daily rain probabilities, and temperature trends to help you plan ...\'}, {\'url\': \'https://www.weatherapi.com/\', \'content\': "{\'location\': {\'name\': \'Los Angeles\', \'region\': \'California\', \'country\': \'United States of America\', \'lat\': 34.05, \'lon\': -118.24, \'tz_id\': \'America/Los_Angeles\', \'localtime_epoch\': 1718875921, \'localtime\': \'2024-06-20 2:32\'}, \'current\': {\'last_updated_epoch\': 1718875800, \'last_updated\': \'2024-06-20 02:30\', \'temp_c\': 18.7, \'temp_f\': 65.6, \'is_day\': 0, \'condition\': {\'text\': \'Clear\', \'icon\': \'//cdn.weatherapi.com/weather/64x64/night/113.png\', \'code\': 1000}, \'wind_mph\': 2.9, \'wind_kph\': 4.7, \'wind_degree\': 203, \'wind_dir\': \'SSW\', \'pressure_mb\': 1009.0, \'pressure_in\': 29.8, \'precip_mm\': 0.0, \'precip_in\': 0.0, \'humidity\': 70, \'cloud\': 1, \'feelslike_c\': 18.7, \'feelslike_f\': 65.6, \'windchill_c\': 18.7, \'windchill_f\': 65.6, \'heatindex_c\': 18.7, \'heatindex_f\': 65.6, \'dewpoint_c\': 13.0, \'dewpoint_f\': 55.5, \'vis_km\': 10.0, \'vis_miles\': 6.0, \'uv\': 1.0, \'gust_mph\': 4.0, \'gust_kph\': 6.4}}"}]', name='tavily_search_results_json', tool_call_id='call_kMhq28EoaucoODMREw8vcBXK')]} {'messages': [AIMessage(content='The current weather in Los Angeles is 65.6°F (18.7°C) with clear skies. The wind is coming from the south-southwest at 2.9 mph (4.7 kph), and the humidity is at 70%. The visibility is 10 kilometers, and the UV index is 1.', response_metadata={'token_usage': {'completion_tokens': 69, 'prompt_tokens': 1514, 'total_tokens': 1583}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'stop', 'logprobs': None}, id='run-cc163793-b080-4b5b-8d78-32afd14c592a-0')]}
In [12]:
Copied!
messages = [HumanMessage(content="Which one is warmer?")]
thread = {"configurable": {"thread_id": "1"}}
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
messages = [HumanMessage(content="Which one is warmer?")] thread = {"configurable": {"thread_id": "1"}} for event in abot.graph.stream({"messages": messages}, thread): for v in event.values(): print(v)
{'messages': [AIMessage(content='Los Angeles is currently warmer at 65.6°F (18.7°C), compared to San Francisco, which is at 54°F (12.2°C).', response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 1595, 'total_tokens': 1630}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'stop', 'logprobs': None}, id='run-89a16322-95b8-4d97-b399-ef112189427a-0')]}
Changing the thread id, we are not giving the agent the old messages
In [13]:
Copied!
messages = [HumanMessage(content="Which one is warmer?")]
thread = {"configurable": {"thread_id": "2"}}
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
messages = [HumanMessage(content="Which one is warmer?")] thread = {"configurable": {"thread_id": "2"}} for event in abot.graph.stream({"messages": messages}, thread): for v in event.values(): print(v)
{'messages': [AIMessage(content='Could you please provide more context or specify the items or locations you are comparing in terms of warmth? This will help me provide a more accurate answer.', response_metadata={'token_usage': {'completion_tokens': 31, 'prompt_tokens': 149, 'total_tokens': 180}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'stop', 'logprobs': None}, id='run-74ba27bc-56bc-4ce9-9b19-943f5f9a02bc-0')]}
In [14]:
Copied!
messages = [HumanMessage(content="How many kcalories I have in 200gr of chicken and 130gr pasta?")]
thread = {"configurable": {"thread_id": "3"}}
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
messages = [HumanMessage(content="How many kcalories I have in 200gr of chicken and 130gr pasta?")] thread = {"configurable": {"thread_id": "3"}} for event in abot.graph.stream({"messages": messages}, thread): for v in event.values(): print(v)
{'messages': [AIMessage(content="To accurately determine the caloric content of your meal, we need to consider the nutritional information for both chicken and pasta.\n\n1. **Chicken (cooked, skinless, boneless)**\n2. **Pasta (uncooked, dry weight)**\n\nI'll look up the average caloric content for both chicken and pasta per gram and then calculate the total calories for your specified quantities.", additional_kwargs={'tool_calls': [{'id': 'call_fyqQmeN7fuoDkjZ7SpZlGhj2', 'function': {'arguments': '{"query": "calories in 100 grams of cooked chicken"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}, {'id': 'call_IqA9TxNKiy4KtNGjxom0vHzj', 'function': {'arguments': '{"query": "calories in 100 grams of dry pasta"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 147, 'prompt_tokens': 162, 'total_tokens': 309}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_9cb5d38cf7', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-eb4d2438-2d6b-4c0e-9f18-e452a42cff20-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'calories in 100 grams of cooked chicken'}, 'id': 'call_fyqQmeN7fuoDkjZ7SpZlGhj2'}, {'name': 'tavily_search_results_json', 'args': {'query': 'calories in 100 grams of dry pasta'}, 'id': 'call_IqA9TxNKiy4KtNGjxom0vHzj'}])]} Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'calories in 100 grams of cooked chicken'}, 'id': 'call_fyqQmeN7fuoDkjZ7SpZlGhj2'} Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'calories in 100 grams of dry pasta'}, 'id': 'call_IqA9TxNKiy4KtNGjxom0vHzj'} Back to the model! {'messages': [ToolMessage(content="[{'url': 'https://www.nutritionix.com/food/chicken-breasts/100-g', 'content': 'Chicken Breasts. Amount Per Serving. Calories 165. % Daily Value*. Total Fat 3.6g 5%. Saturated Fat 1g 5%. Polyunsaturated Fat 0.8g. Monounsaturated Fat 1.2g. Cholesterol 85mg 28%.'}, {'url': 'https://www.fatsecret.com/calories-nutrition/generic/chicken-breast-ns-as-to-skin-eaten?portionid=50321&portionamount=100.000', 'content': 'There are 195 calories in 100 grams of Chicken Breast. Calorie breakdown: 37% fat, 0% carbs, 63% protein. Other Common Serving Sizes: Serving Size Calories; 1 oz raw (yield after cooking, bone removed) 29: 1 oz boneless (yield after cooking) 35: 1 oz, with bone cooked (yield after bone removed) 47:'}]", name='tavily_search_results_json', tool_call_id='call_fyqQmeN7fuoDkjZ7SpZlGhj2'), ToolMessage(content="[{'url': 'https://www.nutritionix.com/food/uncooked-pasta/100-g', 'content': 'Uncooked Pasta. Amount Per Serving. Calories 371. % Daily Value*. Total Fat 1.5g 2%. Saturated Fat 0.3g 1%. Trans Fat 0g. Polyunsaturated Fat 0.6g. Monounsaturated Fat 0.2g.'}, {'url': 'https://www.fatsecret.com/calories-nutrition/usda/dry-spaghetti?portionid=62549&portionamount=100.000', 'content': 'There are 371 calories in 100 grams of Dry Spaghetti. Calorie breakdown: 4% fat, 82% carbs, 14% protein. Other Common Serving Sizes: Serving Size Calories; 2 oz: 211: 100 g: 371: ... Enriched Dry Pasta : Dry Spaghetti (Protein Fortified, Enriched) view more results: Used in these Member Recipes: Vegetarian Spaghetti Bolognaise:'}]", name='tavily_search_results_json', tool_call_id='call_IqA9TxNKiy4KtNGjxom0vHzj')]} {'messages': [AIMessage(content="Based on the nutritional information I found:\n\n1. **Chicken (cooked, skinless, boneless)**\n - Calories: Approximately 165-195 kcal per 100 grams.\n2. **Pasta (uncooked, dry weight)**\n - Calories: Approximately 371 kcal per 100 grams.\n\nLet's take the average values for calculation:\n- Chicken: 180 kcal per 100 grams\n- Pasta: 371 kcal per 100 grams\n\n### Calculations:\n1. **Chicken:**\n - For 200 grams: \\( 200 \\, \\text{grams} \\times \\frac{180 \\, \\text{kcal}}{100 \\, \\text{grams}} = 360 \\, \\text{kcal} \\)\n\n2. **Pasta:**\n - For 130 grams: \\( 130 \\, \\text{grams} \\times \\frac{371 \\, \\text{kcal}}{100 \\, \\text{grams}} = 482.3 \\, \\text{kcal} \\)\n\n### Total Calories:\n- Chicken: 360 kcal\n- Pasta: 482.3 kcal\n\n**Total: 360 + 482.3 = 842.3 kcal**\n\nSo, in 200 grams of chicken and 130 grams of pasta, you have approximately **842.3 kcal**.", response_metadata={'token_usage': {'completion_tokens': 278, 'prompt_tokens': 776, 'total_tokens': 1054}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_f4e629d0a5', 'finish_reason': 'stop', 'logprobs': None}, id='run-7b3e1438-f3c0-4a22-baee-bdc4cade244f-0')]}
Streaming tokens¶
In [15]:
Copied!
from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver
memory = AsyncSqliteSaver.from_conn_string(":memory:")
abot = Agent(model, [tool], system=prompt, checkpointer=memory)
from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver memory = AsyncSqliteSaver.from_conn_string(":memory:") abot = Agent(model, [tool], system=prompt, checkpointer=memory)
In [16]:
Copied!
messages = [HumanMessage(content="What is the weather in SF?")]
thread = {"configurable": {"thread_id": "4"}}
async for event in abot.graph.astream_events({"messages": messages}, thread, version="v1"):
kind = event["event"]
if kind == "on_chat_model_stream":
content = event["data"]["chunk"].content
if content:
# Empty content in the context of OpenAI means
# that the model is asking for a tool to be invoked.
# So we only print non-empty content
print(content, end="|")
messages = [HumanMessage(content="What is the weather in SF?")] thread = {"configurable": {"thread_id": "4"}} async for event in abot.graph.astream_events({"messages": messages}, thread, version="v1"): kind = event["event"] if kind == "on_chat_model_stream": content = event["data"]["chunk"].content if content: # Empty content in the context of OpenAI means # that the model is asking for a tool to be invoked. # So we only print non-empty content print(content, end="|")
/usr/local/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future. warn_beta(
Calling: {'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_4fwP2pqpuLDwSDfzXXrTB3Zb'} Back to the model! The| current| weather| in| San| Francisco|,| CA| is| clear| with| a| temperature| of| |53|°F|.|
In [ ]:
Copied!
Last update: 2024-10-23
Created: 2024-10-23
Created: 2024-10-23