From 426a5c1b10de1fc81db1efa644703ba9f5c38985 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 12 Jan 2024 17:55:19 -0800 Subject: [PATCH 01/25] Implement StateGraph --- langgraph/channels/binop.py | 8 +- langgraph/graph/__init__.py | 135 +------------------------ langgraph/graph/graph.py | 128 ++++++++++++++++++++++++ langgraph/graph/state.py | 117 ++++++++++++++++++++++ langgraph/pregel/read.py | 16 ++- tests/test_pregel.py | 191 +++++++++++++++++++++++++++++++++++- 6 files changed, 456 insertions(+), 139 deletions(-) create mode 100644 langgraph/graph/graph.py create mode 100644 langgraph/graph/state.py diff --git a/langgraph/channels/binop.py b/langgraph/channels/binop.py index 6ec62fcb9..962ce7140 100644 --- a/langgraph/channels/binop.py +++ b/langgraph/channels/binop.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from typing import Callable, Generator, Generic, Optional, Sequence, Type +from typing import Annotated, Callable, Generator, Generic, Optional, Sequence, Type from typing_extensions import Self @@ -16,7 +16,11 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): ``` """ - def __init__(self, typ: Type[Value], operator: Callable[[Value, Value], Value]): + def __init__( + self, + typ: Type[Value], + operator: Callable[[Value, Value], Value], + ): self.typ = typ self.operator = operator try: diff --git a/langgraph/graph/__init__.py b/langgraph/graph/__init__.py index 4d5393c5a..1173035b5 100644 --- a/langgraph/graph/__init__.py +++ b/langgraph/graph/__init__.py @@ -1,133 +1,4 @@ -from asyncio import iscoroutinefunction -from collections import defaultdict -from typing import Any, Callable, Dict, NamedTuple +from langgraph.graph.graph import END, Graph +from langgraph.graph.state import StateGraph -from langchain_core.runnables import Runnable -from langchain_core.runnables.base import ( - RunnableLambda, - RunnableLike, - coerce_to_runnable, -) - -from langgraph.pregel import Channel, Pregel - -END = "__end__" - - -class Branch(NamedTuple): - condition: Callable[..., str] - ends: dict[str, str] - - def runnable(self, input: Any) -> Runnable: - result = self.condition(input) - destination = self.ends[result] - return Channel.write_to(f"{destination}:inbox" if destination != END else END) - - -class Graph: - def __init__(self) -> None: - self.nodes: dict[str, Runnable] = {} - self.edges = set[tuple[str, str]]() - self.branches: defaultdict[str, list[Branch]] = defaultdict(list) - - def add_node(self, key: str, action: RunnableLike) -> None: - if key in self.nodes: - raise ValueError(f"Node `{key}` already present.") - if key == END: - raise ValueError(f"Node `{key}` is reserved.") - - self.nodes[key] = coerce_to_runnable(action) - - def add_edge(self, start_key: str, end_key: str) -> None: - if start_key == END: - raise ValueError("END cannot be a start node") - if start_key not in self.nodes: - raise ValueError(f"Need to add_node `{start_key}` first") - if end_key not in self.nodes and end_key != END: - raise ValueError(f"Need to add_node `{end_key}` first") - - # TODO: support multiple message passing - if start_key in set(start for start, _ in self.edges): - raise ValueError(f"Already found path for {start_key}") - - self.edges.add((start_key, end_key)) - - def add_conditional_edges( - self, - start_key: str, - condition: Callable[..., str], - conditional_edge_mapping: Dict[str, str], - ) -> None: - if start_key not in self.nodes: - raise ValueError(f"Need to add_node `{start_key}` first") - if iscoroutinefunction(condition): - raise ValueError("Condition cannot be a coroutine function") - for destination in conditional_edge_mapping.values(): - if destination not in self.nodes and destination != END: - raise ValueError(f"Need to add_node `{destination}` first") - - self.branches[start_key].append(Branch(condition, conditional_edge_mapping)) - - def set_entry_point(self, key: str) -> None: - if key not in self.nodes: - raise ValueError(f"Need to add_node `{key}` first") - self.entry_point = key - - def set_finish_point(self, key: str) -> None: - return self.add_edge(key, END) - - def compile(self) -> Pregel: - ################################################ - # STEP 1: VALIDATE GRAPH STRUCTURE # - ################################################ - - all_starts = {src for src, _ in self.edges} | {src for src in self.branches} - all_ends = ( - {end for _, end in self.edges} - | { - end - for branch_list in self.branches.values() - for branch in branch_list - for end in branch.ends.values() - } - | {self.entry_point} - ) - - for node in self.nodes: - if node not in all_ends: - raise ValueError(f"Node `{node}` is not reachable") - if node not in all_starts: - raise ValueError(f"Node `{node}` is a dead-end") - - ################################################ - # STEP 2: CREATE GRAPH # - ################################################ - - outgoing_edges = defaultdict(list) - for start, end in self.edges: - outgoing_edges[start].append(f"{end}:inbox" if end != END else END) - - nodes = { - key: (Channel.subscribe_to(f"{key}:inbox") | node | Channel.write_to(key)) - for key, node in self.nodes.items() - } - - for key in self.nodes: - outgoing = outgoing_edges[key] - edges_key = f"{key}:edges" - if outgoing or key in self.branches: - nodes[edges_key] = Channel.subscribe_to(key) - if outgoing: - nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) - if key in self.branches: - for branch in self.branches[key]: - nodes[edges_key] |= RunnableLambda( - branch.runnable, name=f"{key}_condition" - ) - - return Pregel( - nodes=nodes, - input=f"{self.entry_point}:inbox", - output=END, - hidden=[f"{node}:inbox" for node in self.nodes], - ) +__all__ = ["END", "Graph", "StateGraph"] diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py new file mode 100644 index 000000000..4caac7a16 --- /dev/null +++ b/langgraph/graph/graph.py @@ -0,0 +1,128 @@ +from asyncio import iscoroutinefunction +from collections import defaultdict +from typing import Any, Callable, Dict, NamedTuple + +from langchain_core.runnables import Runnable +from langchain_core.runnables.base import ( + RunnableLambda, + RunnableLike, + coerce_to_runnable, +) + +from langgraph.pregel import Channel, Pregel + +END = "__end__" + + +class Branch(NamedTuple): + condition: Callable[..., str] + ends: dict[str, str] + + def runnable(self, input: Any) -> Runnable: + result = self.condition(input) + destination = self.ends[result] + return Channel.write_to(f"{destination}:inbox" if destination != END else END) + + +class Graph: + def __init__(self) -> None: + self.nodes: dict[str, Runnable] = {} + self.edges = set[tuple[str, str]]() + self.branches: defaultdict[str, list[Branch]] = defaultdict(list) + + def add_node(self, key: str, action: RunnableLike) -> None: + if key in self.nodes: + raise ValueError(f"Node `{key}` already present.") + if key == END: + raise ValueError(f"Node `{key}` is reserved.") + + self.nodes[key] = coerce_to_runnable(action) + + def add_edge(self, start_key: str, end_key: str) -> None: + if start_key == END: + raise ValueError("END cannot be a start node") + if start_key not in self.nodes: + raise ValueError(f"Need to add_node `{start_key}` first") + if end_key not in self.nodes and end_key != END: + raise ValueError(f"Need to add_node `{end_key}` first") + + # TODO: support multiple message passing + if start_key in set(start for start, _ in self.edges): + raise ValueError(f"Already found path for {start_key}") + + self.edges.add((start_key, end_key)) + + def add_conditional_edges( + self, + start_key: str, + condition: Callable[..., str], + conditional_edge_mapping: Dict[str, str], + ) -> None: + if start_key not in self.nodes: + raise ValueError(f"Need to add_node `{start_key}` first") + if iscoroutinefunction(condition): + raise ValueError("Condition cannot be a coroutine function") + for destination in conditional_edge_mapping.values(): + if destination not in self.nodes and destination != END: + raise ValueError(f"Need to add_node `{destination}` first") + + self.branches[start_key].append(Branch(condition, conditional_edge_mapping)) + + def set_entry_point(self, key: str) -> None: + if key not in self.nodes: + raise ValueError(f"Need to add_node `{key}` first") + self.entry_point = key + + def set_finish_point(self, key: str) -> None: + return self.add_edge(key, END) + + def validate(self) -> None: + all_starts = {src for src, _ in self.edges} | {src for src in self.branches} + all_ends = ( + {end for _, end in self.edges} + | { + end + for branch_list in self.branches.values() + for branch in branch_list + for end in branch.ends.values() + } + | {self.entry_point} + ) + + for node in self.nodes: + if node not in all_ends: + raise ValueError(f"Node `{node}` is not reachable") + if node not in all_starts: + raise ValueError(f"Node `{node}` is a dead-end") + + def compile(self) -> Pregel: + self.validate() + + outgoing_edges = defaultdict(list) + for start, end in self.edges: + outgoing_edges[start].append(f"{end}:inbox" if end != END else END) + + nodes = { + key: (Channel.subscribe_to(f"{key}:inbox") | node | Channel.write_to(key)) + for key, node in self.nodes.items() + } + + for key in self.nodes: + outgoing = outgoing_edges[key] + edges_key = f"{key}:edges" + if outgoing or key in self.branches: + nodes[edges_key] = Channel.subscribe_to(key) + if outgoing: + nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) + if key in self.branches: + for branch in self.branches[key]: + nodes[edges_key] |= RunnableLambda( + branch.runnable, name=f"{key}_condition" + ) + + return Pregel( + nodes=nodes, + input=f"{self.entry_point}:inbox", + output=END, + hidden=[f"{node}:inbox" for node in self.nodes], + ) diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py new file mode 100644 index 000000000..c3f58edb6 --- /dev/null +++ b/langgraph/graph/state.py @@ -0,0 +1,117 @@ +from collections import defaultdict +from functools import partial +from inspect import signature +from typing import Any, Optional, Type + +from langchain_core.runnables import RunnableConfig, RunnableLambda + +from langgraph.channels.base import BaseChannel +from langgraph.channels.binop import BinaryOperatorAggregate +from langgraph.channels.last_value import LastValue +from langgraph.graph.graph import END, Graph +from langgraph.pregel import Channel, Pregel +from langgraph.pregel.read import ChannelRead +from langgraph.pregel.write import ChannelWrite + +START = "__start__" + + +class StateGraph(Graph): + def __init__(self, schema: Type[Any]) -> None: + super().__init__() + self.schema = schema + self.channels = _get_channels(schema) + + def compile(self) -> Pregel: + self.validate() + + if any(key in self.nodes for key in self.channels): + raise ValueError("Cannot use channel names as node names") + + state_keys = list(self.channels) + + outgoing_edges = defaultdict(list) + for start, end in self.edges: + outgoing_edges[start].append(f"{end}:inbox" if end != END else END) + + nodes = { + key: ( + Channel.subscribe_to(f"{key}:inbox") + | partial(_coerce_state, self.schema) # coerce/validate using schema + | node + | _update_state + | Channel.write_to(key) + ) + for key, node in self.nodes.items() + } + + for key in self.nodes: + outgoing = outgoing_edges[key] + edges_key = f"{key}:edges" + if outgoing or key in self.branches: + nodes[edges_key] = Channel.subscribe_to(key) | ChannelRead(state_keys) + if outgoing: + nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) + if key in self.branches: + for branch in self.branches[key]: + nodes[edges_key] |= RunnableLambda( + branch.runnable, name=f"{key}_condition" + ) + + nodes[START] = ( + Channel.subscribe_to(f"{START}:inbox") + | _update_state + | Channel.write_to(START) + ) + nodes[f"{START}:edges"] = ( + Channel.subscribe_to(START) + | ChannelRead(state_keys) + | Channel.write_to(f"{self.entry_point}:inbox") + ) + + return Pregel( + nodes=nodes, + channels=self.channels, + input=f"{START}:inbox", + output=END, + hidden=[f"{node}:inbox" for node in self.nodes] + [START] + state_keys, + ) + + +def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]: + return schema(**input) + + +def _update_state(input: dict[str, Any], config: RunnableConfig): + ChannelWrite.do_write(config, **input) + return input + + +def _get_channels(schema: Type[dict]) -> dict[str, BaseChannel]: + if not hasattr(schema, "__annotations__"): + raise ValueError("Schema must be a class with type annotations") + + channels: dict[str, BaseChannel] = {} + for name, typ in schema.__annotations__.items(): + if channel := _is_field_binop(typ): + channels[name] = channel + else: + channels[name] = LastValue(typ) + + return channels + + +def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: + if hasattr(typ, "__metadata__"): + meta = typ.__metadata__ + if len(meta) == 1 and callable(meta[0]): + sig = signature(meta[0]) + params = list(sig.parameters.values()) + if len(params) == 2 and len( + [ + p + for p in params + if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) + ] + ): + return BinaryOperatorAggregate(typ, meta[0]) diff --git a/langgraph/pregel/read.py b/langgraph/pregel/read.py index 33fadf46c..11c1a050b 100644 --- a/langgraph/pregel/read.py +++ b/langgraph/pregel/read.py @@ -23,7 +23,7 @@ from langgraph.constants import CONFIG_KEY_READ class ChannelRead(RunnableLambda): - channel: str + channel: Union[str, list[str]] @property def config_specs(self) -> list[ConfigurableFieldSpec]: @@ -37,7 +37,7 @@ class ChannelRead(RunnableLambda): ), ] - def __init__(self, channel: str) -> None: + def __init__(self, channel: Union[str, list[str]]) -> None: super().__init__(func=self._read, afunc=self._aread) self.channel = channel self.name = f"ChannelRead<{channel}>" @@ -50,7 +50,11 @@ class ChannelRead(RunnableLambda): f"Runnable {self} is not configured with a read function" "Make sure to call in the context of a Pregel process" ) - return read(self.channel) + return ( + read(self.channel) + if isinstance(self.channel, str) + else {chan: read(chan) for chan in self.channel} + ) async def _aread(self, _: Any, config: RunnableConfig) -> Any: try: @@ -60,7 +64,11 @@ class ChannelRead(RunnableLambda): f"Runnable {self} is not configured with a read function" "Make sure to call in the context of a Pregel process" ) - return read(self.channel) + return ( + read(self.channel) + if isinstance(self.channel, str) + else {chan: read(chan) for chan in self.channel} + ) default_bound: RunnablePassthrough = RunnablePassthrough() diff --git a/tests/test_pregel.py b/tests/test_pregel.py index cccb64fce..a56e641f2 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -2,7 +2,7 @@ import operator import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager -from typing import Generator +from typing import Annotated, Generator, TypedDict import pytest from langchain_core.runnables import RunnablePassthrough @@ -15,6 +15,7 @@ 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.graph.state import StateGraph from langgraph.pregel import Channel, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -771,3 +772,191 @@ def test_conditional_graph() -> None: } }, ] + + +def test_conditional_graph_state() -> None: + from copy import deepcopy + + from langchain.llms.fake import FakeStreamingListLLM + from langchain_community.tools import tool + from langchain_core.agents import AgentAction, AgentFinish + from langchain_core.prompts import PromptTemplate + + class AgentState(TypedDict): + input: str + agent_outcome: AgentAction | AgentFinish | None + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + # Assemble the tools + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + # Construct the agent + prompt = PromptTemplate.from_template("Hello!") + + llm = FakeStreamingListLLM( + responses=[ + "tool:search_api:query", + "tool:search_api:another", + "finish:answer", + ] + ) + + def agent_parser(input: str) -> AgentFinish | AgentAction: + if input.startswith("finish"): + _, answer = input.split(":") + return { + "agent_outcome": AgentFinish( + return_values={"answer": answer}, log=input + ) + } + else: + _, tool_name, tool_input = input.split(":") + return { + "agent_outcome": AgentAction( + tool=tool_name, tool_input=tool_input, log=input + ) + } + + agent = prompt | llm | agent_parser + + # Define tool execution logic + def execute_tools(data: AgentState) -> dict: + agent_action: AgentAction = data.pop("agent_outcome") + observation = {t.name: t for t in tools}[agent_action.tool].invoke( + agent_action.tool_input + ) + return {"intermediate_steps": [(agent_action, observation)]} + + # Define decision-making logic + def should_continue(data: AgentState) -> str: + # Logic to decide whether to continue in the loop or exit + if isinstance(data["agent_outcome"], AgentFinish): + return "exit" + else: + return "continue" + + # Define a new graph + workflow = StateGraph(AgentState) + + workflow.add_node("agent", agent) + workflow.add_node("tools", execute_tools) + + workflow.set_entry_point("agent") + + workflow.add_conditional_edges( + "agent", should_continue, {"continue": "tools", "exit": END} + ) + + workflow.add_edge("tools", "agent") + + app = workflow.compile() + + assert app.invoke({"input": "what is weather in sf"}) == { + "input": "what is weather in sf", + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ), + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + + assert [deepcopy(c) for c in app.stream({"input": "what is weather in sf"})] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + } + }, + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + } + }, + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + } + }, + { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + }, + { + "__end__": { + "input": "what is weather in sf", + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ), + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + }, + ] From 38577136015382926138b567601cdbca6690fd78 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 12 Jan 2024 17:57:04 -0800 Subject: [PATCH 02/25] Lint --- langgraph/channels/binop.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/langgraph/channels/binop.py b/langgraph/channels/binop.py index 962ce7140..6ec62fcb9 100644 --- a/langgraph/channels/binop.py +++ b/langgraph/channels/binop.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from typing import Annotated, Callable, Generator, Generic, Optional, Sequence, Type +from typing import Callable, Generator, Generic, Optional, Sequence, Type from typing_extensions import Self @@ -16,11 +16,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): ``` """ - def __init__( - self, - typ: Type[Value], - operator: Callable[[Value, Value], Value], - ): + def __init__(self, typ: Type[Value], operator: Callable[[Value, Value], Value]): self.typ = typ self.operator = operator try: From 3d58aff93edb3f940bb152bed641f2e7ac642605 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sat, 13 Jan 2024 10:44:42 -0800 Subject: [PATCH 03/25] Add async test --- tests/test_pregel_async.py | 194 ++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 2 deletions(-) diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 3ee58d257..96adce78f 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -1,7 +1,7 @@ import asyncio import operator from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncGenerator, AsyncIterator, Generator +from typing import Annotated, Any, AsyncGenerator, AsyncIterator, Generator, TypedDict import pytest from langchain_core.runnables import RunnablePassthrough @@ -13,7 +13,7 @@ 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.graph import END, Graph, StateGraph from langgraph.pregel import Channel, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -817,3 +817,193 @@ async def test_conditional_graph() -> None: # Check that agent (one of the nodes) has its output streamed to the logs assert "/logs/agent/streamed_output/-" in patch_paths + + +async def test_conditional_graph_state() -> None: + from copy import deepcopy + + from langchain.llms.fake import FakeStreamingListLLM + from langchain_community.tools import tool + from langchain_core.agents import AgentAction, AgentFinish + from langchain_core.prompts import PromptTemplate + + class AgentState(TypedDict): + input: str + agent_outcome: AgentAction | AgentFinish | None + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + # Assemble the tools + @tool() + def search_api(query: str) -> str: + """Searches the API for the query.""" + return f"result for {query}" + + tools = [search_api] + + # Construct the agent + prompt = PromptTemplate.from_template("Hello!") + + llm = FakeStreamingListLLM( + responses=[ + "tool:search_api:query", + "tool:search_api:another", + "finish:answer", + ] + ) + + def agent_parser(input: str) -> AgentFinish | AgentAction: + if input.startswith("finish"): + _, answer = input.split(":") + return { + "agent_outcome": AgentFinish( + return_values={"answer": answer}, log=input + ) + } + else: + _, tool_name, tool_input = input.split(":") + return { + "agent_outcome": AgentAction( + tool=tool_name, tool_input=tool_input, log=input + ) + } + + agent = prompt | llm | agent_parser + + # Define tool execution logic + def execute_tools(data: AgentState) -> dict: + agent_action: AgentAction = data.pop("agent_outcome") + observation = {t.name: t for t in tools}[agent_action.tool].invoke( + agent_action.tool_input + ) + return {"intermediate_steps": [(agent_action, observation)]} + + # Define decision-making logic + def should_continue(data: AgentState) -> str: + # Logic to decide whether to continue in the loop or exit + if isinstance(data["agent_outcome"], AgentFinish): + return "exit" + else: + return "continue" + + # Define a new graph + workflow = StateGraph(AgentState) + + workflow.add_node("agent", agent) + workflow.add_node("tools", execute_tools) + + workflow.set_entry_point("agent") + + workflow.add_conditional_edges( + "agent", should_continue, {"continue": "tools", "exit": END} + ) + + workflow.add_edge("tools", "agent") + + app = workflow.compile() + + assert await app.ainvoke({"input": "what is weather in sf"}) == { + "input": "what is weather in sf", + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ), + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + + assert [ + deepcopy(c) async for c in app.astream({"input": "what is weather in sf"}) + ] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + } + }, + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + } + }, + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + } + }, + { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + }, + { + "__end__": { + "input": "what is weather in sf", + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ), + ( + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ), + ], + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + }, + ] From d68faa6d06b10eb5b0e5988516ae37a93d46e74e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sat, 13 Jan 2024 10:55:18 -0800 Subject: [PATCH 04/25] Update tracing name --- langgraph/pregel/__init__.py | 6 ++++-- tests/test_pregel.py | 24 ++++++++++++------------ tests/test_pregel_async.py | 24 ++++++++++++------------ 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 2521708ac..46baeb6d4 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -162,6 +162,8 @@ class Pregel( saver: Optional[BaseCheckpointSaver] = None + name: str = "LangGraph" + class Config: arbitrary_types_allowed = True @@ -191,7 +193,7 @@ class Pregel( return super().get_input_schema(config) else: return create_model( # type: ignore[call-overload] - "PregelInput", + self.get_name("Input"), **{ k: (self.channels[k].UpdateType, None) for k in self.input or self.channels.keys() @@ -210,7 +212,7 @@ class Pregel( return super().get_output_schema(config) else: return create_model( # type: ignore[call-overload] - "PregelOutput", + self.get_name("Output"), **{k: (self.channels[k].ValueType, None) for k in self.output}, ) diff --git a/tests/test_pregel.py b/tests/test_pregel.py index a56e641f2..ded25a5f5 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -41,8 +41,8 @@ def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: graph.set_finish_point("add_one") gapp = graph.compile() - assert app.input_schema.schema() == {"title": "PregelInput", "type": "integer"} - assert app.output_schema.schema() == {"title": "PregelOutput", "type": "integer"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} + assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} assert app.invoke(2) == 3 assert app.invoke(2, output=["output"]) == {"output": 3} assert repr(app), "does not raise recursion error" @@ -56,8 +56,8 @@ def test_invoke_single_process_in_out_implicit_channels(mocker: MockerFixture) - app = Pregel(nodes={"one": chain}) - assert app.input_schema.schema() == {"title": "PregelInput"} - assert app.output_schema.schema() == {"title": "PregelOutput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.output_schema.schema() == {"title": "LangGraphOutput"} assert app.invoke(2) == 3 @@ -71,9 +71,9 @@ def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> None: app = Pregel(nodes={"one": chain}, output=["output", "fixed", "output_plus_one"]) - assert app.input_schema.schema() == {"title": "PregelInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": { "output": {"title": "Output"}, @@ -95,8 +95,8 @@ def test_invoke_single_process_in_out_reserved_is_last(mocker: MockerFixture) -> app = Pregel(nodes={"one": chain}) - assert app.input_schema.schema() == {"title": "PregelInput"} - assert app.output_schema.schema() == {"title": "PregelOutput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.output_schema.schema() == {"title": "LangGraphOutput"} assert app.invoke(2) == {"input": 3, "is_last_step": False} assert app.invoke(2, {"recursion_limit": 1}) == {"input": 3, "is_last_step": True} @@ -112,9 +112,9 @@ def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: output=["output"], ) - assert app.input_schema.schema() == {"title": "PregelInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": {"output": {"title": "Output"}}, } @@ -134,12 +134,12 @@ def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> None: ) assert app.input_schema.schema() == { - "title": "PregelInput", + "title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input"}}, } assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": {"output": {"title": "Output"}}, } diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 96adce78f..3f94cf86d 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -39,8 +39,8 @@ async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: graph.set_finish_point("add_one") gapp = graph.compile() - assert app.input_schema.schema() == {"title": "PregelInput", "type": "integer"} - assert app.output_schema.schema() == {"title": "PregelOutput", "type": "integer"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} + assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} assert await app.ainvoke(2) == 3 assert await app.ainvoke(2, output=["output"]) == {"output": 3} @@ -55,8 +55,8 @@ async def test_invoke_single_process_in_out_implicit_channels( app = Pregel(nodes={"one": chain}) - assert app.input_schema.schema() == {"title": "PregelInput"} - assert app.output_schema.schema() == {"title": "PregelOutput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.output_schema.schema() == {"title": "LangGraphOutput"} assert await app.ainvoke(2) == 3 @@ -70,9 +70,9 @@ async def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> N app = Pregel(nodes={"one": chain}, output=["output", "fixed", "output_plus_one"]) - assert app.input_schema.schema() == {"title": "PregelInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": { "output": {"title": "Output"}, @@ -96,8 +96,8 @@ async def test_invoke_single_process_in_out_reserved_is_last( app = Pregel(nodes={"one": chain}) - assert app.input_schema.schema() == {"title": "PregelInput"} - assert app.output_schema.schema() == {"title": "PregelOutput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.output_schema.schema() == {"title": "LangGraphOutput"} assert await app.ainvoke(2) == {"input": 3, "is_last_step": False} assert await app.ainvoke(2, {"recursion_limit": 1}) == { "input": 3, @@ -114,9 +114,9 @@ async def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: output=["output"], ) - assert app.input_schema.schema() == {"title": "PregelInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput"} assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": {"output": {"title": "Output"}}, } @@ -136,12 +136,12 @@ async def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> ) assert app.input_schema.schema() == { - "title": "PregelInput", + "title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input"}}, } assert app.output_schema.schema() == { - "title": "PregelOutput", + "title": "LangGraphOutput", "type": "object", "properties": {"output": {"title": "Output"}}, } From e497d14db01ddde77f691755ebad8227453dc05c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sat, 13 Jan 2024 15:58:25 -0800 Subject: [PATCH 05/25] Fixes --- langgraph/graph/state.py | 3 ++- langgraph/pregel/__init__.py | 7 +++++-- langgraph/pregel/write.py | 12 +++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index c3f58edb6..3b708478a 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -83,7 +83,8 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]: def _update_state(input: dict[str, Any], config: RunnableConfig): - ChannelWrite.do_write(config, **input) + if input is not None: + ChannelWrite.do_write(config, **input) return input diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 46baeb6d4..c37a0e74c 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -563,7 +563,10 @@ def _read_channel( try: return channels[chan].get() except EmptyChannelError: - return None + if catch: + return None + else: + raise def _apply_writes( @@ -604,7 +607,7 @@ def _apply_writes_from_view( checkpoint: Checkpoint, channels: Mapping[str, BaseChannel], values: dict[str, Any] ) -> None: for chan, value in values.items(): - if value == channels[chan].get(): + if value == _read_channel(channels, chan): continue assert isinstance(channels[chan], LastValue), ( diff --git a/langgraph/pregel/write.py b/langgraph/pregel/write.py index 2063f5018..e44b3e3db 100644 --- a/langgraph/pregel/write.py +++ b/langgraph/pregel/write.py @@ -51,6 +51,11 @@ class ChannelWrite(RunnablePassthrough): values = [ (chan, r.invoke(input, config) if r else input) for chan, r in self.channels ] + values = [ + write + for write, chan in zip(values, self.channels) + if chan[1] is None or write[1] is not None + ] self.do_write(config, **dict(values)) @@ -59,10 +64,15 @@ class ChannelWrite(RunnablePassthrough): (chan, await r.ainvoke(input, config) if r else input) for chan, r in self.channels ] + values = [ + write + for write, chan in zip(values, self.channels) + if chan[1] is None or write[1] is not None + ] self.do_write(config, **dict(values)) @staticmethod def do_write(config: RunnableConfig, **values: Any) -> None: write: TYPE_SEND = config["configurable"][CONFIG_KEY_SEND] - write([(chan, val) for chan, val in values.items() if val is not None]) + write([(chan, val) for chan, val in values.items()]) From ec219c4d493bbfcadf4f9e2768c3ef2a06cd5ce1 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Sun, 14 Jan 2024 15:36:05 -0800 Subject: [PATCH 06/25] cr --- examples/agent_executor.ipynb | 104 ++ examples/langgraph.ipynb | 1635 +++++++++++++---- examples/messages_executor.ipynb | 96 + examples/messages_executor_how_to/base.ipynb | 235 +++ .../dynamically_returning_directly.ipynb | 302 +++ .../force-calling-a-tool-first.ipynb | 287 +++ .../human-in-the-loop.ipynb | 252 +++ .../managing-agent-steps.ipynb | 237 +++ .../respond-in-format.ipynb | 265 +++ langgraph/prebuilt/__init__.py | 0 langgraph/prebuilt/agent_executor.py | 90 + langgraph/prebuilt/messages_executor.py | 122 ++ langgraph/prebuilt/tool_executor.py | 40 + 13 files changed, 3268 insertions(+), 397 deletions(-) create mode 100644 examples/agent_executor.ipynb create mode 100644 examples/messages_executor.ipynb create mode 100644 examples/messages_executor_how_to/base.ipynb create mode 100644 examples/messages_executor_how_to/dynamically_returning_directly.ipynb create mode 100644 examples/messages_executor_how_to/force-calling-a-tool-first.ipynb create mode 100644 examples/messages_executor_how_to/human-in-the-loop.ipynb create mode 100644 examples/messages_executor_how_to/managing-agent-steps.ipynb create mode 100644 examples/messages_executor_how_to/respond-in-format.ipynb create mode 100644 langgraph/prebuilt/__init__.py create mode 100644 langgraph/prebuilt/agent_executor.py create mode 100644 langgraph/prebuilt/messages_executor.py create mode 100644 langgraph/prebuilt/tool_executor.py diff --git a/examples/agent_executor.ipynb b/examples/agent_executor.ipynb new file mode 100644 index 000000000..610a74998 --- /dev/null +++ b/examples/agent_executor.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "e2fdcac4-d134-402b-b423-b0cf4b939f5d", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.agent_executor import create_agent_executor\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_community.tools.tavily_search import TavilySearchResults" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "afb59979-c7a3-435f-b147-f8d501f6ff13", + "metadata": {}, + "outputs": [], + "source": [ + "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)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9be722f0-c9ab-4bd2-af27-66adf51134d2", + "metadata": {}, + "outputs": [], + "source": [ + "app = create_agent_executor(agent_runnable, tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "019b591b-fd71-4ee8-ae94-06d0e2dc6a4d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", + "----\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"}, log=\"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\")}\n", + "----\n", + "{'input': 'what is the weather in sf', 'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"}, log=\"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"input\": \"what is the weather in sf\"}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "162f647a-36b4-45c3-a171-c8452b05af01", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/langgraph.ipynb b/examples/langgraph.ipynb index 34fd6c3f4..cc558d276 100644 --- a/examples/langgraph.ipynb +++ b/examples/langgraph.ipynb @@ -1,400 +1,1241 @@ { - "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 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()" - ] - }, - { - "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" - } + "cells": [ + { + "cell_type": "markdown", + "id": "396e20d9-8684-40ea-a46a-e3dfa36ed5a6", + "metadata": {}, + "source": [ + "## Existing Agent Executor" + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": 1, + "id": "d642e6af-217a-4414-a78c-509b44155eca", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/harrisonchase/workplace/langchain/libs/core/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The class `langchain_community.chat_models.openai.ChatOpenAI` was deprecated in langchain-community 0.0.10 and will be removed in 0.2.0. An updated version of the class exists in the langchain-openai package and should be used instead. To use it run `pip install -U langchain-openai` and import as `from langchain_openai import ChatOpenAI`.\n", + " warn_deprecated(\n" + ] + } + ], + "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", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "from langgraph.prebuilt.agent_executor import create_agent_executor\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", + "tool_executor = ToolExecutor(tools)\n", + "chain = create_agent_executor(agent_runnable, tool_executor)" + ] + }, + { + "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.whereandwhen.net/when/north-america/california/san-francisco-ca/january/',\n", + " 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}])],\n", + " 'agent_outcome': AgentFinish(return_values={'output': 'I found information about the weather in San Francisco in January 2024. It looks like the weather statistics for January 2023 are available, showing the average temperatures and precipitation for each day. If you need current weather information, I can help you find a reliable source for that.'}, log='I found information about the weather in San Francisco in January 2024. It looks like the weather statistics for January 2023 are available, showing the average temperatures and precipitation for each day. If you need current weather information, I can help you find a reliable source for that.')}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain.invoke({\"input\": \"what is the weather in sf\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dff32d55-f8b3-45fd-8de7-daa973aaa20b", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "2d518968-2c61-4f4b-a2ae-8f8a545a0e7b", + "metadata": {}, + "source": [ + "## Agent Messages Executor" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4c468b87-3ff4-4d61-87bb-4fe0b61bca13", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "from langchain_core.messages import FunctionMessage, HumanMessage, SystemMessage\n", + "from langchain.tools.render import format_tool_to_openai_function\n", + "import json\n", + "from langchain.chat_models import ChatOpenAI\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "from langgraph.prebuilt.agent_messages_executor import create_agent_messages_executor\n", + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "659351eb-194c-4cc0-8f8b-ebbcc753ee38", + "metadata": {}, + "outputs": [], + "source": [ + "def call_model(messages):\n", + " response = model.invoke(messages)\n", + " return messages + [response]\n", + "\n", + "\n", + "def exit(messages):\n", + " last_message = messages[-1]\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " else:\n", + " return \"function\"\n", + "\n", + "tool_executor = ToolExecutor(tools)\n", + "def call_tool(messages):\n", + " last_message = messages[-1]\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " response = tool_executor.execute(action)\n", + " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", + " return messages + [function_message]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b78d8ba1-a428-4022-b5fc-cfb0744d2ac1", + "metadata": {}, + "outputs": [], + "source": [ + "chain = create_agent_messages_executor(call_model, call_tool, exit)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e6f7d494-d922-4f9b-8499-36b179917522", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json')]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't find the current weather information for San Francisco. However, you can check the weather in San Francisco for January 2024 on this website: [San Francisco Weather in January](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).\")]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't find the current weather information for San Francisco. However, you can check the weather in San Francisco for January 2024 on this website: [San Francisco Weather in January](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).\")]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "markdown", + "id": "479f258a-6b61-42a5-85bb-c1c141f6d1fb", + "metadata": {}, + "source": [ + "### Human in the Loop\n", + "\n", + "#### Require confirmation" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b7e94bc5-38c0-403c-8902-91eafd7e0c92", + "metadata": {}, + "outputs": [], + "source": [ + "def call_tool(messages):\n", + " last_message = messages[-1]\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " response = input(prompt=f\"[y/n] Okay to call this tool? {action}\")\n", + " if response == \"n\":\n", + " raise ValueError\n", + " response = tool_executor.execute(action)\n", + " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", + " return messages + [function_message]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e112af6b-6339-4a54-bf90-dcc0a3b3f7ed", + "metadata": {}, + "outputs": [], + "source": [ + "chain = create_agent_messages_executor(call_model, call_tool, exit)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b3ad0780-8f1f-4ddd-9472-6f6400f0dcee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", + "----\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log='' \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json')]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't retrieve the current weather information for San Francisco. However, you can visit this [link](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/) to check the weather statistics for San Francisco in January.\")]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't retrieve the current weather information for San Francisco. However, you can visit this [link](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/) to check the weather statistics for San Francisco in January.\")]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1653c386-dd60-4283-ac03-21f00d57bddb", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "def call_tool(messages):\n", + " last_message = messages[-1]\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " response = input(prompt=f\"[y/n] Okay to call this tool? {action}\")\n", + " if response == \"**EXIT**\":\n", + " raise ValueError\n", + " elif response:\n", + " print(\"foo\")\n", + " action.tool_input = response\n", + " response = tool_executor.execute(action)\n", + " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", + " return messages + [function_message]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b3d06fbc-a8d8-4a2a-b436-98975a69ef38", + "metadata": {}, + "outputs": [], + "source": [ + "chain = create_agent_messages_executor(call_model, call_tool, exit)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1aeb7d89-2cb1-4e41-98b4-cb645d1245c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", + "----\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log='' {'query': 'current weather in SF'}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "foo\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json')]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this website](https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/) to find information about the weather in San Francisco for the month of January.\")]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this website](https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/) to find information about the weather in San Francisco for the month of January.\")]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "markdown", + "id": "4b003c09-ceeb-44bf-94c5-502bde98e3c1", + "metadata": {}, + "source": [ + "## Respond in a specific format" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "fac71b90-d7d0-4cab-af3b-f3ed8264472f", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from typing import List" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "5d80baf8-977d-44b4-a092-9d449f16a577", + "metadata": {}, + "outputs": [], + "source": [ + "class Answer(BaseModel):\n", + " \"\"\"Final Response\"\"\"\n", + " temp: int = Field(description=\"current temperature, in Farenheit\")\n", + " source: List[str] = Field(description=\"URLs to go to to learn more info\")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "ead5fb7e-ad02-4554-9595-2cbac99207a8", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function\n", + "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools] + [convert_pydantic_to_openai_function(Answer)])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "8e708efa-501e-42e6-94bd-71f72b61ec48", + "metadata": {}, + "outputs": [], + "source": [ + "def call_model(messages):\n", + " response = model.invoke(messages)\n", + " return messages + [response]\n", + "\n", + "\n", + "def exit(messages):\n", + " last_message = messages[-1]\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " elif \"function_call\" in last_message.additional_kwargs and last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Answer\":\n", + " return \"end\"\n", + " else:\n", + " return \"function\"" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "40264371-76ad-4216-8554-8afb7632d741", + "metadata": {}, + "outputs": [], + "source": [ + "chain = create_agent_messages_executor(call_model, call_tool, exit)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "7f790767-3272-4b28-8264-ac621606be18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}})]\n", + "----\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'current weather in San Francisco'} log='' \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json')]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'Answer', 'arguments': '{\\n \"temp\": 65,\\n \"source\": [\"https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/\"]\\n}'}})]\n", + "----\n", + "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'Answer', 'arguments': '{\\n \"temp\": 65,\\n \"source\": [\"https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/\"]\\n}'}})]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "markdown", + "id": "ddc74d57-b82a-406d-9f19-ef7808ee9ceb", + "metadata": {}, + "source": [ + "## Tool State" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bacb638e-5bd5-425c-a019-b8345a24ef17", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.tools import tool\n", + "from langchain_core.pydantic_v1 import BaseModel, Field" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e3e2a10a-b1c9-4320-8f01-a42b9c6b132d", + "metadata": {}, + "outputs": [], + "source": [ + "class IntSchema(BaseModel):\n", + " num: int\n", + " ls: dict\n", + "\n", + " @classmethod\n", + " def schema(cls):\n", + " schema = super().schema()\n", + " properties = schema.get('properties', {})\n", + " properties.pop('ls', None) # Remove the hidden attribute\n", + " return schema" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3bd82332-4f47-42b8-b70b-b8b2dedac19c", + "metadata": {}, + "outputs": [], + "source": [ + "@tool(args_schema=IntSchema)\n", + "def add_int(num, ls):\n", + " \"\"\"Call this to add number to the list.\"\"\"\n", + " ls[\"foo\"].append(num)\n", + " print(ls)\n", + " return \"Done!\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2a8d4bed-04c8-4bf2-ac84-5356cbd07d3a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'num': {'title': 'Num', 'type': 'integer'}}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IntSchema.schema()[\"properties\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9471f29b-0c10-4d07-8d15-f3463c453fcb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/harrisonchase/workplace/langchain/libs/core/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The class `langchain_community.chat_models.openai.ChatOpenAI` was deprecated in langchain-community 0.0.10 and will be removed in 0.2.0. An updated version of the class exists in the langchain-openai package and should be used instead. To use it run `pip install -U langchain-openai` and import as `from langchain_openai import ChatOpenAI`.\n", + " warn_deprecated(\n" + ] + } + ], + "source": [ + "from langchain_core.agents import AgentAction\n", + "from langchain_core.messages import FunctionMessage, HumanMessage, SystemMessage\n", + "from langchain.tools.render import format_tool_to_openai_function\n", + "import json\n", + "from langchain.chat_models import ChatOpenAI\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "from langgraph.prebuilt.executor import create_executor\n", + "tools = [add_int]\n", + "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bf64ec0f-4563-446a-952b-c9ad3016d063", + "metadata": {}, + "outputs": [], + "source": [ + "ls = {\"foo\": []}\n", + "def call_model(messages):\n", + " response = model.invoke(messages)\n", + " return messages + [response]\n", + "\n", + "\n", + "def exit(messages):\n", + " last_message = messages[-1]\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " else:\n", + " return \"continue\"\n", + "\n", + "tool_executor = ToolExecutor(tools)\n", + "def call_tool(messages):\n", + " last_message = messages[-1]\n", + " agent_action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " agent_action.tool_input[\"ls\"] = ls\n", + " tool_to_use = {t.name: t for t in tools}[agent_action.tool]\n", + " # Call that tool on the input\n", + " observation = tool_to_use.invoke(agent_action.tool_input)\n", + " function_message = FunctionMessage(content=str(observation), name=agent_action.tool)\n", + " return messages + [function_message]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "83e7429b-1981-44ec-b567-50aa3a7b1731", + "metadata": {}, + "outputs": [], + "source": [ + "chain = create_executor(call_model, call_tool, exit)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bda94e2c-9cf7-49b4-b687-466f1cbf81b2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'foo': []}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9b63e7d1-a421-4a6b-ad47-93d8b497fd3e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}})]\n", + "----\n", + "{'foo': [1]}\n", + "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int')]\n", + "----\n", + "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number one has been added to the list.')]\n", + "----\n", + "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number one has been added to the list.')]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"add the number one to the list\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b2bbf236-c6b1-472c-a1a6-64d057096584", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'foo': [1]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7679dc19-8ee4-4139-b2bd-2773ed378193", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}})]\n", + "----\n", + "{'foo': [1, 3]}\n", + "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int')]\n", + "----\n", + "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number 3 has been added to the list.')]\n", + "----\n", + "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number 3 has been added to the list.')]\n", + "----\n" + ] + } + ], + "source": [ + "messages = [HumanMessage(content=\"add the number 3 to the list\")]\n", + "for s in chain.stream(messages):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "acb104ef-270d-4a03-b14c-c01b8e391935", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'foo': [1, 3]}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ls" + ] + }, + { + "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": "0c349b42-ae43-4564-90d2-80eff5b9c236", + "metadata": {}, + "outputs": [], + "source": [ + "## Plan and Execute\n", + "\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from typing import List, Tuple\n", + "\n", + "\n", + "class PlanExecute(BaseModel):\n", + "\n", + " plan: List[str] = []\n", + " past_steps: List[Tuple] = []\n", + " response: str = \"\"\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain.chains.openai_functions import create_structured_output_runnable\n", + "from langchain_core.tools import tool\n", + "\n", + "class Plan(BaseModel):\n", + " \"\"\"Plan to follow in future\"\"\"\n", + " steps: List[str] = Field(description=\"different steps to follow, should be in sorted order\")\n", + "\n", + "planner_prompt = ChatPromptTemplate.from_template(\"\"\"For the given objective, come up with a simple step by step plan. \\\n", + "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", + "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", + "\n", + "{objective}\"\"\")\n", + "planner = create_structured_output_runnable(Plan, ChatOpenAI(model=\"gpt-4-1106-preview\", temperature=0), planner_prompt)\n", + "\n", + "planner.invoke({'objective': 'what is leo dicaprios gf age raised to .23'})\n", + "\n", + "@tool\n", + "def search(query:str):\n", + " \"\"\"Get a response from google\"\"\"\n", + " return 25\n", + "\n", + "@tool\n", + "def math(equation: str):\n", + " \"\"\"Solve a math equation\"\"\"\n", + " return .34\n", + "\n", + "tools = [search, math]\n", + "\n", + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "# Choose the LLM that will drive the agent\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n", + "\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_core.agents import AgentFinish\n", + "\n", + "\n", + "# Define the agent\n", + "# Note that here, we are using `.assign` to add the output of the agent to the dictionary\n", + "# This dictionary will be returned from the node\n", + "# The reason we don't want to return just the result of `agent_runnable` from this node is\n", + "# that we want to continue passing around all the other inputs\n", + "agent = RunnablePassthrough.assign(\n", + " agent_outcome = agent_runnable\n", + ")\n", + "\n", + "# Define the function to execute tools\n", + "def action(data):\n", + " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", + " agent_action = data.pop('agent_outcome')\n", + " # Get the tool to use\n", + " tool_to_use = {t.name: t for t in tools}[agent_action.tool]\n", + " # Call that tool on the input\n", + " observation = tool_to_use.invoke(agent_action.tool_input)\n", + " # We now add in the action and the observation to the `intermediate_steps` list\n", + " # This is the list of all previous actions taken and their output\n", + " data['intermediate_steps'].append((agent_action, observation))\n", + " return data\n", + "\n", + "# Define logic that will be used to determine which conditional edge to go down\n", + "def should_continue(data):\n", + " # If the agent outcome is an AgentFinish, then we return `exit` string\n", + " # This will be used when setting up the graph to define the flow\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"end\"\n", + " # Otherwise, an AgentAction is returned\n", + " # Here we return `continue` string\n", + " # This will be used when setting up the graph to define the flow\n", + " else:\n", + " return \"continue\"\n", + "\n", + "def plan(inputs):\n", + " inputs['state'] = PlanExecute(plan=planner.invoke(inputs).steps)\n", + " inputs['input'] = inputs['state'].plan[0]\n", + " inputs['intermediate_steps'] = []\n", + " return inputs\n", + "\n", + "from langchain.chains.openai_functions import create_openai_fn_runnable\n", + "class Response(BaseModel):\n", + " \"\"\"Response to user.\"\"\"\n", + " response: str\n", + "\n", + "replanner_prompt = ChatPromptTemplate.from_template(\"\"\"For the given objective, come up with a simple step by step plan. \\\n", + "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", + "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", + "\n", + "Your objective was this:\n", + "{objective}\n", + "\n", + "Your original plan was this:\n", + "{plan}\n", + "\n", + "You have currently done the follow steps:\n", + "{steps}\n", + "\n", + "Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan.\"\"\")\n", + "\n", + "\n", + "replanner = create_openai_fn_runnable([Plan, Response], ChatOpenAI(model=\"gpt-4-1106-preview\", temperature=0), replanner_prompt)\n", + "\n", + "replanner.invoke({\"objective\": \"look up the temperature\", \"plan\": \"look up the temperature\", \"steps\": [(\"look up the temperature\", \"i am in san diego\")]})\n", + "\n", + "def replan(inputs):\n", + " inputs['state'].past_steps.append((inputs['state'].plan[0], inputs['agent_outcome'].return_values['output']))\n", + " sub_inputs = {\n", + " \"objective\": inputs[\"objective\"],\n", + " \"plan\": inputs[\"state\"].plan,\n", + " \"steps\": inputs[\"state\"].past_steps\n", + " }\n", + " output = replanner.invoke(sub_inputs)\n", + " if isinstance(output, Response):\n", + " inputs['state'].response = output.response\n", + " else:\n", + " inputs['state'].plan = output.steps\n", + " inputs['input'] = inputs['state'].plan[0]\n", + " return inputs\n", + "\n", + "\n", + "def should_end(inputs):\n", + " if inputs['state'].response:\n", + " return True\n", + " else:\n", + " return False\n", + "\n", + "from langgraph.graph import END, Graph\n", + "\n", + "workflow = Graph()\n", + "\n", + "# Add the plan node\n", + "workflow.add_node(\"plan\", plan)\n", + "\n", + "# Add the agent node, we give it name `agent` which we will use later\n", + "workflow.add_node(\"agent\", agent)\n", + "# Add the action node, we give it name `action` which we will use later\n", + "workflow.add_node(\"action\", action)\n", + "\n", + "# Add a replan node\n", + "workflow.add_node(\"replan\", replan)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"plan\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we go back to replan\n", + " \"end\": \"replan\"\n", + " }\n", + ")\n", + "\n", + "# From plan we go to agent\n", + "workflow.add_edge('plan', 'agent')\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "workflow.add_conditional_edges(\n", + " \"replan\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_end,\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " True: END,\n", + " False: \"agent\",\n", + " }\n", + ")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "chain = workflow.compile()\n", + "\n", + "for s in chain.stream({\"objective\": \"what is leo dicaprios gf age raised to .34\"}):\n", + " print(s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb6fb6a8-f6f5-444a-9033-ef9e0bd6fd23", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/examples/messages_executor.ipynb b/examples/messages_executor.ipynb new file mode 100644 index 000000000..e0e145a98 --- /dev/null +++ b/examples/messages_executor.ipynb @@ -0,0 +1,96 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "efb7e3c0-c63f-40f6-93ce-19681d650fc2", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a7025f33-3160-41cf-868b-17ebc916fb1d", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "32b4ae66-f667-4a8b-a602-503fd0effcd9", + "metadata": {}, + "outputs": [], + "source": [ + "app = create_messages_executor(model, tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0abc5655-d772-450c-832f-1fee1111a5f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content=\"I apologize, but I couldn't retrieve the current weather information for San Francisco. However, you can check the weather history for January 2024 in San Francisco on this website: [San Francisco Weather History](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't retrieve the current weather information for San Francisco. However, you can check the weather history for January 2024 in San Francisco on this website: [San Francisco Weather History](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/base.ipynb b/examples/messages_executor_how_to/base.ipynb new file mode 100644 index 000000000..8b6397135 --- /dev/null +++ b/examples/messages_executor_how_to/base.ipynb @@ -0,0 +1,235 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/dynamically_returning_directly.ipynb b/examples/messages_executor_how_to/dynamically_returning_directly.ipynb new file mode 100644 index 000000000..6822c442d --- /dev/null +++ b/examples/messages_executor_how_to/dynamically_returning_directly.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "75bbcaa8-b23a-409c-9745-08d3aca7c3cb", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "class SearchTool(search_tool.args_schema):\n", + " \"\"\"Look up things online, optionally returning directly\"\"\"\n", + " query: str = Field(description=\"query to look up online\")\n", + " return_direct: bool = Field(\n", + " description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\", \n", + " default = False\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0c93c6e1-532b-472e-b9f7-d374b9d3c89e", + "metadata": {}, + "outputs": [], + "source": [ + "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [search_tool]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we check if it's suppose to return direct\n", + " else:\n", + " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", + " if arguments.get(\"return_direct\", False):\n", + " return \"final\"\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " tool_name = last_message.additional_kwargs[\"function_call\"][\"name\"]\n", + " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", + " if tool_name == \"tavily_search_results_json\":\n", + " if \"return_direct\" in arguments:\n", + " del arguments[\"return_direct\"]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=tool_name,\n", + " tool_input=arguments,\n", + " log=\"\",\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "workflow.add_node(\"final\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Final call\n", + " \"final\": \"final\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "workflow.add_edge('final', END)\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content='The current weather in San Francisco is not available. However, you can check the weather history for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The current weather in San Francisco is not available. However, you can check the weather history for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf? return this result directly by setting return_direct = True\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d2e89d8-0e35-465c-8984-1aa37a132b03", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/force-calling-a-tool-first.ipynb b/examples/messages_executor_how_to/force-calling-a-tool-first.ipynb new file mode 100644 index 000000000..d4fb92bf5 --- /dev/null +++ b/examples/messages_executor_how_to/force-calling-a-tool-first.ipynb @@ -0,0 +1,287 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "47bf79f1-652c-43dc-aeb3-0f0de4400539", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'tavily_search_results_json'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tools[0].name" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ab62da37-9632-4dc8-833b-feb4c62bab37", + "metadata": {}, + "outputs": [], + "source": [ + "# This is the new first - the first call of the model we want to explicitly hard-code some action\n", + "from langchain_core.messages import AIMessage\n", + "import json\n", + "\n", + "def first_model(state):\n", + " human_input = state['messages'][-1].content\n", + " return {\"messages\": [AIMessage(\n", + " content=\"\", \n", + " additional_kwargs={\n", + " \"function_call\": {\n", + " \"name\": \"tavily_search_results_json\", \n", + " \"arguments\": json.dumps({\"query\": human_input})\n", + " }\n", + " }\n", + " )\n", + " ]}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the new entrypoint\n", + "workflow.add_node(\"first_agent\", first_model)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"first_agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# After we call the first agent, we know we want to go to action\n", + "workflow.add_edge('first_agent', 'action')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this link](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to see the historical weather data for January 2024 in San Francisco.\")]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this link](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to see the historical weather data for January 2024 in San Francisco.\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/human-in-the-loop.ipynb b/examples/messages_executor_how_to/human-in-the-loop.ipynb new file mode 100644 index 000000000..5f6219809 --- /dev/null +++ b/examples/messages_executor_how_to/human-in-the-loop.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "# Here we add some lines to add a human-in-the-loop\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " response = input(prompt=f\"[y/n] continue with: {action}?\")\n", + " if response == \"n\":\n", + " raise ValueError\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log=''? y\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the historical weather data for San Francisco in January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the historical weather data for San Francisco in January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/managing-agent-steps.ipynb b/examples/messages_executor_how_to/managing-agent-steps.ipynb new file mode 100644 index 000000000..b136431f5 --- /dev/null +++ b/examples/messages_executor_how_to/managing-agent-steps.ipynb @@ -0,0 +1,237 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " if len(messages) > 10:\n", + " messages = messages[-10:]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/respond-in-format.ipynb b/examples/messages_executor_how_to/respond-in-format.ipynb new file mode 100644 index 000000000..85085b8e1 --- /dev/null +++ b/examples/messages_executor_how_to/respond-in-format.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a3bdf328-f34c-421e-9771-85f2740fbad6", + "metadata": {}, + "outputs": [], + "source": [ + "# Here we bind an additional function beside just tools - the response format\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "class Response(BaseModel):\n", + " \"\"\"Final response to the user\"\"\"\n", + " temperature: float = Field(description=\"the temperature\")\n", + " other_notes: str = Field(description=\"any other notes about the weather\")\n", + "\n", + "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "metadata": {}, + "outputs": [], + "source": [ + "model = model.bind_functions(functions + [convert_pydantic_to_openai_function(Response)])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "# We create the AgentState that we will pass around\n", + "# This simply involves a list of messages\n", + "# We want steps to return messages to append to the list\n", + "# So we annotate the messages attribute with operator.add\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "0759bddc-010a-4e3f-9f41-3a51c1ea5144", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentAction\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "# This needs to get updated\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we need to check what type of function call it is\n", + " elif last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Response\":\n", + " return \"end\"\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "2a048e72-525a-4dcb-a91d-efacaddd8848", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an AgentAction from the function_call\n", + " action = AgentAction(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " log=\"\",\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 80,\\n \"other_notes\": \"Mostly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 80,\\n \"other_notes\": \"Mostly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langgraph/prebuilt/__init__.py b/langgraph/prebuilt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py new file mode 100644 index 000000000..77b365e35 --- /dev/null +++ b/langgraph/prebuilt/agent_executor.py @@ -0,0 +1,90 @@ +from typing import Annotated, TypedDict +import operator +from langchain_core.agents import AgentAction, AgentFinish +from langgraph.graph import StateGraph, END +from langgraph.prebuilt.tool_executor import ToolExecutor + + + + +def create_agent_executor(agent_runnable, tools, input_schema=None): + + if isinstance(tools, ToolExecutor): + tool_executor = tools + else: + tool_executor = ToolExecutor(tools) + + + if input_schema is None: + class AgentState(TypedDict): + input: str + agent_outcome: AgentAction | AgentFinish | None + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + else: + class AgentState(input_schema): + agent_outcome: AgentAction | AgentFinish | None + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + def should_continue(data): + # If the agent outcome is an AgentFinish, then we return `exit` string + # This will be used when setting up the graph to define the flow + if isinstance(data['agent_outcome'], AgentFinish): + return "end" + # Otherwise, an AgentAction is returned + # Here we return `continue` string + # This will be used when setting up the graph to define the flow + else: + return "continue" + + def run_agent(data): + agent_outcome = agent_runnable.invoke(data) + return {"agent_outcome": agent_outcome} + + # Define the function to execute tools + def execute_tools(data): + # Get the most recent agent_outcome - this is the key added in the `agent` above + agent_action = data['agent_outcome'] + output = tool_executor.invoke(agent_action) + return {"intermediate_steps": [(agent_action, str(output))]} + + # Define a new graph + workflow = StateGraph(AgentState) + + # Define the two nodes we will cycle between + workflow.add_node("agent", run_agent) + workflow.add_node("action", execute_tools) + + # Set the entrypoint as `agent` + # This means that this node is the first one called + workflow.set_entry_point("agent") + + # We now add a conditional edge + workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END + } + ) + + # We now add a normal edge from `tools` to `agent`. + # This means that after `tools` is called, `agent` node is called next. + workflow.add_edge('action', 'agent') + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + return workflow.compile() diff --git a/langgraph/prebuilt/messages_executor.py b/langgraph/prebuilt/messages_executor.py new file mode 100644 index 000000000..760f20ab1 --- /dev/null +++ b/langgraph/prebuilt/messages_executor.py @@ -0,0 +1,122 @@ +from langchain_core.runnables import RunnablePassthrough +from langchain_core.messages import FunctionMessage +from langchain_core.agents import AgentFinish, AgentAction +import json + +from langchain.tools.render import format_tool_to_openai_function +from langgraph.prebuilt.tool_executor import ToolExecutor +from langchain_core.utils.function_calling import convert_pydantic_to_openai_function +from typing import Annotated, TypedDict, Sequence +from langchain_core.messages import BaseMessage +import operator +from langchain_core.agents import AgentAction, AgentFinish +from langgraph.graph import StateGraph, END + + +def _get_tool_executor_and_functions(tools, response_format): + if isinstance(tools, ToolExecutor): + tool_executor = tools + tool_classes = tools.tools + else: + tool_executor = ToolExecutor(tools) + tool_classes = tools + + functions = [format_tool_to_openai_function(t) for t in tool_classes] + if response_format is not None: + functions.append(convert_pydantic_to_openai_function(response_format)) + return tool_executor, functions + + +def create_messages_executor(model, tools, response_format = None): + tool_executor, functions = _get_tool_executor_and_functions(tools, response_format) + model = model.bind_functions([format_tool_to_openai_function(t) for t in tools]) + + # Define the function that determines whether to continue or not + def should_continue(state): + messages = state['messages'] + last_message = messages[-1] + # If there is no function call, then we finish + if "function_call" not in last_message.additional_kwargs: + return "end" + # Otherwise if there is, we need to check what type of function call it is + else: + if response_format is None: + return "continue" + elif last_message.additional_kwargs["function_call"]["name"] == response_format.__name__: + return "end" + else: + return "continue" + + # Define the function that calls the model + def call_model(state): + messages = state['messages'] + response = model.invoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + + # Define the function to execute tools + def call_tool(state): + messages = state['messages'] + # Based on the continue condition + # we know the last message involves a function call + last_message = messages[-1] + # We construct an AgentAction from the function_call + action = AgentAction( + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]), + log="", + ) + # We call the tool_executor and get back a response + response = tool_executor.invoke(action) + # We use the response to create a FunctionMessage + function_message = FunctionMessage(content=str(response), name=action.tool) + # We return a list, because this will get added to the existing list + return {"messages": [function_message]} + + # We create the AgentState that we will pass around + # This simply involves a list of messages + # We want steps to return messages to append to the list + # So we annotate the messages attribute with operator.add + class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] + + # Define a new graph + workflow = StateGraph(AgentState) + + # Define the two nodes we will cycle between + workflow.add_node("agent", call_model) + workflow.add_node("action", call_tool) + + # Set the entrypoint as `agent` + # This means that this node is the first one called + workflow.set_entry_point("agent") + + # We now add a conditional edge + workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END + } + ) + + # We now add a normal edge from `tools` to `agent`. + # This means that after `tools` is called, `agent` node is called next. + workflow.add_edge('action', 'agent') + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + return workflow.compile() diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py new file mode 100644 index 000000000..7f860f256 --- /dev/null +++ b/langgraph/prebuilt/tool_executor.py @@ -0,0 +1,40 @@ +from langchain_core.runnables import RunnableBinding, RunnableLambda +from typing import Sequence, Any +from langchain_core.tools import BaseTool +from langchain_core.agents import AgentAction +INVALID_TOOL_MSG_TEMPLATE = ( + "{requested_tool_name} is not a valid tool, " + "try one of [{available_tool_names_str}]." +) + +class ToolExecutor(RunnableBinding): + + tools: Sequence[BaseTool] + tool_map: dict + invalid_tool_msg_template: str + def __init__(self, tools: Sequence[BaseTool], invalid_tool_msg_template: str = INVALID_TOOL_MSG_TEMPLATE, **kwargs: Any) -> None: + + bound = RunnableLambda(self._execute, afunc=self._aexecute) + super().__init__(bound=bound, tools=tools, tool_map ={t.name: t for t in tools}, invalid_tool_msg_template=invalid_tool_msg_template, **kwargs) + + def _execute(self, tool_invocation: AgentAction) -> Any: + if tool_invocation.tool not in self.tool_map: + return self.invalid_tool_msg_template.format( + requested_tool_name=tool_invocation.tool, + available_tool_names_str=", ".join([t.name for t in self.tools]) + ) + else: + tool = self.tool_map[tool_invocation.tool] + output = tool.invoke(tool_invocation.tool_input) + return output + + async def _aexecute(self, tool_invocation: AgentAction) -> Any: + if tool_invocation.tool not in self.tool_map: + return self.invalid_tool_msg_template.format( + requested_tool_name=tool_invocation.tool, + available_tool_names_str=", ".join([t.name for t in self.tools]) + ) + else: + tool = self.tool_map[tool_invocation.tool] + output = await tool.ainvoke(tool_invocation.tool_input) + return output \ No newline at end of file From 6383e24b0820479dd11f88315062375af76d17f1 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 08:35:52 -0800 Subject: [PATCH 07/25] Manually mark runs as hidden in langsmith --- langgraph/graph/graph.py | 8 +++++--- langgraph/graph/state.py | 10 +++++++--- langgraph/pregel/__init__.py | 7 +++++++ langgraph/pregel/read.py | 3 ++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 4caac7a16..b5e317334 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -29,6 +29,7 @@ class Graph: self.nodes: dict[str, Runnable] = {} self.edges = set[tuple[str, str]]() self.branches: defaultdict[str, list[Branch]] = defaultdict(list) + self.support_multiple_edges = False def add_node(self, key: str, action: RunnableLike) -> None: if key in self.nodes: @@ -46,8 +47,9 @@ class Graph: if end_key not in self.nodes and end_key != END: raise ValueError(f"Need to add_node `{end_key}` first") - # TODO: support multiple message passing - if start_key in set(start for start, _ in self.edges): + if not self.support_multiple_edges and start_key in set( + start for start, _ in self.edges + ): raise ValueError(f"Already found path for {start_key}") self.edges.add((start_key, end_key)) @@ -111,7 +113,7 @@ class Graph: outgoing = outgoing_edges[key] edges_key = f"{key}:edges" if outgoing or key in self.branches: - nodes[edges_key] = Channel.subscribe_to(key) + nodes[edges_key] = Channel.subscribe_to(key, tags=["langsmith:hidden"]) if outgoing: nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) if key in self.branches: diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index 3b708478a..5b5c658ca 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -21,6 +21,8 @@ class StateGraph(Graph): super().__init__() self.schema = schema self.channels = _get_channels(schema) + if any(isinstance(c, BinaryOperatorAggregate) for c in self.channels.values()): + self.support_multiple_edges = True def compile(self) -> Pregel: self.validate() @@ -49,7 +51,9 @@ class StateGraph(Graph): outgoing = outgoing_edges[key] edges_key = f"{key}:edges" if outgoing or key in self.branches: - nodes[edges_key] = Channel.subscribe_to(key) | ChannelRead(state_keys) + nodes[edges_key] = Channel.subscribe_to( + key, tags=["langsmith:hidden"] + ) | ChannelRead(state_keys) if outgoing: nodes[edges_key] |= Channel.write_to(*[dest for dest in outgoing]) if key in self.branches: @@ -59,12 +63,12 @@ class StateGraph(Graph): ) nodes[START] = ( - Channel.subscribe_to(f"{START}:inbox") + Channel.subscribe_to(f"{START}:inbox", tags=["langsmith:hidden"]) | _update_state | Channel.write_to(START) ) nodes[f"{START}:edges"] = ( - Channel.subscribe_to(START) + Channel.subscribe_to(START, tags=["langsmith:hidden"]) | ChannelRead(state_keys) | Channel.write_to(f"{self.entry_point}:inbox") ) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index c37a0e74c..cddac689a 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -84,8 +84,10 @@ class Channel: def subscribe_to( cls, channels: str, + *, key: Optional[str] = None, when: Optional[Callable[[Any], bool]] = None, + tags: Optional[Sequence[str]] = None, ) -> ChannelInvoke: ... @@ -94,8 +96,10 @@ class Channel: def subscribe_to( cls, channels: Sequence[str], + *, key: None = None, when: Optional[Callable[[Any], bool]] = None, + tags: Optional[Sequence[str]] = None, ) -> ChannelInvoke: ... @@ -103,8 +107,10 @@ class Channel: def subscribe_to( cls, channels: Union[str, Sequence[str]], + *, key: Optional[str] = None, when: Optional[Callable[[Any], bool]] = None, + tags: Optional[Sequence[str]] = None, ) -> ChannelInvoke: """Runs process.invoke() each time channels are updated, with a dict of the channel values as input.""" @@ -121,6 +127,7 @@ class Channel: ), triggers=[channels] if isinstance(channels, str) else channels, when=when, + tags=tags, ) @classmethod diff --git a/langgraph/pregel/read.py b/langgraph/pregel/read.py index 11c1a050b..ebba367b9 100644 --- a/langgraph/pregel/read.py +++ b/langgraph/pregel/read.py @@ -90,6 +90,7 @@ class ChannelInvoke(RunnableBindingBase): channels: Mapping[None, str] | Mapping[str, str], triggers: Sequence[str], when: Optional[Callable[[Any], bool]] = None, + tags: Optional[Sequence[str]] = None, *, bound: Optional[Runnable[Any, Any]] = None, kwargs: Optional[Mapping[str, Any]] = None, @@ -102,7 +103,7 @@ class ChannelInvoke(RunnableBindingBase): when=when, bound=bound or default_bound, kwargs=kwargs or {}, - config=config, + config={**(config or {}), "tags": tags or []}, **other_kwargs, ) From f6894fb5ddd00137f1a9bc00f80161a532e38d67 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 09:14:30 -0800 Subject: [PATCH 08/25] Add input_keys, rename output_keys, add interrupt --- langgraph/pregel/__init__.py | 92 ++++++++++++++++++++++++++---------- tests/test_pregel.py | 8 ++-- tests/test_pregel_async.py | 8 ++-- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index cddac689a..b61381548 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -161,6 +161,8 @@ class Pregel( hidden: Sequence[str] = Field(default_factory=list) + interrupt: Sequence[str] = Field(default_factory=list) + input: Union[str, Sequence[str]] = "input" step_timeout: Optional[float] = None @@ -229,13 +231,16 @@ class Pregel( run_manager: CallbackManagerForChainRun, config: RunnableConfig, *, - output: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, ) -> Iterator[Union[dict[str, Any], Any]]: if config["recursion_limit"] < 1: raise ValueError("recursion_limit must be at least 1") # assign defaults - if output is None: - output = [chan for chan in self.channels if chan not in self.hidden] + if output_keys is None: + output_keys = [chan for chan in self.channels if chan not in self.hidden] + if input_keys is None: + input_keys = self.input # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one @@ -249,7 +254,7 @@ class Pregel( _apply_writes( checkpoint, channels, - deque(w for c in input for w in map_input(self.input, c)), + deque(w for c in input for w in map_input(input_keys, c)), config, 0, ) @@ -314,10 +319,10 @@ class Pregel( print_checkpoint(step, channels) # yield current value and checkpoint view - if step_output := map_output(output, pending_writes, channels): + if step_output := map_output(output_keys, pending_writes, channels): yield step_output # we can detect updates when output is multiple channels (ie. dict) - if not isinstance(output, str): + if not isinstance(output_keys, str): # if view was updated, apply writes to channels _apply_writes_from_view(checkpoint, channels, step_output) @@ -326,6 +331,10 @@ class Pregel( checkpoint = create_checkpoint(checkpoint, channels) self.saver.put(config, checkpoint) + # interrupt if any channel written to is in interrupt list + if any(chan for chan, _ in pending_writes if chan in self.interrupt): + break + # save end of run checkpoint if self.saver is not None and self.saver.at == CheckpointAt.END_OF_RUN: checkpoint = create_checkpoint(checkpoint, channels) @@ -337,7 +346,8 @@ class Pregel( run_manager: AsyncCallbackManagerForChainRun, config: RunnableConfig, *, - output: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, ) -> AsyncIterator[Union[dict[str, Any], Any]]: if config["recursion_limit"] < 1: raise ValueError("recursion_limit must be at least 1") @@ -351,8 +361,10 @@ class Pregel( None, ) # assign defaults - if output is None: - output = [chan for chan in self.channels if chan not in self.hidden] + if output_keys is None: + output_keys = [chan for chan in self.channels if chan not in self.hidden] + if input_keys is None: + input_keys = self.input # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one @@ -364,7 +376,7 @@ class Pregel( _apply_writes( checkpoint, channels, - deque([w async for c in input for w in map_input(self.input, c)]), + deque([w async for c in input for w in map_input(input_keys, c)]), config, 0, ) @@ -434,10 +446,10 @@ class Pregel( print_checkpoint(step, channels) # yield current value and checkpoint view - if step_output := map_output(output, pending_writes, channels): + if step_output := map_output(output_keys, pending_writes, channels): yield step_output # we can detect updates when output is multiple channels (ie. dict) - if not isinstance(output, str): + if not isinstance(output_keys, str): # if view was updated, apply writes to channels _apply_writes_from_view(checkpoint, channels, step_output) @@ -446,6 +458,10 @@ class Pregel( checkpoint = create_checkpoint(checkpoint, channels) await self.saver.aput(config, checkpoint) + # interrupt if any channel written to is in interrupt list + if any(chan for chan, _ in pending_writes if chan in self.interrupt): + break + # save end of run checkpoint if self.saver is not None and self.saver.at == CheckpointAt.END_OF_RUN: checkpoint = create_checkpoint(checkpoint, channels) @@ -456,14 +472,16 @@ class Pregel( input: Union[dict[str, Any], Any], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: latest: Union[dict[str, Any], Any] = None for chunk in self.stream( input, config, - output=output if output is not None else self.output, + output_keys=output_keys if output_keys is not None else self.output, + input_keys=input_keys, **kwargs, ): latest = chunk @@ -474,21 +492,34 @@ class Pregel( input: Union[dict[str, Any], Any], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> Iterator[Union[dict[str, Any], Any]]: - return self.transform(iter([input]), config, output=output, **kwargs) + return self.transform( + iter([input]), + config, + output_keys=output_keys, + input_keys=input_keys, + **kwargs, + ) def transform( self, input: Iterator[Union[dict[str, Any], Any]], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> Iterator[Union[dict[str, Any], Any]]: for chunk in self._transform_stream_with_config( - input, self._transform, config, output=output, **kwargs + input, + self._transform, + config, + output_keys=output_keys, + input_keys=input_keys, + **kwargs, ): yield chunk @@ -497,14 +528,16 @@ class Pregel( input: Union[dict[str, Any], Any], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: latest: Union[dict[str, Any], Any] = None async for chunk in self.astream( input, config, - output=output if output is not None else self.output, + output_keys=output_keys if output_keys is not None else self.output, + input_keys=input_keys, **kwargs, ): latest = chunk @@ -515,14 +548,19 @@ class Pregel( input: Union[dict[str, Any], Any], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> AsyncIterator[Union[dict[str, Any], Any]]: async def input_stream() -> AsyncIterator[Union[dict[str, Any], Any]]: yield input async for chunk in self.atransform( - input_stream(), config, output=output, **kwargs + input_stream(), + config, + output_keys=output_keys, + input_keys=input_keys, + **kwargs, ): yield chunk @@ -531,11 +569,17 @@ class Pregel( input: AsyncIterator[Union[dict[str, Any], Any]], config: Optional[RunnableConfig] = None, *, - output: Optional[Union[str, Sequence[str]]] = None, + output_keys: Optional[Union[str, Sequence[str]]] = None, + input_keys: Optional[Union[str, Sequence[str]]] = None, **kwargs: Any, ) -> AsyncIterator[Union[dict[str, Any], Any]]: async for chunk in self._atransform_stream_with_config( - input, self._atransform, config, output=output, **kwargs + input, + self._atransform, + config, + output_keys=output_keys, + input_keys=input_keys, + **kwargs, ): yield chunk diff --git a/tests/test_pregel.py b/tests/test_pregel.py index ded25a5f5..7553e4bc6 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -44,7 +44,7 @@ def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} assert app.invoke(2) == 3 - assert app.invoke(2, output=["output"]) == {"output": 3} + assert app.invoke(2, output_keys=["output"]) == {"output": 3} assert repr(app), "does not raise recursion error" assert gapp.invoke(2) == 3 @@ -157,6 +157,8 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert app.invoke(2) == 4 + assert app.invoke(2, input_keys="inbox") == 3 + for step, values in enumerate(app.stream(2), start=1): if step == 1: assert values == { @@ -238,7 +240,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: input=["input", "inbox"], ) - assert [*app.stream({"input": 2, "inbox": 12}, output="output")] == [ + assert [*app.stream({"input": 2, "inbox": 12}, output_keys="output")] == [ 13, 4, ] # [12 + 1, 2 + 1 + 1] @@ -259,7 +261,7 @@ def test_batch_two_processes_in_out() -> None: app = Pregel(nodes={"one": one, "two": two}) assert app.batch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] - assert app.batch([3, 2, 1, 3, 5], output=["output"]) == [ + assert app.batch([3, 2, 1, 3, 5], output_keys=["output"]) == [ {"output": 5}, {"output": 4}, {"output": 3}, diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 3f94cf86d..cbd5406ad 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -42,7 +42,7 @@ async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} assert await app.ainvoke(2) == 3 - assert await app.ainvoke(2, output=["output"]) == {"output": 3} + assert await app.ainvoke(2, output_keys=["output"]) == {"output": 3} assert await gapp.ainvoke(2) == 3 @@ -157,6 +157,8 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert await app.ainvoke(2) == 4 + assert await app.ainvoke(2, input_keys="inbox") == 3 + step = 0 async for values in app.astream(2): step += 1 @@ -247,7 +249,7 @@ 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}, output="output") + c async for c in pubsub.astream({"input": 2, "inbox": 12}, output_keys="output") ] == [13, 4] assert [c async for c in pubsub.astream({"input": 2, "inbox": 12})] == [ {"inbox": [3], "output": 13}, @@ -269,7 +271,7 @@ async def test_batch_two_processes_in_out() -> None: ) assert await app.abatch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] - assert await app.abatch([3, 2, 1, 3, 5], output=["output"]) == [ + assert await app.abatch([3, 2, 1, 3, 5], output_keys=["output"]) == [ {"output": 5}, {"output": 4}, {"output": 3}, From da5e6d7319c8ac152d9ac79fb78e632daa760cbc Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 13:16:05 -0800 Subject: [PATCH 09/25] stash --- README.md | 483 ++++--- examples/agent_executor.ipynb | 104 -- examples/agent_executor/base.ipynb | 274 ++++ .../force-calling-a-tool-first.ipynb | 268 ++++ examples/agent_executor/high-level.ipynb | 302 ++++ .../agent_executor/human-in-the-loop.ipynb | 239 ++++ .../agent_executor/managing-agent-steps.ipynb | 226 +++ .../base.ipynb | 0 .../dynamically-returning-directly.ipynb} | 0 .../force-calling-a-tool-first.ipynb | 0 .../high-level.ipynb} | 50 +- .../human-in-the-loop.ipynb | 0 .../managing-agent-steps.ipynb | 0 .../respond-in-format.ipynb | 0 examples/combine_docs.ipynb | 371 ----- examples/draft-revise-loop.py | 116 -- examples/langgraph.ipynb | 1241 ----------------- examples/rag.py | 58 - examples/readme.py | 132 -- examples/recursive-web-loader.py | 134 -- langgraph/prebuilt/__init__.py | 5 + langgraph/prebuilt/agent_executor.py | 48 +- ...{messages_executor.py => chat_executor.py} | 37 +- langgraph/prebuilt/tool_executor.py | 8 +- 24 files changed, 1679 insertions(+), 2417 deletions(-) delete mode 100644 examples/agent_executor.ipynb create mode 100644 examples/agent_executor/base.ipynb create mode 100644 examples/agent_executor/force-calling-a-tool-first.ipynb create mode 100644 examples/agent_executor/high-level.ipynb create mode 100644 examples/agent_executor/human-in-the-loop.ipynb create mode 100644 examples/agent_executor/managing-agent-steps.ipynb rename examples/{messages_executor_how_to => chat_executor_with_function_calling}/base.ipynb (100%) rename examples/{messages_executor_how_to/dynamically_returning_directly.ipynb => chat_executor_with_function_calling/dynamically-returning-directly.ipynb} (100%) rename examples/{messages_executor_how_to => chat_executor_with_function_calling}/force-calling-a-tool-first.ipynb (100%) rename examples/{messages_executor.ipynb => chat_executor_with_function_calling/high-level.ipynb} (67%) rename examples/{messages_executor_how_to => chat_executor_with_function_calling}/human-in-the-loop.ipynb (100%) rename examples/{messages_executor_how_to => chat_executor_with_function_calling}/managing-agent-steps.ipynb (100%) rename examples/{messages_executor_how_to => chat_executor_with_function_calling}/respond-in-format.ipynb (100%) delete mode 100644 examples/combine_docs.ipynb delete mode 100644 examples/draft-revise-loop.py delete mode 100644 examples/langgraph.ipynb delete mode 100644 examples/rag.py delete mode 100644 examples/readme.py delete mode 100644 examples/recursive-web-loader.py rename langgraph/prebuilt/{messages_executor.py => chat_executor.py} (75%) diff --git a/README.md b/README.md index 37be4a254..d3863b8d4 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,52 @@ tools = [TavilySearchResults(max_results=1)] prompt = hub.pull("hwchase17/openai-functions-agent") # Choose the LLM that will drive the agent -llm = ChatOpenAI(model="gpt-3.5-turbo-1106") +# We set streaming=True so that we can stream tokens (we will cover this more detail later on) +llm = ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True) # Construct the OpenAI Functions agent agent_runnable = create_openai_functions_agent(llm, tools, prompt) ``` +### Define the agent state + +The main type of graph in `langgraph` is the `StatefulGraph`. +This graph is parameterized by a state object that it passes around to each node. +Each node then returns operations to update that state. +These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute. +Whether to set or add is denoted by annotating the state object you construct the graph with. + +The state for the traditional LangChain agent has a few attributes: + +1. `input`: This is the input string representing the main ask from the user, passed in as input. +2. `chat_history`: This is any previous conversation messages, also passed in as input. +3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent. +4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools. + +Let's make these ideas concrete by create an agent state! + +```python +from typing import TypedDict, Annotated, Sequence, Union +from langchain_core.agents import AgentAction, AgentFinish +from langchain_core.messages import BaseMessage +import operator + + +class AgentState(TypedDict): + # The input string + input: str + # The list of previous messages in the conversation + chat_history: Sequence[BaseMessage] + # The outcome of a given call to the agent + # Needs `None` as a valid type, since this is what this will start as + agent_outcome: Union[AgentAction, AgentFinish, None] + # List of actions and corresponding observations + # Here we annotate this with `operator.add` to indicate that operations to + # this state should be ADDED to the existing values (not overwrite it) + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + +``` + ### Define the nodes We now need to define a few different nodes in our graph. @@ -93,38 +133,31 @@ The path that is taken is not known until that node is run (the LLM decides). Let's define the nodes, as well as a function to decide how what conditional edge to take. ```python -from langchain_core.runnables import RunnablePassthrough from langchain_core.agents import AgentFinish +from langgraph.prebuilt.tool_executor import ToolExecutor +# This a helper class we have that is useful for running tools +# It takes in an agent action and calls that tool and returns the result +tool_executor = ToolExecutor(tools) # Define the agent -# Note that here, we are using `.assign` to add the output of the agent to the dictionary -# This dictionary will be returned from the node -# The reason we don't want to return just the result of `agent_runnable` from this node is -# that we want to continue passing around all the other inputs -agent = RunnablePassthrough.assign( - agent_outcome = agent_runnable -) +def run_agent(data): + agent_outcome = agent_runnable.invoke(data) + return {"agent_outcome": agent_outcome} # Define the function to execute tools def execute_tools(data): # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data.pop('agent_outcome') - # Get the tool to use - tool_to_use = {t.name: t for t in tools}[agent_action.tool] - # Call that tool on the input - observation = tool_to_use.invoke(agent_action.tool_input) - # We now add in the action and the observation to the `intermediate_steps` list - # This is the list of all previous actions taken and their output - data['intermediate_steps'].append((agent_action, observation)) - return data + agent_action = data['agent_outcome'] + output = tool_executor.invoke(agent_action) + return {"intermediate_steps": [(agent_action, str(output))]} # Define logic that will be used to determine which conditional edge to go down def should_continue(data): # If the agent outcome is an AgentFinish, then we return `exit` string # This will be used when setting up the graph to define the flow if isinstance(data['agent_outcome'], AgentFinish): - return "exit" + return "end" # Otherwise, an AgentAction is returned # Here we return `continue` string # This will be used when setting up the graph to define the flow @@ -134,51 +167,51 @@ def should_continue(data): ### Define the graph -We can now put it alltogether and define the graph! +We can now put it all together and define the graph! ```python -from langgraph.graph import END, Graph +from langgraph.graph import END, StateGraph -workflow = Graph() +# Define a new graph + workflow = StateGraph(AgentState) -# Add the agent node, we give it name `agent` which we will use later -workflow.add_node("agent", agent) -# Add the tools node, we give it name `tools` which we will use later -workflow.add_node("tools", execute_tools) + # Define the two nodes we will cycle between + workflow.add_node("agent", run_agent) + workflow.add_node("action", execute_tools) -# Set the entrypoint as `agent` -# This means that this node is the first one called -workflow.set_entry_point("agent") + # Set the entrypoint as `agent` + # This means that this node is the first one called + workflow.set_entry_point("agent") -# We now add a conditional edge -workflow.add_conditional_edges( - # First, we define the start node. We use `agent`. - # This means these are the edges taken after the `agent` node is called. - "agent", - # Next, we pass in the function that will determine which node is called next. - should_continue, - # Finally we pass in a mapping. - # The keys are strings, and the values are other nodes. - # END is a special node marking that the graph should finish. - # What will happen is we will call `should_continue`, and then the output of that - # will be matched against the keys in this mapping. - # Based on which one it matches, that node will then be called. - { - # If `tools`, then we call the tool node. - "continue": "tools", - # Otherwise we finish. - "exit": END - } -) + # We now add a conditional edge + workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END + } + ) -# We now add a normal edge from `tools` to `agent`. -# This means that after `tools` is called, `agent` node is called next. -workflow.add_edge('tools', 'agent') + # We now add a normal edge from `tools` to `agent`. + # This means that after `tools` is called, `agent` node is called next. + workflow.add_edge('action', 'agent') -# Finally, we compile it! -# This compiles it into a LangChain Runnable, -# meaning you can use it as you would any other runnable -chain = workflow.compile() + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + chain = workflow.compile() ``` ### Use it! @@ -187,7 +220,7 @@ We can now use it! This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables ```python -chain.invoke({"input": "what is the weather in sf", "intermediate_steps": []}) +chain.invoke({"input": "what is the weather in sf"}) ``` ## Streaming @@ -200,7 +233,7 @@ One of the benefits of using LangGraph is that it is easy to stream output as it ```python for output in chain.stream( - {"input": "what is the weather in sf", "intermediate_steps": []} + {"input": "what is the weather in sf"} ): # stream() yields dictionaries with output keyed by node name for key, value in output.items(): @@ -213,102 +246,34 @@ for output in chain.stream( ``` Output from node 'agent': --- -{'agent_outcome': 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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), - 'input': 'what is the weather in sf', - 'intermediate_steps': []} +{'agent_outcome': 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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})])} --- -Output from node 'tools': +Output from node 'action': --- -{'input': 'what is the weather in sf', - '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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), - [{'content': 'Best time to go to San Francisco? ' - 'Weather in San Francisco in january ' - '2024 How was the weather last january? ' - 'Here is the day by day recorded weather ' - 'in San Francisco in january 2023: ' - 'Seasonal average climate and ' - 'temperature of San Francisco in ' - 'january 8% 46% 29% 12% 8% Evolution of ' - 'daily average temperature and ' - 'precipitation in San Francisco in ' - 'januaryWeather in San Francisco in ' - 'january 2024. The weather in San ' - 'Francisco in january comes from ' - 'statistical datas on the past years. ' - 'You can view the weather statistics the ' - 'entire month, but also by using the ' - 'tabs for the beginning, the middle and ' - 'the end of the month. ... 08-01-2023 ' - '52°F to 58°F. 09-01-2023 54°F to 61°F. ' - '10-01-2023 52°F to ...', - 'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/'}])]} +{'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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), "[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]")]} --- Output from node 'agent': --- -{'agent_outcome': AgentFinish(return_values={'output': 'The weather in San Francisco in January ranges from 52°F to 61°F. For more detailed and current weather information, you may want to check a reliable weather website or app.'}, log='The weather in San Francisco in January ranges from 52°F to 61°F. For more detailed and current weather information, you may want to check a reliable weather website or app.'), - 'input': 'what is the weather in sf', - '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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), - [{'content': 'Best time to go to San Francisco? ' - 'Weather in San Francisco in january ' - '2024 How was the weather last january? ' - 'Here is the day by day recorded weather ' - 'in San Francisco in january 2023: ' - 'Seasonal average climate and ' - 'temperature of San Francisco in ' - 'january 8% 46% 29% 12% 8% Evolution of ' - 'daily average temperature and ' - 'precipitation in San Francisco in ' - 'januaryWeather in San Francisco in ' - 'january 2024. The weather in San ' - 'Francisco in january comes from ' - 'statistical datas on the past years. ' - 'You can view the weather statistics the ' - 'entire month, but also by using the ' - 'tabs for the beginning, the middle and ' - 'the end of the month. ... 08-01-2023 ' - '52°F to 58°F. 09-01-2023 54°F to 61°F. ' - '10-01-2023 52°F to ...', - 'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/'}])]} +{'agent_outcome': AgentFinish(return_values={'output': "It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."}, log="It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.")} --- Output from node '__end__': --- -{'agent_outcome': AgentFinish(return_values={'output': 'The weather in San Francisco in January ranges from 52°F to 61°F. For more detailed and current weather information, you may want to check a reliable weather website or app.'}, log='The weather in San Francisco in January ranges from 52°F to 61°F. For more detailed and current weather information, you may want to check a reliable weather website or app.'), - 'input': 'what is the weather in sf', - '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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), - [{'content': 'Best time to go to San Francisco? ' - 'Weather in San Francisco in january ' - '2024 How was the weather last january? ' - 'Here is the day by day recorded weather ' - 'in San Francisco in january 2023: ' - 'Seasonal average climate and ' - 'temperature of San Francisco in ' - 'january 8% 46% 29% 12% 8% Evolution of ' - 'daily average temperature and ' - 'precipitation in San Francisco in ' - 'januaryWeather in San Francisco in ' - 'january 2024. The weather in San ' - 'Francisco in january comes from ' - 'statistical datas on the past years. ' - 'You can view the weather statistics the ' - 'entire month, but also by using the ' - 'tabs for the beginning, the middle and ' - 'the end of the month. ... 08-01-2023 ' - '52°F to 58°F. 09-01-2023 54°F to 61°F. ' - '10-01-2023 52°F to ...', - 'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/'}])]} +{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': "It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."}, log="It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."), '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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), "[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]")]} --- ``` ### Streaming LLM Tokens -You can also access the LLM tokens as they are produced by each node. In this case only the "agent" node produces LLM tokens. +You can also access the LLM tokens as they are produced by each node. +In this case only the "agent" node produces LLM tokens. +In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True)`) ```python async for output in chain.astream_log( @@ -395,20 +360,106 @@ content=')' content='' ``` + + + + +## When to Use + +When should you use this versus [LangChain Expression Language](https://python.langchain.com/docs/expression_language/)? + +If you need cycles. + +Langchain Expression Language allows you to easily define chains (DAGs) but does not have a good mechanism for adding in cycles. +`langgraph` adds that syntax. + +## Examples + + +### ChatExecutor: with function calling + +### AgentExecutor + + ## Documentation There are only a few new APIs to use. -The main new class is `Graph`. +### StateGraph + +The main entrypoint is `StateGraph`. ```python -from langgraph.graph import Graph +from langgraph.graph import StateGraph ``` This class is responsible for constructing the graph. It exposes an interface inspired by [NetworkX](https://networkx.org/documentation/latest/). +This graph is parameterized by a state object that it passes around to each node. -### `.add_node` + +#### `__init__` + +```python + def __init__(self, schema: Type[Any]) -> None: +``` + +When constructing the graph, you need to pass in a schema for a state. +Each node then returns operations to update that state. +These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute. +Whether to set or add is denoted by annotating the state object you construct the graph with. + +The recommended way to specify the schema is with a typed dictionary: `from typing import TypedDict` + +You can then annotate the different attributes using `from typing imoport Annotated`. +Currently, the only supported annotation is `import operator; operator.add`. +This annotation will make it so that any node that returns this attribute ADDS that new result to the existing value. + +Let's take a look at an example: + +```python +from typing import TypedDict, Annotated, Union +from langchain_core.agents import AgentAction, AgentFinish +import operator + + +class AgentState(TypedDict): + # The input string + input: str + # The outcome of a given call to the agent + # Needs `None` as a valid type, since this is what this will start as + agent_outcome: Union[AgentAction, AgentFinish, None] + # List of actions and corresponding observations + # Here we annotate this with `operator.add` to indicate that operations to + # this state should be ADDED to the existing values (not overwrite it) + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + +``` + +We can then use this like: + +```python +# Initialize the StateGraph with this state +graph = StateGraph(AgentState) +# Create nodes and edges +... +# Compile the graph +app = graph.compile() + +# The inputs should be a dictionary, because the state is a TypedDict +inputs = { + # Let's assume this the input + "input": "hi" + # Let's assume agent_outcome is set by the graph as some point + # It doesn't need to be provided, and it will be None by default + # Let's assume `intermediate_steps` is built up over time by the graph + # It doesn't need to provided, and it will be empty list by default + # The reason `intermediate_steps` is an empty list and not `None` is because + # it's annotated with `operator.add` +} +``` + +#### `.add_node` ```python def add_node(self, key: str, action: RunnableLike) -> None: @@ -420,7 +471,7 @@ It takes two arguments: - `key`: A string representing the name of the node. This must be unique. - `action`: The action to take when this node is called. This should either be a function or a runnable. -### `.add_edge` +#### `.add_edge` ```python def add_edge(self, start_key: str, end_key: str) -> None: @@ -433,7 +484,7 @@ It takes two arguments. - `start_key`: A string representing the name of the start node. This key must have already been registered in the graph. - `end_key`: A string representing the name of the end node. This key must have already been registered in the graph. -### `.add_conditional_edges` +#### `.add_conditional_edges` ```python def add_conditional_edges( @@ -452,7 +503,7 @@ This takes three arguments: - `condition`: A function to call to decide what to do next. The input will be the output of the start node. It should return a string that is present in `conditional_edge_mapping` and represents the edge to take. - `conditional_edge_mapping`: A mapping of string to string. The keys should be strings that may be returned by `condition`. The values should be the downstream node to call if that condition is returned. -### `.set_entry_point` +#### `.set_entry_point` ```python def set_entry_point(self, key: str) -> None: @@ -464,7 +515,7 @@ It only takes one argument: - `key`: The name of the node that should be called first. -### `.set_finish_point` +#### `.set_finish_point` ```python def set_finish_point(self, key: str) -> None: @@ -478,6 +529,17 @@ It only has one argument: Note: This does not need to be called if at any point you previously created an edge (conditional or normal) to `END` +### Graph + +```python +from langgraph.graph import Graph + +graph = Graph() +``` + +This has the same interface as `StateGraph` with the exception that it doesn't update a state object over time, and rather relies on passing around the full state from each step. +This means that whatever is returned from one node is the input to the next as is. + ### `END` ```python @@ -491,89 +553,86 @@ It can be used in two places: - As the `end_key` in `add_edge` - As a value in `conditional_edge_mapping` as passed to `add_conditional_edges` -## When to Use -When should you use this versus [LangChain Expression Language](https://python.langchain.com/docs/expression_language/)? +## Prebuilt Examples -If you need cycles. +There are also a few methods we've added to make it easy to use common, prebuilt graphs and components. -Langchain Expression Language allows you to easily define chains (DAGs) but does not have a good mechanism for adding in cycles. -`langgraph` adds that syntax. - -## Examples - -### AgentExecutor - -See the above Quick Start for an example of re-creating the LangChain [`AgentExecutor`](https://python.langchain.com/docs/modules/agents/concepts#agentexecutor) class. - -### Forced Function Calling - -One simple modification of the above Graph is to modify it such that a certain tool is always called first. -This can be useful if you want to enforce a certain tool is called, but still want to enable agentic behavior after the fact. - -Assuming you have done the above Quick Start, you can build off it like: - -#### Define the first tool call - -Here, we manually define the first tool call that we will make. -Notice that it does that same thing as `agent` would have done (adds the `agent_outcome` key). -This is so that we can easily plug it in. +### ToolExecutor ```python -from langchain_core.agents import AgentActionMessageLog - -def first_agent(inputs): - action = AgentActionMessageLog( - # We force call this tool - tool="tavily_search_results_json", - # We just pass in the `input` key to this tool - tool_input=inputs["input"], - log="", - message_log=[] - ) - inputs["agent_outcome"] = action - return inputs +from langgraph.prebuilt import ToolExecutor ``` -#### Create the graph - -We can now create a new graph with this new node +This is a simple helper class to help with calling tools. +It is parameterized by a list of tools: ```python -workflow = Graph() - -# Add the same nodes as before, plus this "first agent" -workflow.add_node("first_agent", first_agent) -workflow.add_node("agent", agent) -workflow.add_node("tools", execute_tools) - -# We now set the entry point to be this first agent -workflow.set_entry_point("first_agent") - -# We define the same edges as before -workflow.add_conditional_edges( - "agent", - should_continue, - { - "continue": "tools", - "exit": END - } -) -workflow.add_edge('tools', 'agent') - -# We also define a new edge, from the "first agent" to the tools node -# This is so that we can call the tool -workflow.add_edge('first_agent', 'tools') - -# We now compile the graph as before -chain = workflow.compile() +tools = [...] +tool_executor = ToolExecutor(tools) ``` -#### Use it! +It then exposes a [runnable interface](https://python.langchain.com/docs/expression_language/interface). +It can be used to call tools: you can pass in an [AgentAction](https://python.langchain.com/docs/modules/agents/concepts#agentaction) and it will look up the relevant tool and call it with the appropriate input. -We can now use it as before! -Depending on whether or not the first tool call is actually useful, this may save you an LLM call or two. +### chat_executor.create_function_calling_executor ```python -chain.invoke({"input": "what is the weather in sf", "intermediate_steps": []}) +from langgraph.prebuilt import chat_executor +``` + +This is a helper function for creating a graph that works with a chat model that utilizes function calling. +Can be created by passing in a model and a list of tools. +The model must be one that supports OpenAI function calling. + +```python +from langchain_openai import ChatOpenAI +from langchain_community.tools.tavily_search import TavilySearchResults +from langgraph.prebuilt import chat_executor +from langchain_core.messages import HumanMessage + +tools = [TavilySearchResults(max_results=1)] +model = ChatOpenAI() + +app = chat_executor.create_function_calling_executor(model, tools) + +inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} +for s in app.stream(inputs): + print(list(s.values())[0]) + print("----") +``` + +### create_agent_executor + +```python +from langgraph.prebuilt import create_agent_executor +``` + +This is a helper function for creating a graph that works with [LangChain Agents](https://python.langchain.com/docs/modules/agents/). +Can be created by passing in an agent and a list of tools. + +```python +from langgraph.prebuilt import create_agent_executor +from langchain_openai import ChatOpenAI +from langchain import hub +from langchain.agents import create_openai_functions_agent +from langchain_community.tools.tavily_search import TavilySearchResults + +tools = [TavilySearchResults(max_results=1)] + +# Get the prompt to use - you can modify this! +prompt = hub.pull("hwchase17/openai-functions-agent") + +# Choose the LLM that will drive the agent +llm = ChatOpenAI(model="gpt-3.5-turbo-1106") + +# Construct the OpenAI Functions agent +agent_runnable = create_openai_functions_agent(llm, tools, prompt) + +app = create_agent_executor(agent_runnable, tools) + +inputs = {"input": "what is the weather in sf", "chat_history": []} +for s in app.stream(inputs): + print(list(s.values())[0]) + print("----") ``` diff --git a/examples/agent_executor.ipynb b/examples/agent_executor.ipynb deleted file mode 100644 index 610a74998..000000000 --- a/examples/agent_executor.ipynb +++ /dev/null @@ -1,104 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "e2fdcac4-d134-402b-b423-b0cf4b939f5d", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.agent_executor import create_agent_executor\n", - "from langchain_openai import ChatOpenAI\n", - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.tools.tavily_search import TavilySearchResults" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "afb59979-c7a3-435f-b147-f8d501f6ff13", - "metadata": {}, - "outputs": [], - "source": [ - "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)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "9be722f0-c9ab-4bd2-af27-66adf51134d2", - "metadata": {}, - "outputs": [], - "source": [ - "app = create_agent_executor(agent_runnable, tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "019b591b-fd71-4ee8-ae94-06d0e2dc6a4d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", - "----\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "----\n", - "{'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"}, log=\"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\")}\n", - "----\n", - "{'input': 'what is the weather in sf', 'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"}, log=\"It seems that I couldn't retrieve the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date weather forecast for San Francisco.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "----\n" - ] - } - ], - "source": [ - "inputs = {\"input\": \"what is the weather in sf\"}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "162f647a-36b4-45c3-a171-c8452b05af01", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb new file mode 100644 index 000000000..83b17b20e --- /dev/null +++ b/examples/agent_executor/base.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f725852e-71ef-4615-8cac-011a516fbe72", + "metadata": {}, + "source": [ + "# Agent Executor From Scratch\n", + "\n", + "In this notebook we will go over how to build a basic agent executor from scratch." + ] + }, + { + "cell_type": "markdown", + "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", + "metadata": {}, + "source": [ + "## Create the LangChain agent\n", + "\n", + "First, we will create the LangChain agent. For more information on LangChain agents, see [this documentation](https://python.langchain.com/docs/modules/agents/)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\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\", streaming=True)\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" + ] + }, + { + "cell_type": "markdown", + "id": "972e58b3-fe3c-449d-b3c4-8fa2217afd07", + "metadata": {}, + "source": [ + "## Define the graph state\n", + "\n", + "We now define the graph state. The state for the traditional LangChain agent has a few attributes:\n", + "\n", + "1. `input`: This is the input string representing the main ask from the user, passed in as input.\n", + "2. `chat_history`: This is any previous conversation messages, also passed in as input.\n", + "3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent.\n", + "4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, List, Union\n", + "from langchain_core.agents import AgentAction, AgentFinish\n", + "from langchain_core.messages import BaseMessage\n", + "import operator\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The input string\n", + " input: str\n", + " # The list of previous messages in the conversation\n", + " chat_history: list[BaseMessage]\n", + " # The outcome of a given call to the agent\n", + " # Needs `None` as a valid type, since this is what this will start as\n", + " agent_outcome: Union[AgentAction, AgentFinish, None]\n", + " # List of actions and corresponding observations\n", + " # Here we annotate this with `operator.add` to indicate that operations to\n", + " # this state should be ADDED to the existing values (not overwrite it)\n", + " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" + ] + }, + { + "cell_type": "markdown", + "id": "cd27b281-cc9a-49c9-be78-8b98a7d905c4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentFinish\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "\n", + "# This a helper class we have that is useful for running tools\n", + "# It takes in an agent action and calls that tool and returns the result\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Define the agent\n", + "def run_agent(data):\n", + " agent_outcome = agent_runnable.invoke(data)\n", + " return {\"agent_outcome\": agent_outcome}\n", + "\n", + "# Define the function to execute tools\n", + "def execute_tools(data):\n", + " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", + " agent_action = data['agent_outcome']\n", + " output = tool_executor.invoke(agent_action)\n", + " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", + "\n", + "# Define logic that will be used to determine which conditional edge to go down\n", + "def should_continue(data):\n", + " # If the agent outcome is an AgentFinish, then we return `exit` string\n", + " # This will be used when setting up the graph to define the flow\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"end\"\n", + " # Otherwise, an AgentAction is returned\n", + " # Here we return `continue` string\n", + " # This will be used when setting up the graph to define the flow\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "markdown", + "id": "c0b211f4-0c5c-4792-b18d-cd70907c71e7", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", run_agent)\n", + "workflow.add_node(\"action\", execute_tools)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "214ae46e-c297-465d-86db-2b0312ed3530", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", + "----\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"}, log=\"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\")}\n", + "----\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"}, log=\"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/agent_executor/force-calling-a-tool-first.ipynb b/examples/agent_executor/force-calling-a-tool-first.ipynb new file mode 100644 index 000000000..70712a949 --- /dev/null +++ b/examples/agent_executor/force-calling-a-tool-first.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\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\", streaming=True)\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, List, Union\n", + "from langchain_core.agents import AgentAction, AgentFinish\n", + "from langchain_core.messages import BaseMessage\n", + "import operator\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The input string\n", + " input: str\n", + " # The list of previous messages in the conversation\n", + " chat_history: list[BaseMessage]\n", + " # The outcome of a given call to the agent\n", + " # Needs `None` as a valid type, since this is what this will start as\n", + " agent_outcome: Union[AgentAction, AgentFinish, None]\n", + " # List of actions and corresponding observations\n", + " # Here we annotate this with `operator.add` to indicate that operations to\n", + " # this state should be ADDED to the existing values (not overwrite it)\n", + " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentFinish\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "\n", + "# This a helper class we have that is useful for running tools\n", + "# It takes in an agent action and calls that tool and returns the result\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Define the agent\n", + "def run_agent(data):\n", + " agent_outcome = agent_runnable.invoke(data)\n", + " return {\"agent_outcome\": agent_outcome}\n", + "\n", + "# Define the function to execute tools\n", + "def execute_tools(data):\n", + " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", + " agent_action = data['agent_outcome']\n", + " output = tool_executor.invoke(agent_action)\n", + " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", + "\n", + "# Define logic that will be used to determine which conditional edge to go down\n", + "def should_continue(data):\n", + " # If the agent outcome is an AgentFinish, then we return `exit` string\n", + " # This will be used when setting up the graph to define the flow\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"end\"\n", + " # Otherwise, an AgentAction is returned\n", + " # Here we return `continue` string\n", + " # This will be used when setting up the graph to define the flow\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4883e47a-0a15-429c-bf31-1e8afe982a77", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'tavily_search_results_json'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tools[0].name" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fb16db55-ff1a-4e16-94c1-dcb8b2a8f0ba", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentActionMessageLog\n", + "\n", + "def first_agent(inputs):\n", + " action = AgentActionMessageLog(\n", + " # We force call this tool\n", + " tool=\"tavily_search_results_json\",\n", + " # We just pass in the `input` key to this tool\n", + " tool_input=inputs[\"input\"],\n", + " log=\"\",\n", + " message_log=[]\n", + " )\n", + " return {\"agent_outcome\": action}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", run_agent)\n", + "workflow.add_node(\"action\", execute_tools)\n", + "workflow.add_node(\"first_agent\", first_agent)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"first_agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "workflow.add_edge('first_agent', 'action')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "chain = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "214ae46e-c297-465d-86db-2b0312ed3530", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'first_agent':\n", + "---\n", + "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[])}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\")]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"}, log=\"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\")}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"}, log=\"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "for output in chain.stream(\n", + " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/agent_executor/high-level.ipynb b/examples/agent_executor/high-level.ipynb new file mode 100644 index 000000000..f2d853d5a --- /dev/null +++ b/examples/agent_executor/high-level.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f961801a-6025-4b73-be3b-c3a8a75d4167", + "metadata": {}, + "source": [ + "# Agent Executor\n", + "\n", + "This notebook walks through an example creating an agent executor to work with an existing LangChain agent.\n", + "This is useful for getting started quickly.\n", + "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder." + ] + }, + { + "cell_type": "markdown", + "id": "6ae180d9-abd3-4a44-8fb1-a2c89434fbeb", + "metadata": {}, + "source": [ + "## Set up LangChain Agent\n", + "\n", + "First, will set up our LangChain Agent. \n", + "See documentation [here](https://python.langchain.com/docs/modules/agents/) for more information on what these agents are and how to think about them" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e2fdcac4-d134-402b-b423-b0cf4b939f5d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_community.tools.tavily_search import TavilySearchResults" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "afb59979-c7a3-435f-b147-f8d501f6ff13", + "metadata": {}, + "outputs": [], + "source": [ + "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)" + ] + }, + { + "cell_type": "markdown", + "id": "0bcb5ff8-b2d1-4fb2-bed4-3726f96db772", + "metadata": {}, + "source": [ + "## Create agent executor\n", + "\n", + "Now we will use the high level method to create the agent executor" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7a138eb4-a469-4b30-a059-99d6ea944648", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import create_agent_executor" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9be722f0-c9ab-4bd2-af27-66adf51134d2", + "metadata": {}, + "outputs": [], + "source": [ + "app = create_agent_executor(agent_runnable, tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "019b591b-fd71-4ee8-ae94-06d0e2dc6a4d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", + "----\n", + "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoWeather ☀ ⛅ San Francisco ☀ ⛅ January ☀ ⛅ Information on temperature, sunshine hours, water temperature & rainfall in January for San Francisco. ... Are you planning a holiday with hopefully nice weather in San Francisco in January 2024? Here you can find all information about the weather in San Francisco in January: ... 15. January ...'}]\")]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\")}\n", + "----\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can visit a reliable weather website or check a weather app for the most up-to-date information.\"), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"current weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoWeather ☀ ⛅ San Francisco ☀ ⛅ January ☀ ⛅ Information on temperature, sunshine hours, water temperature & rainfall in January for San Francisco. ... Are you planning a holiday with hopefully nice weather in San Francisco in January 2024? Here you can find all information about the weather in San Francisco in January: ... 15. January ...'}]\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "c6a664cd-083e-4d85-aeaf-501463881f05", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentFinish(return_values={'output': \"I couldn't find the current weather in San Francisco. However, you can check the weather on a reliable weather website or using a weather app for the most up-to-date information.\"}, log=\"I couldn't find the current weather in San Francisco. However, you can check the weather on a reliable weather website or using a weather app for the most up-to-date information.\")" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s['__end__']['agent_outcome']" + ] + }, + { + "cell_type": "markdown", + "id": "a7bd3e55-ee7e-4276-81bd-39e6131fcf77", + "metadata": {}, + "source": [ + "## Custom Input Schema\n", + "\n", + "By default, the `create_agent_executor` assumes that the input will be a dictionary with two keys: `input` and `chat_history`. \n", + "If this is not the case, you can easily customize the input schema.\n", + "You should do this, by defining a schema as a TypedDict.\n", + "\n", + "For this example, we will create a new agent that expects `question` and `language` as inputs." + ] + }, + { + "cell_type": "markdown", + "id": "a98c5ec5-f836-4b3c-b37b-00102b496366", + "metadata": {}, + "source": [ + "### Create New Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "676841ec-b5a6-495e-a88a-7eb0ab3cbae6", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "\n", + "prompt = ChatPromptTemplate.from_messages([\n", + " (\"human\", \"Respond to the user question: {question}. Answer in this language: {language}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\")\n", + "])\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" + ] + }, + { + "cell_type": "markdown", + "id": "5889d980-d209-447b-8489-1d4873acfdc2", + "metadata": {}, + "source": [ + "### Define Input Schema" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3d1df06d-1564-46a1-a72f-58dfc65927bc", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2fdbb687-9c72-42c7-afcb-3f8940f3e5f4", + "metadata": {}, + "outputs": [], + "source": [ + "class InputSchema(TypedDict):\n", + " question: str\n", + " language: str" + ] + }, + { + "cell_type": "markdown", + "id": "329bb518-02a8-477c-8898-d04cb64fc460", + "metadata": {}, + "source": [ + "### Create new agent executor" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1ad88990-896d-48d5-bd34-01c9f6a37734", + "metadata": {}, + "outputs": [], + "source": [ + "app = create_agent_executor(agent_runnable, tools, input_schema=InputSchema)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "13ffa18c-9a9f-4e0e-8298-32aeff94ce5d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})])}\n", + "----\n", + "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})]), '[{\\'url\\': \\'https://www.mxbars.net/2024/01/14/san-francisco-supercross-2024-results-and-points-video/\\', \\'content\\': \"Scritto domenica 14 Gennaio 2024 alle 04:38. SAN FRANCISCO Oracle Park, CA January 13, 2024 sera gli orari sono anticipati di due ore causa mal tempo per non compromettere lo spettacolo! secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non . Commenta la gara\\\\xa0CLICCANDO\\\\xa0il link! http://forum.mxbars.net/viewtopic.php?f=18&t=50182SAN FRANCISCO. Oracle Park, CA. January 13, 2024. Ecco che la NUOVA stagione del Monster Energy Supercross 2024 continua, dopo Anaheim 1 si passa al secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non vede possibilità di chiudersi, per la lotta nella 450 dove sono tutti agguerriti e quest\\'anno il livello è ancora più alto ...\"}]')]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': 'Al momento sta piovendo a San Francisco.'}, log='Al momento sta piovendo a San Francisco.')}\n", + "----\n", + "{'question': 'what is the weather in sf', 'language': 'italian', 'agent_outcome': AgentFinish(return_values={'output': 'Al momento sta piovendo a San Francisco.'}, log='Al momento sta piovendo a San Francisco.'), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'che tempo fa a sf'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'che tempo fa a sf'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\"query\":\"che tempo fa a sf\"}', 'name': 'tavily_search_results_json'}})]), '[{\\'url\\': \\'https://www.mxbars.net/2024/01/14/san-francisco-supercross-2024-results-and-points-video/\\', \\'content\\': \"Scritto domenica 14 Gennaio 2024 alle 04:38. SAN FRANCISCO Oracle Park, CA January 13, 2024 sera gli orari sono anticipati di due ore causa mal tempo per non compromettere lo spettacolo! secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non . Commenta la gara\\\\xa0CLICCANDO\\\\xa0il link! http://forum.mxbars.net/viewtopic.php?f=18&t=50182SAN FRANCISCO. Oracle Park, CA. January 13, 2024. Ecco che la NUOVA stagione del Monster Energy Supercross 2024 continua, dopo Anaheim 1 si passa al secondo weekend con la seconda tappa e pista molto tecnica per San Francisco dove sta piovendo e lo stadio aperto non vede possibilità di chiudersi, per la lotta nella 450 dove sono tutti agguerriti e quest\\'anno il livello è ancora più alto ...\"}]')]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"question\": \"what is the weather in sf\", \"language\": \"italian\"}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "fd60f5d6-bd4b-4995-80dd-63f268c17cff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentFinish(return_values={'output': 'Il clima a San Francisco durante il mese di gennaio è generalmente fresco con temperature medie di circa 9.6°C (49.2°F) e massime di 14°C (57.3°F). Si consiglia di prepararsi a temperature fresche se si pianifica una visita a San Francisco in gennaio.'}, log='Il clima a San Francisco durante il mese di gennaio è generalmente fresco con temperature medie di circa 9.6°C (49.2°F) e massime di 14°C (57.3°F). Si consiglia di prepararsi a temperature fresche se si pianifica una visita a San Francisco in gennaio.')" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s['__end__']['agent_outcome']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20cac1a0-0c51-4cbd-ae27-929d71db2b56", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/agent_executor/human-in-the-loop.ipynb b/examples/agent_executor/human-in-the-loop.ipynb new file mode 100644 index 000000000..3aa623330 --- /dev/null +++ b/examples/agent_executor/human-in-the-loop.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\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\", streaming=True)\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, List, Union\n", + "from langchain_core.agents import AgentAction, AgentFinish\n", + "from langchain_core.messages import BaseMessage\n", + "import operator\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The input string\n", + " input: str\n", + " # The list of previous messages in the conversation\n", + " chat_history: list[BaseMessage]\n", + " # The outcome of a given call to the agent\n", + " # Needs `None` as a valid type, since this is what this will start as\n", + " agent_outcome: Union[AgentAction, AgentFinish, None]\n", + " # List of actions and corresponding observations\n", + " # Here we annotate this with `operator.add` to indicate that operations to\n", + " # this state should be ADDED to the existing values (not overwrite it)\n", + " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentFinish\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "\n", + "# This a helper class we have that is useful for running tools\n", + "# It takes in an agent action and calls that tool and returns the result\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Define the agent\n", + "def run_agent(data):\n", + " agent_outcome = agent_runnable.invoke(data)\n", + " return {\"agent_outcome\": agent_outcome}\n", + "\n", + "# Define the function to execute tools\n", + "def execute_tools(data):\n", + " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", + " agent_action = data['agent_outcome']\n", + " response = input(prompt=f\"[y/n] continue with: {agent_action}?\")\n", + " if response == \"n\":\n", + " raise ValueError\n", + " output = tool_executor.invoke(agent_action)\n", + " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", + "\n", + "# Define logic that will be used to determine which conditional edge to go down\n", + "def should_continue(data):\n", + " # If the agent outcome is an AgentFinish, then we return `exit` string\n", + " # This will be used when setting up the graph to define the flow\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"end\"\n", + " # Otherwise, an AgentAction is returned\n", + " # Here we return `continue` string\n", + " # This will be used when setting up the graph to define the flow\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", run_agent)\n", + "workflow.add_node(\"action\", execute_tools)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "chain = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "214ae46e-c297-465d-86db-2b0312ed3530", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] continue with: 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]? y\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'action':\n", + "---\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"}, log=\"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\")}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"}, log=\"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "for output in chain.stream(\n", + " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/agent_executor/managing-agent-steps.ipynb b/examples/agent_executor/managing-agent-steps.ipynb new file mode 100644 index 000000000..7d7ffa498 --- /dev/null +++ b/examples/agent_executor/managing-agent-steps.ipynb @@ -0,0 +1,226 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\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\", streaming=True)\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, List, Union\n", + "from langchain_core.agents import AgentAction, AgentFinish\n", + "from langchain_core.messages import BaseMessage\n", + "import operator\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The input string\n", + " input: str\n", + " # The list of previous messages in the conversation\n", + " chat_history: list[BaseMessage]\n", + " # The outcome of a given call to the agent\n", + " # Needs `None` as a valid type, since this is what this will start as\n", + " agent_outcome: Union[AgentAction, AgentFinish, None]\n", + " # List of actions and corresponding observations\n", + " # Here we annotate this with `operator.add` to indicate that operations to\n", + " # this state should be ADDED to the existing values (not overwrite it)\n", + " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.agents import AgentFinish\n", + "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "\n", + "# This a helper class we have that is useful for running tools\n", + "# It takes in an agent action and calls that tool and returns the result\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Define the agent\n", + "def run_agent(data):\n", + " inputs = data.copy()\n", + " if len(inputs['intermediate_steps']) > 5:\n", + " inputs['intermediate_steps'] = inputs['intermediate_steps'][-5:]\n", + " agent_outcome = agent_runnable.invoke(inputs)\n", + " return {\"agent_outcome\": agent_outcome}\n", + "\n", + "# Define the function to execute tools\n", + "def execute_tools(data):\n", + " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", + " agent_action = data['agent_outcome']\n", + " output = tool_executor.invoke(agent_action)\n", + " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", + "\n", + "# Define logic that will be used to determine which conditional edge to go down\n", + "def should_continue(data):\n", + " # If the agent outcome is an AgentFinish, then we return `exit` string\n", + " # This will be used when setting up the graph to define the flow\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"end\"\n", + " # Otherwise, an AgentAction is returned\n", + " # Here we return `continue` string\n", + " # This will be used when setting up the graph to define the flow\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", run_agent)\n", + "workflow.add_node(\"action\", execute_tools)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "chain = workflow.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "214ae46e-c297-465d-86db-2b0312ed3530", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"}, log=\"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\")}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"}, log=\"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "for output in chain.stream(\n", + " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/messages_executor_how_to/base.ipynb b/examples/chat_executor_with_function_calling/base.ipynb similarity index 100% rename from examples/messages_executor_how_to/base.ipynb rename to examples/chat_executor_with_function_calling/base.ipynb diff --git a/examples/messages_executor_how_to/dynamically_returning_directly.ipynb b/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb similarity index 100% rename from examples/messages_executor_how_to/dynamically_returning_directly.ipynb rename to examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb diff --git a/examples/messages_executor_how_to/force-calling-a-tool-first.ipynb b/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb similarity index 100% rename from examples/messages_executor_how_to/force-calling-a-tool-first.ipynb rename to examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb diff --git a/examples/messages_executor.ipynb b/examples/chat_executor_with_function_calling/high-level.ipynb similarity index 67% rename from examples/messages_executor.ipynb rename to examples/chat_executor_with_function_calling/high-level.ipynb index e0e145a98..9eed3d07c 100644 --- a/examples/messages_executor.ipynb +++ b/examples/chat_executor_with_function_calling/high-level.ipynb @@ -1,5 +1,28 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "8bcd1a3d-7c50-4f58-be4e-1ed654aa33be", + "metadata": {}, + "source": [ + "# Chat Executor: with function calling\n", + "\n", + "This notebook walks through an example creating a chat executor that uses function calling.\n", + "This is useful for getting started quickly.\n", + "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder." + ] + }, + { + "cell_type": "markdown", + "id": "e130cf70-a30e-47d7-8fd5-464f1a92e374", + "metadata": {}, + "source": [ + "## Set up the chat model and tools\n", + "\n", + "Here we will define the chat model and tools that we want to use.\n", + "Importantly, this model MUST support OpenAI function calling." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -7,10 +30,9 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", + "from langgraph.prebuilt import chat_executor\n", "from langchain_core.messages import HumanMessage" ] }, @@ -25,6 +47,16 @@ "model = ChatOpenAI()" ] }, + { + "cell_type": "markdown", + "id": "43064805-2ac9-4b5a-850c-a68dd7282350", + "metadata": {}, + "source": [ + "## Create executor\n", + "\n", + "We can now use the high level interface to create the executor" + ] + }, { "cell_type": "code", "execution_count": 3, @@ -32,7 +64,15 @@ "metadata": {}, "outputs": [], "source": [ - "app = create_messages_executor(model, tools)" + "app = chat_executor.create_function_calling_executor(model, tools)" + ] + }, + { + "cell_type": "markdown", + "id": "d63dbfc7-a5c1-4a03-991c-f0789ba52c52", + "metadata": {}, + "source": [ + "We can now invoke this executor. The input to this must be a dictionary with a single `messsages` key that contains a list of messages." ] }, { @@ -49,9 +89,9 @@ "----\n", "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", "----\n", - "{'messages': [AIMessage(content=\"I apologize, but I couldn't retrieve the current weather information for San Francisco. However, you can check the weather history for January 2024 in San Francisco on this website: [San Francisco Weather History](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't retrieve the current weather information for San Francisco. However, you can check the weather history for January 2024 in San Francisco on this website: [San Francisco Weather History](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", "----\n" ] } diff --git a/examples/messages_executor_how_to/human-in-the-loop.ipynb b/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb similarity index 100% rename from examples/messages_executor_how_to/human-in-the-loop.ipynb rename to examples/chat_executor_with_function_calling/human-in-the-loop.ipynb diff --git a/examples/messages_executor_how_to/managing-agent-steps.ipynb b/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb similarity index 100% rename from examples/messages_executor_how_to/managing-agent-steps.ipynb rename to examples/chat_executor_with_function_calling/managing-agent-steps.ipynb diff --git a/examples/messages_executor_how_to/respond-in-format.ipynb b/examples/chat_executor_with_function_calling/respond-in-format.ipynb similarity index 100% rename from examples/messages_executor_how_to/respond-in-format.ipynb rename to examples/chat_executor_with_function_calling/respond-in-format.ipynb diff --git a/examples/combine_docs.ipynb b/examples/combine_docs.ipynb deleted file mode 100644 index d981c2444..000000000 --- a/examples/combine_docs.ipynb +++ /dev/null @@ -1,371 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "780c1001-557c-4b03-8ebd-a2a381d5f85d", - "metadata": {}, - "source": [ - "# Combine Docs\n", - "\n", - "LangGraph is a great choice for implementating workflows that involve operating over longer documents because of its recursive nature" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "624c452c-ddd5-4390-9065-7ec55dc64b96", - "metadata": {}, - "outputs": [], - "source": [ - "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\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 langgraph.pregel import Channel, Pregel\n", - "from langgraph.channels import Topic" - ] - }, - { - "cell_type": "markdown", - "id": "271728d7-b3c8-4ec6-a728-19835e282ec3", - "metadata": {}, - "source": [ - "## Stuff Documents\n", - "\n", - "Stuff documents is simple - just a chain" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0462aff0-1b88-49cc-bfe2-3c169d5e1d63", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.schema.runnable import RunnableLambda" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "59d6430b-c113-4498-9ffc-f4623f7a0b5c", - "metadata": {}, - "outputs": [], - "source": [ - "DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template=\"{page_content}\")\n", - "\n", - "_combine_documents = RunnableLambda(\n", - " lambda x: format_document(x, DEFAULT_DOCUMENT_PROMPT)\n", - ").map() | (lambda x: \"\\n\\n\".join(x))" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "29b2668d-e4a6-4876-9b04-bdc841774c62", - "metadata": {}, - "outputs": [], - "source": [ - "docs = [\n", - " Document(page_content=\"Harrison used to work at Kensho\"),\n", - " Document(page_content=\"Ankush worked at Facebook\"),\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "17da58b7-8685-4d0a-9a47-c398c085d477", - "metadata": {}, - "outputs": [], - "source": [ - "stuff_chain = (\n", - " {\n", - " \"question\": lambda x: x[\"question\"],\n", - " \"context\": (lambda x: x[\"docs\"]) | _combine_documents,\n", - " }\n", - " | ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"Answer user questions based on the following documents:\\n\\n{context}\",\n", - " ),\n", - " (\"human\", \"{question}\"),\n", - " ]\n", - " )\n", - " | ChatOpenAI()\n", - " | StrOutputParser()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "87295b71-0afc-4901-b57c-a7b945aa4bd9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Harrison used to work at Kensho.'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "stuff_chain.invoke({\"question\": \"where did harrison work\", \"docs\": docs})" - ] - }, - { - "cell_type": "markdown", - "id": "fff324c1-7fbf-41e5-861f-a10ba0112dbd", - "metadata": {}, - "source": [ - "## Reduce Documents\n", - "\n", - "Reduce documents tries to merge documents recursively." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "b15f5abb-1cfe-4965-a021-c891506c5dd2", - "metadata": {}, - "outputs": [], - "source": [ - "many_docs = docs * 5" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ccad04a3-fd3f-4e73-b895-29e53535f000", - "metadata": {}, - "outputs": [], - "source": [ - "def _split_list_of_docs(docs, max_length=70):\n", - " new_result_doc_list = []\n", - " _sub_result_docs = []\n", - " for doc in docs:\n", - " _sub_result_docs.append(doc)\n", - " _num_tokens = sum([len(d.page_content) for d in _sub_result_docs])\n", - " if _num_tokens > max_length:\n", - " if len(_sub_result_docs) == 1:\n", - " raise ValueError(\n", - " \"A single document was longer than the context length,\"\n", - " \" we cannot handle this.\"\n", - " )\n", - " new_result_doc_list.append(_sub_result_docs[:-1])\n", - " _sub_result_docs = _sub_result_docs[-1:]\n", - " new_result_doc_list.append(_sub_result_docs)\n", - " return new_result_doc_list" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "11cfd337-9f3b-4b26-ba30-251e17b18994", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[[Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')],\n", - " [Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')],\n", - " [Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')],\n", - " [Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')],\n", - " [Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')]]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Just to show what its like split\n", - "split_docs = _split_list_of_docs(many_docs)\n", - "split_docs" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "8d524ba6-0939-4a5d-8db0-4fa1ef06eaeb", - "metadata": {}, - "outputs": [], - "source": [ - "channels = {\n", - " # input\n", - " \"docs\": Topic(Document),\n", - " # intermediate\n", - " \"docs_to_finalize\": Topic(Document),\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "67370694-86f4-4b64-9d4f-38b2e306abeb", - "metadata": {}, - "outputs": [], - "source": [ - "def decide(docs: list[Document]) -> Runnable:\n", - " if len(_split_list_of_docs(docs)) > 1:\n", - " # send back to the beginning if we still need to collapse more\n", - " return Channel.write_to(\"docs\")\n", - " else:\n", - " # send to the finalizer if we're ready to produce final answer\n", - " return Channel.write_to(\"docs_to_finalize\")\n", - "\n", - "\n", - "def split_docs_with_question(input: dict[str, str | list[Document]]) -> list[dict[str, str | list[Document]]]:\n", - " return [\n", - " {\"docs\": docs, \"question\": input[\"question\"]}\n", - " for docs in _split_list_of_docs(input[\"docs\"])\n", - " ]\n", - "\n", - "\n", - "collapse = (\n", - " Channel.subscribe_to([\"docs\", \"question\"])\n", - " | split_docs_with_question\n", - " | stuff_chain.map() # Collapse each list of docs to a single string\n", - " | (lambda x: [Document(page_content=s) for s in x]) # A new (smaller) list of docs\n", - " | decide\n", - ")\n", - "\n", - "# Convert final set of docs to an answer\n", - "finalize = (\n", - " Channel.subscribe_to(\"docs_to_finalize\", key=\"docs\").join([\"question\"])\n", - " | stuff_chain\n", - " | Channel.write_to(\"answer\")\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "3019e7d2-ab7f-4868-b43c-ad898d824a26", - "metadata": {}, - "outputs": [], - "source": [ - "reduce_chain = Pregel(\n", - " chains={\n", - " \"collapse\": collapse,\n", - " \"finalize\": finalize,\n", - " },\n", - " channels=channels,\n", - " input=[\"question\", \"docs\"],\n", - " output=\"answer\",\n", - " debug=True,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "69fcb829-3dae-432a-8db3-11bbb179a7d2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[36;1m\u001b[1;3m[pregel/step]\u001b[0m \u001b[1mStarting step 0 with 1 task. Next tasks:\n", - "\u001b[0m- collapse({'docs': [Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook'),\n", - " Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook'),\n", - " Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook'),\n", - " Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook'),\n", - " Document(page_content='Harrison used to work at Kensho'),\n", - " Document(page_content='Ankush worked at Facebook')],\n", - " 'question': 'where did harrison work'})\n", - "\u001b[36;1m\u001b[1;3m[pregel/checkpoint]\u001b[0m \u001b[1mFinishing step 0. Channel values:\n", - "\u001b[0m{'docs': [...], 'docs_to_finalize': [], 'question': 'where did harrison work'}\n", - "\u001b[36;1m\u001b[1;3m[pregel/step]\u001b[0m \u001b[1mStarting step 1 with 1 task. Next tasks:\n", - "\u001b[0m- collapse({'docs': [Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.')],\n", - " 'question': 'where did harrison work'})\n", - "\u001b[36;1m\u001b[1;3m[pregel/checkpoint]\u001b[0m \u001b[1mFinishing step 1. Channel values:\n", - "\u001b[0m{'docs': [...], 'docs_to_finalize': [], 'question': 'where did harrison work'}\n", - "\u001b[36;1m\u001b[1;3m[pregel/step]\u001b[0m \u001b[1mStarting step 2 with 1 task. Next tasks:\n", - "\u001b[0m- collapse({'docs': [Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.')],\n", - " 'question': 'where did harrison work'})\n", - "\u001b[36;1m\u001b[1;3m[pregel/checkpoint]\u001b[0m \u001b[1mFinishing step 2. Channel values:\n", - "\u001b[0m{'docs': [], 'docs_to_finalize': [...], 'question': 'where did harrison work'}\n", - "\u001b[36;1m\u001b[1;3m[pregel/step]\u001b[0m \u001b[1mStarting step 3 with 1 task. Next tasks:\n", - "\u001b[0m- finalize({'docs': [Document(page_content='Harrison used to work at Kensho.'),\n", - " Document(page_content='Harrison used to work at Kensho.')]})\n", - "\u001b[36;1m\u001b[1;3m[pregel/checkpoint]\u001b[0m \u001b[1mFinishing step 3. Channel values:\n", - "\u001b[0m{'answer': 'Harrison used to work at Kensho.',\n", - " 'docs': [],\n", - " 'docs_to_finalize': [],\n", - " 'question': 'where did harrison work'}\n" - ] - }, - { - "data": { - "text/plain": [ - "'Harrison used to work at Kensho.'" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "reduce_chain.invoke({\"question\": \"where did harrison work\", \"docs\": many_docs})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "265b29cd-d4f4-4e48-8d4e-b759e909ac2e", - "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 -} diff --git a/examples/draft-revise-loop.py b/examples/draft-revise-loop.py deleted file mode 100644 index fae95a14e..000000000 --- a/examples/draft-revise-loop.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import annotations - -from langchain.chat_models.openai import ChatOpenAI -from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser -from langchain_core.output_parsers import StrOutputParser -from langchain_core.prompts import SystemMessagePromptTemplate - -from langgraph.pregel import Channel, Pregel - -# prompts - -drafter_prompt = ( - SystemMessagePromptTemplate.from_template( - "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." - ) - + "Question:\n\n{question}" -) - -reviser_prompt = ( - SystemMessagePromptTemplate.from_template( - "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." - ) - + "Draft:\n\n{draft}" - + "Editor's notes:\n\n{notes}" -) - -editor_prompt = ( - SystemMessagePromptTemplate.from_template( - "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." - ) - + "Draft:\n\n{draft}" -) - -editor_functions = [ - { - "name": "revise", - "description": "Sends the draft for revision", - "parameters": { - "type": "object", - "properties": { - "notes": { - "type": "string", - "description": "The editor's notes to guide the revision.", - }, - }, - }, - }, - { - "name": "accept", - "description": "Accepts the draft", - "parameters": { - "type": "object", - "properties": {"ready": {"const": True}}, - }, - }, -] - -# llms - -gpt3 = ChatOpenAI(model="gpt-3.5-turbo") -gpt4 = ChatOpenAI(model="gpt-4") - -# chains - -drafter_chain = drafter_prompt | gpt3 | StrOutputParser() - -editor_chain = ( - editor_prompt - | gpt4.bind(functions=editor_functions) - | JsonOutputFunctionsParser(args_only=False) -) - -reviser_chain = reviser_prompt | gpt3 | StrOutputParser() - -# application - -drafter = ( - # subscribe to question channel as a dict with a single key, "question" - Channel.subscribe_to(["question"]) | drafter_chain | Channel.write_to("draft") -) - -editor = ( - # subscribe to draft channel as a dict with a single key, "draft" - Channel.subscribe_to(["draft"]) - | editor_chain - | Channel.write_to( - # send to "notes" channel if the editor does not accept the draft - notes=lambda x: x["arguments"]["notes"] if x["name"] == "revise" else None - ) -) - -reviser = ( - # subscribe to new values of "notes" channel, - # and join them with the input value (question) and "draft" - Channel.subscribe_to(["notes"]).join(["question", "draft"]) - | reviser_chain - | Channel.write_to("draft") -) - -draft_revise_loop = Pregel( - chains={ - "drafter": drafter, - "editor": editor, - "reviser": reviser, - }, - # input will be a dict with a single key, "question" - input=["question"], - # output will be the value of "draft" - output="draft", - # debug logging - debug=True, -) - -# run - -print(draft_revise_loop.invoke({"question": "What food do turtles eat?"})) diff --git a/examples/langgraph.ipynb b/examples/langgraph.ipynb deleted file mode 100644 index cc558d276..000000000 --- a/examples/langgraph.ipynb +++ /dev/null @@ -1,1241 +0,0 @@ -{ - "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": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/harrisonchase/workplace/langchain/libs/core/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The class `langchain_community.chat_models.openai.ChatOpenAI` was deprecated in langchain-community 0.0.10 and will be removed in 0.2.0. An updated version of the class exists in the langchain-openai package and should be used instead. To use it run `pip install -U langchain-openai` and import as `from langchain_openai import ChatOpenAI`.\n", - " warn_deprecated(\n" - ] - } - ], - "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", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "from langgraph.prebuilt.agent_executor import create_agent_executor\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", - "tool_executor = ToolExecutor(tools)\n", - "chain = create_agent_executor(agent_runnable, tool_executor)" - ] - }, - { - "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.whereandwhen.net/when/north-america/california/san-francisco-ca/january/',\n", - " 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}])],\n", - " 'agent_outcome': AgentFinish(return_values={'output': 'I found information about the weather in San Francisco in January 2024. It looks like the weather statistics for January 2023 are available, showing the average temperatures and precipitation for each day. If you need current weather information, I can help you find a reliable source for that.'}, log='I found information about the weather in San Francisco in January 2024. It looks like the weather statistics for January 2023 are available, showing the average temperatures and precipitation for each day. If you need current weather information, I can help you find a reliable source for that.')}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chain.invoke({\"input\": \"what is the weather in sf\"})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dff32d55-f8b3-45fd-8de7-daa973aaa20b", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "id": "2d518968-2c61-4f4b-a2ae-8f8a545a0e7b", - "metadata": {}, - "source": [ - "## Agent Messages Executor" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4c468b87-3ff4-4d61-87bb-4fe0b61bca13", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.agents import AgentAction\n", - "from langchain_core.messages import FunctionMessage, HumanMessage, SystemMessage\n", - "from langchain.tools.render import format_tool_to_openai_function\n", - "import json\n", - "from langchain.chat_models import ChatOpenAI\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "from langgraph.prebuilt.agent_messages_executor import create_agent_messages_executor\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "659351eb-194c-4cc0-8f8b-ebbcc753ee38", - "metadata": {}, - "outputs": [], - "source": [ - "def call_model(messages):\n", - " response = model.invoke(messages)\n", - " return messages + [response]\n", - "\n", - "\n", - "def exit(messages):\n", - " last_message = messages[-1]\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " else:\n", - " return \"function\"\n", - "\n", - "tool_executor = ToolExecutor(tools)\n", - "def call_tool(messages):\n", - " last_message = messages[-1]\n", - " action = AgentAction(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", - " )\n", - " response = tool_executor.execute(action)\n", - " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", - " return messages + [function_message]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b78d8ba1-a428-4022-b5fc-cfb0744d2ac1", - "metadata": {}, - "outputs": [], - "source": [ - "chain = create_agent_messages_executor(call_model, call_tool, exit)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "e6f7d494-d922-4f9b-8499-36b179917522", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json')]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't find the current weather information for San Francisco. However, you can check the weather in San Francisco for January 2024 on this website: [San Francisco Weather in January](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).\")]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I apologize, but I couldn't find the current weather information for San Francisco. However, you can check the weather in San Francisco for January 2024 on this website: [San Francisco Weather in January](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).\")]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "markdown", - "id": "479f258a-6b61-42a5-85bb-c1c141f6d1fb", - "metadata": {}, - "source": [ - "### Human in the Loop\n", - "\n", - "#### Require confirmation" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "b7e94bc5-38c0-403c-8902-91eafd7e0c92", - "metadata": {}, - "outputs": [], - "source": [ - "def call_tool(messages):\n", - " last_message = messages[-1]\n", - " action = AgentAction(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", - " )\n", - " response = input(prompt=f\"[y/n] Okay to call this tool? {action}\")\n", - " if response == \"n\":\n", - " raise ValueError\n", - " response = tool_executor.execute(action)\n", - " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", - " return messages + [function_message]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e112af6b-6339-4a54-bf90-dcc0a3b3f7ed", - "metadata": {}, - "outputs": [], - "source": [ - "chain = create_agent_messages_executor(call_model, call_tool, exit)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "b3ad0780-8f1f-4ddd-9472-6f6400f0dcee", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", - "----\n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log='' \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json')]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't retrieve the current weather information for San Francisco. However, you can visit this [link](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/) to check the weather statistics for San Francisco in January.\")]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 12-01-2023 50°F to 59°F. 13-01-2023 54°F to 58°F. 14-01-2023 54°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't retrieve the current weather information for San Francisco. However, you can visit this [link](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/) to check the weather statistics for San Francisco in January.\")]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "1653c386-dd60-4283-ac03-21f00d57bddb", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "def call_tool(messages):\n", - " last_message = messages[-1]\n", - " action = AgentAction(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", - " )\n", - " response = input(prompt=f\"[y/n] Okay to call this tool? {action}\")\n", - " if response == \"**EXIT**\":\n", - " raise ValueError\n", - " elif response:\n", - " print(\"foo\")\n", - " action.tool_input = response\n", - " response = tool_executor.execute(action)\n", - " function_message = FunctionMessage(content=str(response[0][1]), name=action.tool)\n", - " return messages + [function_message]" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "b3d06fbc-a8d8-4a2a-b436-98975a69ef38", - "metadata": {}, - "outputs": [], - "source": [ - "chain = create_agent_messages_executor(call_model, call_tool, exit)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "1aeb7d89-2cb1-4e41-98b4-cb645d1245c6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}})]\n", - "----\n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log='' {'query': 'current weather in SF'}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "foo\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json')]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this website](https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/) to find information about the weather in San Francisco for the month of January.\")]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this website](https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/) to find information about the weather in San Francisco for the month of January.\")]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "markdown", - "id": "4b003c09-ceeb-44bf-94c5-502bde98e3c1", - "metadata": {}, - "source": [ - "## Respond in a specific format" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "fac71b90-d7d0-4cab-af3b-f3ed8264472f", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from typing import List" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "5d80baf8-977d-44b4-a092-9d449f16a577", - "metadata": {}, - "outputs": [], - "source": [ - "class Answer(BaseModel):\n", - " \"\"\"Final Response\"\"\"\n", - " temp: int = Field(description=\"current temperature, in Farenheit\")\n", - " source: List[str] = Field(description=\"URLs to go to to learn more info\")" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "ead5fb7e-ad02-4554-9595-2cbac99207a8", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function\n", - "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools] + [convert_pydantic_to_openai_function(Answer)])" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "8e708efa-501e-42e6-94bd-71f72b61ec48", - "metadata": {}, - "outputs": [], - "source": [ - "def call_model(messages):\n", - " response = model.invoke(messages)\n", - " return messages + [response]\n", - "\n", - "\n", - "def exit(messages):\n", - " last_message = messages[-1]\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " elif \"function_call\" in last_message.additional_kwargs and last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Answer\":\n", - " return \"end\"\n", - " else:\n", - " return \"function\"" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "40264371-76ad-4216-8554-8afb7632d741", - "metadata": {}, - "outputs": [], - "source": [ - "chain = create_agent_messages_executor(call_model, call_tool, exit)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "7f790767-3272-4b28-8264-ac621606be18", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}})]\n", - "----\n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "[y/n] Okay to call this tool? tool='tavily_search_results_json' tool_input={'query': 'current weather in San Francisco'} log='' \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json')]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'Answer', 'arguments': '{\\n \"temp\": 65,\\n \"source\": [\"https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/\"]\\n}'}})]\n", - "----\n", - "[HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\\n \"query\": \"current weather in San Francisco\"\\n}'}}), FunctionMessage(content=\"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January you can find all information about the weather in San Francisco in January: San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San FranciscoData: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'Answer', 'arguments': '{\\n \"temp\": 65,\\n \"source\": [\"https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/\"]\\n}'}})]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"what is the weather in sf\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "markdown", - "id": "ddc74d57-b82a-406d-9f19-ef7808ee9ceb", - "metadata": {}, - "source": [ - "## Tool State" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bacb638e-5bd5-425c-a019-b8345a24ef17", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "from langchain_core.pydantic_v1 import BaseModel, Field" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "e3e2a10a-b1c9-4320-8f01-a42b9c6b132d", - "metadata": {}, - "outputs": [], - "source": [ - "class IntSchema(BaseModel):\n", - " num: int\n", - " ls: dict\n", - "\n", - " @classmethod\n", - " def schema(cls):\n", - " schema = super().schema()\n", - " properties = schema.get('properties', {})\n", - " properties.pop('ls', None) # Remove the hidden attribute\n", - " return schema" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "3bd82332-4f47-42b8-b70b-b8b2dedac19c", - "metadata": {}, - "outputs": [], - "source": [ - "@tool(args_schema=IntSchema)\n", - "def add_int(num, ls):\n", - " \"\"\"Call this to add number to the list.\"\"\"\n", - " ls[\"foo\"].append(num)\n", - " print(ls)\n", - " return \"Done!\"" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "2a8d4bed-04c8-4bf2-ac84-5356cbd07d3a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'num': {'title': 'Num', 'type': 'integer'}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "IntSchema.schema()[\"properties\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "9471f29b-0c10-4d07-8d15-f3463c453fcb", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/harrisonchase/workplace/langchain/libs/core/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The class `langchain_community.chat_models.openai.ChatOpenAI` was deprecated in langchain-community 0.0.10 and will be removed in 0.2.0. An updated version of the class exists in the langchain-openai package and should be used instead. To use it run `pip install -U langchain-openai` and import as `from langchain_openai import ChatOpenAI`.\n", - " warn_deprecated(\n" - ] - } - ], - "source": [ - "from langchain_core.agents import AgentAction\n", - "from langchain_core.messages import FunctionMessage, HumanMessage, SystemMessage\n", - "from langchain.tools.render import format_tool_to_openai_function\n", - "import json\n", - "from langchain.chat_models import ChatOpenAI\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "from langgraph.prebuilt.executor import create_executor\n", - "tools = [add_int]\n", - "model = ChatOpenAI().bind_functions([format_tool_to_openai_function(t) for t in tools])" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "bf64ec0f-4563-446a-952b-c9ad3016d063", - "metadata": {}, - "outputs": [], - "source": [ - "ls = {\"foo\": []}\n", - "def call_model(messages):\n", - " response = model.invoke(messages)\n", - " return messages + [response]\n", - "\n", - "\n", - "def exit(messages):\n", - " last_message = messages[-1]\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " else:\n", - " return \"continue\"\n", - "\n", - "tool_executor = ToolExecutor(tools)\n", - "def call_tool(messages):\n", - " last_message = messages[-1]\n", - " agent_action = AgentAction(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", - " )\n", - " agent_action.tool_input[\"ls\"] = ls\n", - " tool_to_use = {t.name: t for t in tools}[agent_action.tool]\n", - " # Call that tool on the input\n", - " observation = tool_to_use.invoke(agent_action.tool_input)\n", - " function_message = FunctionMessage(content=str(observation), name=agent_action.tool)\n", - " return messages + [function_message]" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "83e7429b-1981-44ec-b567-50aa3a7b1731", - "metadata": {}, - "outputs": [], - "source": [ - "chain = create_executor(call_model, call_tool, exit)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "bda94e2c-9cf7-49b4-b687-466f1cbf81b2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'foo': []}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ls" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "9b63e7d1-a421-4a6b-ad47-93d8b497fd3e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}})]\n", - "----\n", - "{'foo': [1]}\n", - "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int')]\n", - "----\n", - "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number one has been added to the list.')]\n", - "----\n", - "[HumanMessage(content='add the number one to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 1\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number one has been added to the list.')]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"add the number one to the list\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "b2bbf236-c6b1-472c-a1a6-64d057096584", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'foo': [1]}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ls" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7679dc19-8ee4-4139-b2bd-2773ed378193", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}})]\n", - "----\n", - "{'foo': [1, 3]}\n", - "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int')]\n", - "----\n", - "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number 3 has been added to the list.')]\n", - "----\n", - "[HumanMessage(content='add the number 3 to the list'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'add_int', 'arguments': '{\\n \"num\": 3\\n}'}}), FunctionMessage(content='Done!', name='add_int'), AIMessage(content='The number 3 has been added to the list.')]\n", - "----\n" - ] - } - ], - "source": [ - "messages = [HumanMessage(content=\"add the number 3 to the list\")]\n", - "for s in chain.stream(messages):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "acb104ef-270d-4a03-b14c-c01b8e391935", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'foo': [1, 3]}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ls" - ] - }, - { - "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": "0c349b42-ae43-4564-90d2-80eff5b9c236", - "metadata": {}, - "outputs": [], - "source": [ - "## Plan and Execute\n", - "\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from typing import List, Tuple\n", - "\n", - "\n", - "class PlanExecute(BaseModel):\n", - "\n", - " plan: List[str] = []\n", - " past_steps: List[Tuple] = []\n", - " response: str = \"\"\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_openai import ChatOpenAI\n", - "from langchain.chains.openai_functions import create_structured_output_runnable\n", - "from langchain_core.tools import tool\n", - "\n", - "class Plan(BaseModel):\n", - " \"\"\"Plan to follow in future\"\"\"\n", - " steps: List[str] = Field(description=\"different steps to follow, should be in sorted order\")\n", - "\n", - "planner_prompt = ChatPromptTemplate.from_template(\"\"\"For the given objective, come up with a simple step by step plan. \\\n", - "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", - "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", - "\n", - "{objective}\"\"\")\n", - "planner = create_structured_output_runnable(Plan, ChatOpenAI(model=\"gpt-4-1106-preview\", temperature=0), planner_prompt)\n", - "\n", - "planner.invoke({'objective': 'what is leo dicaprios gf age raised to .23'})\n", - "\n", - "@tool\n", - "def search(query:str):\n", - " \"\"\"Get a response from google\"\"\"\n", - " return 25\n", - "\n", - "@tool\n", - "def math(equation: str):\n", - " \"\"\"Solve a math equation\"\"\"\n", - " return .34\n", - "\n", - "tools = [search, math]\n", - "\n", - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n", - "\n", - "from langchain_core.runnables import RunnablePassthrough\n", - "from langchain_core.agents import AgentFinish\n", - "\n", - "\n", - "# Define the agent\n", - "# Note that here, we are using `.assign` to add the output of the agent to the dictionary\n", - "# This dictionary will be returned from the node\n", - "# The reason we don't want to return just the result of `agent_runnable` from this node is\n", - "# that we want to continue passing around all the other inputs\n", - "agent = RunnablePassthrough.assign(\n", - " agent_outcome = agent_runnable\n", - ")\n", - "\n", - "# Define the function to execute tools\n", - "def action(data):\n", - " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", - " agent_action = data.pop('agent_outcome')\n", - " # Get the tool to use\n", - " tool_to_use = {t.name: t for t in tools}[agent_action.tool]\n", - " # Call that tool on the input\n", - " observation = tool_to_use.invoke(agent_action.tool_input)\n", - " # We now add in the action and the observation to the `intermediate_steps` list\n", - " # This is the list of all previous actions taken and their output\n", - " data['intermediate_steps'].append((agent_action, observation))\n", - " return data\n", - "\n", - "# Define logic that will be used to determine which conditional edge to go down\n", - "def should_continue(data):\n", - " # If the agent outcome is an AgentFinish, then we return `exit` string\n", - " # This will be used when setting up the graph to define the flow\n", - " if isinstance(data['agent_outcome'], AgentFinish):\n", - " return \"end\"\n", - " # Otherwise, an AgentAction is returned\n", - " # Here we return `continue` string\n", - " # This will be used when setting up the graph to define the flow\n", - " else:\n", - " return \"continue\"\n", - "\n", - "def plan(inputs):\n", - " inputs['state'] = PlanExecute(plan=planner.invoke(inputs).steps)\n", - " inputs['input'] = inputs['state'].plan[0]\n", - " inputs['intermediate_steps'] = []\n", - " return inputs\n", - "\n", - "from langchain.chains.openai_functions import create_openai_fn_runnable\n", - "class Response(BaseModel):\n", - " \"\"\"Response to user.\"\"\"\n", - " response: str\n", - "\n", - "replanner_prompt = ChatPromptTemplate.from_template(\"\"\"For the given objective, come up with a simple step by step plan. \\\n", - "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", - "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", - "\n", - "Your objective was this:\n", - "{objective}\n", - "\n", - "Your original plan was this:\n", - "{plan}\n", - "\n", - "You have currently done the follow steps:\n", - "{steps}\n", - "\n", - "Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan.\"\"\")\n", - "\n", - "\n", - "replanner = create_openai_fn_runnable([Plan, Response], ChatOpenAI(model=\"gpt-4-1106-preview\", temperature=0), replanner_prompt)\n", - "\n", - "replanner.invoke({\"objective\": \"look up the temperature\", \"plan\": \"look up the temperature\", \"steps\": [(\"look up the temperature\", \"i am in san diego\")]})\n", - "\n", - "def replan(inputs):\n", - " inputs['state'].past_steps.append((inputs['state'].plan[0], inputs['agent_outcome'].return_values['output']))\n", - " sub_inputs = {\n", - " \"objective\": inputs[\"objective\"],\n", - " \"plan\": inputs[\"state\"].plan,\n", - " \"steps\": inputs[\"state\"].past_steps\n", - " }\n", - " output = replanner.invoke(sub_inputs)\n", - " if isinstance(output, Response):\n", - " inputs['state'].response = output.response\n", - " else:\n", - " inputs['state'].plan = output.steps\n", - " inputs['input'] = inputs['state'].plan[0]\n", - " return inputs\n", - "\n", - "\n", - "def should_end(inputs):\n", - " if inputs['state'].response:\n", - " return True\n", - " else:\n", - " return False\n", - "\n", - "from langgraph.graph import END, Graph\n", - "\n", - "workflow = Graph()\n", - "\n", - "# Add the plan node\n", - "workflow.add_node(\"plan\", plan)\n", - "\n", - "# Add the agent node, we give it name `agent` which we will use later\n", - "workflow.add_node(\"agent\", agent)\n", - "# Add the action node, we give it name `action` which we will use later\n", - "workflow.add_node(\"action\", action)\n", - "\n", - "# Add a replan node\n", - "workflow.add_node(\"replan\", replan)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"plan\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we go back to replan\n", - " \"end\": \"replan\"\n", - " }\n", - ")\n", - "\n", - "# From plan we go to agent\n", - "workflow.add_edge('plan', 'agent')\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"replan\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_end,\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " True: END,\n", - " False: \"agent\",\n", - " }\n", - ")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "chain = workflow.compile()\n", - "\n", - "for s in chain.stream({\"objective\": \"what is leo dicaprios gf age raised to .34\"}):\n", - " print(s)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb6fb6a8-f6f5-444a-9033-ef9e0bd6fd23", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/rag.py b/examples/rag.py deleted file mode 100644 index 414d4ab10..000000000 --- a/examples/rag.py +++ /dev/null @@ -1,58 +0,0 @@ -from langchain.chat_models import ChatOpenAI -from langchain.embeddings import OpenAIEmbeddings -from langchain.vectorstores import FAISS -from langchain_core.messages import AIMessage, AnyMessage, FunctionMessage -from langchain_core.prompts import PromptTemplate - -from langgraph.channels import Topic -from langgraph.pregel import Channel, Pregel - -texts = ["harrison went to kensho"] -embeddings = OpenAIEmbeddings() -db = FAISS.from_texts(texts, embeddings) - -retriever = db.as_retriever() - - -prompt = PromptTemplate.from_template( - """Answer the question "{question}" based on the following context: {context}""" -) - -model = ChatOpenAI() - -chain = ( - Channel.subscribe_to(["question"]) - | { - "context": (lambda x: x["question"]) - | Channel.write_to( - messages=lambda _input: AIMessage( - content="", - additional_kwargs={ - "function_call": "retrieval", - "arguments": {"question": _input}, - }, - ) - ) - | retriever - | Channel.write_to( - messages=lambda documents: FunctionMessage.construct( - content=documents, # function message requires content to be str - name="retrieval", - ) - ), - "question": lambda x: x["question"], - } - | prompt - | model - | Channel.write_to(messages=lambda message: [message]) -) - -app = Pregel( - chains={"chain": chain}, - channels={"messages": Topic(AnyMessage)}, - input=["question"], - output=["messages"], -) - -for s in app.stream({"question": "where did harrison go"}): - print(s) diff --git a/examples/readme.py b/examples/readme.py deleted file mode 100644 index b7077a321..000000000 --- a/examples/readme.py +++ /dev/null @@ -1,132 +0,0 @@ -import asyncio -from pprint import pprint - -from langchain import hub -from langchain.agents import create_openai_functions_agent -from langchain_community.tools.tavily_search import TavilySearchResults -from langchain_core.agents import AgentFinish -from langchain_core.runnables import RunnablePassthrough -from langchain_openai.chat_models import ChatOpenAI - -from langgraph.graph import END, Graph - -tools = [TavilySearchResults(max_results=1)] - -# Get the prompt to use - you can modify this! -prompt = hub.pull("hwchase17/openai-functions-agent") - -# Choose the LLM that will drive the agent -llm = ChatOpenAI(model="gpt-3.5-turbo-1106") - -# Construct the OpenAI Functions agent -agent_runnable = create_openai_functions_agent(llm, tools, prompt) - - -# Define the agent -# Note that here, we are using `.assign` to add the output of the agent to the dictionary -# This dictionary will be returned from the node -# The reason we don't want to return just the result of `agent_runnable` from this node is -# that we want to continue passing around all the other inputs -agent = RunnablePassthrough.assign(agent_outcome=agent_runnable) - - -# Define the function to execute tools -def execute_tools(data): - # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data.pop("agent_outcome") - # Get the tool to use - tool_to_use = {t.name: t for t in tools}[agent_action.tool] - # Call that tool on the input - observation = tool_to_use.invoke(agent_action.tool_input) - # We now add in the action and the observation to the `intermediate_steps` list - # This is the list of all previous actions taken and their output - data["intermediate_steps"].append((agent_action, observation)) - return data - - -# Define logic that will be used to determine which conditional edge to go down -def should_continue(data): - # If the agent outcome is an AgentFinish, then we return `exit` string - # This will be used when setting up the graph to define the flow - if isinstance(data["agent_outcome"], AgentFinish): - return "exit" - # Otherwise, an AgentAction is returned - # Here we return `continue` string - # This will be used when setting up the graph to define the flow - else: - return "continue" - - -# Define the graph - - -workflow = Graph() - -# Add the agent node, we give it name `agent` which we will use later -workflow.add_node("agent", agent) -# Add the tools node, we give it name `tools` which we will use later -workflow.add_node("tools", execute_tools) - -# Set the entrypoint as `agent` -# This means that this node is the first one called -workflow.set_entry_point("agent") - -# We now add a conditional edge -workflow.add_conditional_edges( - # First, we define the start node. We use `agent`. - # This means these are the edges taken after the `agent` node is called. - "agent", - # Next, we pass in the function that will determine which node is called next. - should_continue, - # Finally we pass in a mapping. - # The keys are strings, and the values are other nodes. - # END is a special node marking that the graph should finish. - # What will happen is we will call `should_continue`, and then the output of that - # will be matched against the keys in this mapping. - # Based on which one it matches, that node will then be called. - { - # If `tools`, then we call the tool node. - "continue": "tools", - # Otherwise we finish. - "exit": END, - }, -) - -# We now add a normal edge from `tools` to `agent`. -# This means that after `tools` is called, `agent` node is called next. -workflow.add_edge("tools", "agent") - -# Finally, we compile it! -# This compiles it into a LangChain Runnable, -# meaning you can use it as you would any other runnable -chain = workflow.compile() - - -def main(): - for output in chain.stream( - {"input": "what is the weather in sf", "intermediate_steps": []} - ): - for key, value in output.items(): - print(f"Output from node '{key}':") - print("---") - pprint(value) - print("\n---\n") - - -async def amain(): - async for output in chain.astream_log( - {"input": "what is the weather in sf", "intermediate_steps": []}, - include_types=["llm"], - ): - for op in output.ops: - if op["path"] == "/streamed_output/-": - # this is the output from .stream() - ... - elif op["path"].startswith("/logs/") and op["path"].endswith( - "/streamed_output/-" - ): - # these are tokens from the LLM - print(op["value"]) - - -asyncio.run(amain()) diff --git a/examples/recursive-web-loader.py b/examples/recursive-web-loader.py deleted file mode 100644 index 1d6f186ba..000000000 --- a/examples/recursive-web-loader.py +++ /dev/null @@ -1,134 +0,0 @@ -from contextlib import asynccontextmanager, contextmanager -from typing import AsyncGenerator, Callable, FrozenSet, Generator, Optional, TypedDict - -import httpx -from langchain_core.documents import Document -from langchain_core.runnables import RunnableLambda, RunnablePassthrough -from langchain_core.utils.html import extract_sub_links - -from langgraph.channels.context import Context -from langgraph.channels.topic import Topic -from langgraph.pregel import Channel, Pregel - -# Load url with sync httpx client - - -@contextmanager -def httpx_client() -> Generator[httpx.Client, None, None]: - with httpx.HTTPTransport(retries=3) as transport, httpx.Client( - transport=transport - ) as client: - yield client - - -class LoadUrlInput(TypedDict): - url: str - visited: FrozenSet[str] - client: httpx.Client - - -def load_url(input: LoadUrlInput) -> str: - response = input["client"].get(input["url"]) - return response.text - - -# Same as above but with async httpx client - - -@asynccontextmanager -async def httpx_aclient() -> AsyncGenerator[httpx.AsyncClient, None]: - async with httpx.AsyncHTTPTransport(retries=3) as transport, httpx.AsyncClient( - transport=transport - ) as client: - yield client - - -class LoadUrlInputAsync(TypedDict): - url: str - visited: FrozenSet[str] - client: httpx.AsyncClient - - -async def load_url_async(input: LoadUrlInputAsync) -> str: - response = await input["client"].get(input["url"]) - return response.text - - -# default metadata extractor copied from langchain.document_loaders - - -def _metadata_extractor(raw_html: str, url: str) -> dict: - """Extract metadata from raw html using BeautifulSoup.""" - metadata = {"source": url} - - try: - from bs4 import BeautifulSoup - except ImportError: - return metadata - soup = BeautifulSoup(raw_html, "html.parser") - if title := soup.find("title"): - metadata["title"] = title.get_text() - if description := soup.find("meta", attrs={"name": "description"}): - metadata["description"] = description.get("content", None) - if html := soup.find("html"): - metadata["language"] = html.get("lang", None) - return metadata - - -def recursive_web_loader( - *, - max_depth: int = 2, - extractor: Optional[Callable[[str], str]] = None, - metadata_extractor: Optional[Callable[[str, str], dict]] = None, -) -> Pregel: - # assign default extractors - extractor = extractor or (lambda x: x) - metadata_extractor = metadata_extractor or _metadata_extractor - # define the channels - channels = { - "next_urls": Topic(str, unique=True), - "documents": Topic(Document, accumulate=True), - "client": Context(httpx_client, httpx_aclient), - } - # the main chain that gets executed recursively - # while there are urls in next_urls - visitor = ( - # run the chain below for each url in next_urls - # adding the current values of base_url and httpx client - Channel.subscribe_to_each("next_urls", key="url").join(["client", "base_url"]) - # load the url (with sync and async implementations) - | RunnablePassthrough.assign(body=RunnableLambda(load_url, load_url_async)) - | Channel.write_to( - # send a new document to the documents stream - documents=lambda x: Document( - page_content=extractor(x["body"]), - metadata=metadata_extractor(x["body"], x["url"]), - ), - # send the next urls to the next_urls topic - next_urls=lambda x: extract_sub_links( - x["body"], x["url"], base_url=x["base_url"] - ), - ) - ) - return Pregel( - channels=channels, - chains={ - # use the base_url as the first url to visit - "input": Channel.subscribe_to("base_url") | Channel.write_to("next_urls"), - # add the main chain - "visitor": visitor, - }, - # this will accept a string as input - input="base_url", - # and return a dict with documents and visited set - output=["documents", "visited"], - # debug logging - debug=True, - ).with_config({"recursion_limit": max_depth + 1}) - - -loader = recursive_web_loader(max_depth=3) - -documents = loader.invoke("https://docs.python.org/3.9/") - -print(len(documents["documents"])) diff --git a/langgraph/prebuilt/__init__.py b/langgraph/prebuilt/__init__.py index e69de29bb..89dfa34a3 100644 --- a/langgraph/prebuilt/__init__.py +++ b/langgraph/prebuilt/__init__.py @@ -0,0 +1,5 @@ +from langgraph.prebuilt.agent_executor import create_agent_executor +from langgraph.prebuilt import chat_executor +from langgraph.prebuilt.tool_executor import ToolExecutor + +__all__ = ["create_agent_executor", "chat_executor", "ToolExecutor"] diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py index 77b365e35..45fcb9ade 100644 --- a/langgraph/prebuilt/agent_executor.py +++ b/langgraph/prebuilt/agent_executor.py @@ -1,10 +1,39 @@ -from typing import Annotated, TypedDict import operator +from typing import Annotated, TypedDict, Union, Sequence + from langchain_core.agents import AgentAction, AgentFinish -from langgraph.graph import StateGraph, END +from langchain_core.messages import BaseMessage + +from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor +def _get_agent_state(input_schema= None): + if input_schema is None: + class AgentState(TypedDict): + # The input string + input: str + # The list of previous messages in the conversation + chat_history: Sequence[BaseMessage] + # The outcome of a given call to the agent + # Needs `None` as a valid type, since this is what this will start as + agent_outcome: Union[AgentAction, AgentFinish, None] + # List of actions and corresponding observations + # Here we annotate this with `operator.add` to indicate that operations to + # this state should be ADDED to the existing values (not overwrite it) + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + else: + class AgentState(input_schema): + # The outcome of a given call to the agent + # Needs `None` as a valid type, since this is what this will start as + agent_outcome: Union[AgentAction, AgentFinish, None] + # List of actions and corresponding observations + # Here we annotate this with `operator.add` to indicate that operations to + # this state should be ADDED to the existing values (not overwrite it) + intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] + + return AgentState def create_agent_executor(agent_runnable, tools, input_schema=None): @@ -14,18 +43,9 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): else: tool_executor = ToolExecutor(tools) + state = _get_agent_state(input_schema) - if input_schema is None: - class AgentState(TypedDict): - input: str - agent_outcome: AgentAction | AgentFinish | None - intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] - - else: - class AgentState(input_schema): - agent_outcome: AgentAction | AgentFinish | None - intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] - + # Define logic that will be used to determine which conditional edge to go down def should_continue(data): # If the agent outcome is an AgentFinish, then we return `exit` string # This will be used when setting up the graph to define the flow @@ -49,7 +69,7 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): return {"intermediate_steps": [(agent_action, str(output))]} # Define a new graph - workflow = StateGraph(AgentState) + workflow = StateGraph(state) # Define the two nodes we will cycle between workflow.add_node("agent", run_agent) diff --git a/langgraph/prebuilt/messages_executor.py b/langgraph/prebuilt/chat_executor.py similarity index 75% rename from langgraph/prebuilt/messages_executor.py rename to langgraph/prebuilt/chat_executor.py index 760f20ab1..eebb6998c 100644 --- a/langgraph/prebuilt/messages_executor.py +++ b/langgraph/prebuilt/chat_executor.py @@ -1,35 +1,23 @@ -from langchain_core.runnables import RunnablePassthrough -from langchain_core.messages import FunctionMessage -from langchain_core.agents import AgentFinish, AgentAction import json +import operator +from typing import Annotated, Sequence, TypedDict from langchain.tools.render import format_tool_to_openai_function +from langchain_core.agents import AgentAction +from langchain_core.messages import BaseMessage, FunctionMessage + +from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor -from langchain_core.utils.function_calling import convert_pydantic_to_openai_function -from typing import Annotated, TypedDict, Sequence -from langchain_core.messages import BaseMessage -import operator -from langchain_core.agents import AgentAction, AgentFinish -from langgraph.graph import StateGraph, END -def _get_tool_executor_and_functions(tools, response_format): +def create_function_calling_executor(model, tools): if isinstance(tools, ToolExecutor): tool_executor = tools tool_classes = tools.tools else: tool_executor = ToolExecutor(tools) tool_classes = tools - - functions = [format_tool_to_openai_function(t) for t in tool_classes] - if response_format is not None: - functions.append(convert_pydantic_to_openai_function(response_format)) - return tool_executor, functions - - -def create_messages_executor(model, tools, response_format = None): - tool_executor, functions = _get_tool_executor_and_functions(tools, response_format) - model = model.bind_functions([format_tool_to_openai_function(t) for t in tools]) + model = model.bind_functions([format_tool_to_openai_function(t) for t in tool_classes]) # Define the function that determines whether to continue or not def should_continue(state): @@ -38,14 +26,9 @@ def create_messages_executor(model, tools, response_format = None): # If there is no function call, then we finish if "function_call" not in last_message.additional_kwargs: return "end" - # Otherwise if there is, we need to check what type of function call it is + # Otherwise if there is, we continue else: - if response_format is None: - return "continue" - elif last_message.additional_kwargs["function_call"]["name"] == response_format.__name__: - return "end" - else: - return "continue" + return "continue" # Define the function that calls the model def call_model(state): diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py index 7f860f256..121ef0855 100644 --- a/langgraph/prebuilt/tool_executor.py +++ b/langgraph/prebuilt/tool_executor.py @@ -1,7 +1,9 @@ -from langchain_core.runnables import RunnableBinding, RunnableLambda -from typing import Sequence, Any -from langchain_core.tools import BaseTool +from typing import Any, Sequence + from langchain_core.agents import AgentAction +from langchain_core.runnables import RunnableBinding, RunnableLambda +from langchain_core.tools import BaseTool + INVALID_TOOL_MSG_TEMPLATE = ( "{requested_tool_name} is not a valid tool, " "try one of [{available_tool_names_str}]." From 23f84c9ac9520507c7eed1994fbcc4b2b24efb44 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 13:18:32 -0800 Subject: [PATCH 10/25] Fix tests to run in py 3.9 --- Dockerfile | 4 ++-- tests/test_pregel.py | 8 ++++---- tests/test_pregel_async.py | 17 +++++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2095ad194..d29a3288c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.9 # Set the working directory to /app WORKDIR /app @@ -7,6 +7,6 @@ WORKDIR /app COPY . . # Install any needed packages specified in requirements.txt -RUN pip install poetry && poetry config virtualenvs.create false && poetry install --with test,lint,typing +RUN pip install poetry && poetry config virtualenvs.create false && poetry install --with test,lint,typing,dev RUN poetry run pytest diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 7553e4bc6..65494bd16 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -2,7 +2,7 @@ import operator import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager -from typing import Annotated, Generator, TypedDict +from typing import Annotated, Generator, Optional, TypedDict, Union import pytest from langchain_core.runnables import RunnablePassthrough @@ -584,7 +584,7 @@ def test_conditional_graph() -> None: ] ) - def agent_parser(input: str) -> AgentFinish | AgentAction: + def agent_parser(input: str) -> Union[AgentAction, AgentFinish]: if input.startswith("finish"): _, answer = input.split(":") return AgentFinish(return_values={"answer": answer}, log=input) @@ -786,7 +786,7 @@ def test_conditional_graph_state() -> None: class AgentState(TypedDict): input: str - agent_outcome: AgentAction | AgentFinish | None + agent_outcome: Optional[Union[AgentAction, AgentFinish]] intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] # Assemble the tools @@ -808,7 +808,7 @@ def test_conditional_graph_state() -> None: ] ) - def agent_parser(input: str) -> AgentFinish | AgentAction: + def agent_parser(input: str) -> Union[AgentAction, AgentFinish]: if input.startswith("finish"): _, answer = input.split(":") return { diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index cbd5406ad..e776c512b 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -1,7 +1,16 @@ import asyncio import operator from contextlib import asynccontextmanager, contextmanager -from typing import Annotated, Any, AsyncGenerator, AsyncIterator, Generator, TypedDict +from typing import ( + Annotated, + Any, + AsyncGenerator, + AsyncIterator, + Generator, + Optional, + TypedDict, + Union, +) import pytest from langchain_core.runnables import RunnablePassthrough @@ -621,7 +630,7 @@ async def test_conditional_graph() -> None: ] ) - async def agent_parser(input: str) -> AgentFinish | AgentAction: + async def agent_parser(input: str) -> Union[AgentAction, AgentFinish]: if input.startswith("finish"): _, answer = input.split(":") return AgentFinish(return_values={"answer": answer}, log=input) @@ -831,7 +840,7 @@ async def test_conditional_graph_state() -> None: class AgentState(TypedDict): input: str - agent_outcome: AgentAction | AgentFinish | None + agent_outcome: Optional[Union[AgentAction, AgentFinish]] intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] # Assemble the tools @@ -853,7 +862,7 @@ async def test_conditional_graph_state() -> None: ] ) - def agent_parser(input: str) -> AgentFinish | AgentAction: + def agent_parser(input: str) -> Union[AgentAction, AgentFinish]: if input.startswith("finish"): _, answer = input.split(":") return { From 2e537320c35b8b5e4d3c6cf2ea28b4bbdd85e790 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 13:45:54 -0800 Subject: [PATCH 11/25] Rename saver to checkpointer, expose in graph, state graph, prebuilt agent exec --- langgraph/graph/graph.py | 6 +++-- langgraph/graph/state.py | 4 +++- langgraph/prebuilt/agent_executor.py | 32 +++++++++++++++---------- langgraph/pregel/__init__.py | 36 ++++++++++++++++++---------- tests/test_pregel.py | 2 +- tests/test_pregel_async.py | 2 +- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index b5e317334..4ebd8144e 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -1,6 +1,6 @@ from asyncio import iscoroutinefunction from collections import defaultdict -from typing import Any, Callable, Dict, NamedTuple +from typing import Any, Callable, Dict, NamedTuple, Optional from langchain_core.runnables import Runnable from langchain_core.runnables.base import ( @@ -9,6 +9,7 @@ from langchain_core.runnables.base import ( coerce_to_runnable, ) +from langgraph.checkpoint import BaseCheckpointSaver from langgraph.pregel import Channel, Pregel END = "__end__" @@ -97,7 +98,7 @@ class Graph: if node not in all_starts: raise ValueError(f"Node `{node}` is a dead-end") - def compile(self) -> Pregel: + def compile(self, checkpointer: Optional[BaseCheckpointSaver] = None) -> Pregel: self.validate() outgoing_edges = defaultdict(list) @@ -127,4 +128,5 @@ class Graph: input=f"{self.entry_point}:inbox", output=END, hidden=[f"{node}:inbox" for node in self.nodes], + checkpointer=checkpointer, ) diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index 5b5c658ca..b0cb163d0 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -8,6 +8,7 @@ from langchain_core.runnables import RunnableConfig, RunnableLambda from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.last_value import LastValue +from langgraph.checkpoint import BaseCheckpointSaver from langgraph.graph.graph import END, Graph from langgraph.pregel import Channel, Pregel from langgraph.pregel.read import ChannelRead @@ -24,7 +25,7 @@ class StateGraph(Graph): if any(isinstance(c, BinaryOperatorAggregate) for c in self.channels.values()): self.support_multiple_edges = True - def compile(self) -> Pregel: + def compile(self, checkpointer: Optional[BaseCheckpointSaver] = None) -> Pregel: self.validate() if any(key in self.nodes for key in self.channels): @@ -79,6 +80,7 @@ class StateGraph(Graph): input=f"{START}:inbox", output=END, hidden=[f"{node}:inbox" for node in self.nodes] + [START] + state_keys, + checkpointer=checkpointer, ) diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py index 77b365e35..358520577 100644 --- a/langgraph/prebuilt/agent_executor.py +++ b/langgraph/prebuilt/agent_executor.py @@ -1,27 +1,33 @@ -from typing import Annotated, TypedDict import operator +from typing import Annotated, Optional, TypedDict + from langchain_core.agents import AgentAction, AgentFinish -from langgraph.graph import StateGraph, END + +from langgraph.checkpoint import BaseCheckpointSaver +from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor - - -def create_agent_executor(agent_runnable, tools, input_schema=None): - +def create_agent_executor( + agent_runnable, + tools, + input_schema=None, + checkpointer: Optional[BaseCheckpointSaver] = None, +): if isinstance(tools, ToolExecutor): tool_executor = tools else: tool_executor = ToolExecutor(tools) - if input_schema is None: + class AgentState(TypedDict): input: str agent_outcome: AgentAction | AgentFinish | None intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] else: + class AgentState(input_schema): agent_outcome: AgentAction | AgentFinish | None intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] @@ -29,7 +35,7 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): def should_continue(data): # If the agent outcome is an AgentFinish, then we return `exit` string # This will be used when setting up the graph to define the flow - if isinstance(data['agent_outcome'], AgentFinish): + if isinstance(data["agent_outcome"], AgentFinish): return "end" # Otherwise, an AgentAction is returned # Here we return `continue` string @@ -44,7 +50,7 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): # Define the function to execute tools def execute_tools(data): # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data['agent_outcome'] + agent_action = data["agent_outcome"] output = tool_executor.invoke(agent_action) return {"intermediate_steps": [(agent_action, str(output))]} @@ -76,15 +82,15 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): # If `tools`, then we call the tool node. "continue": "action", # Otherwise we finish. - "end": END - } + "end": END, + }, ) # We now add a normal edge from `tools` to `agent`. # This means that after `tools` is called, `agent` node is called next. - workflow.add_edge('action', 'agent') + workflow.add_edge("action", "agent") # Finally, we compile it! # This compiles it into a LangChain Runnable, # meaning you can use it as you would any other runnable - return workflow.compile() + return workflow.compile(checkpointer=checkpointer) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index b61381548..49187d1f4 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -169,7 +169,7 @@ class Pregel( debug: bool = Field(default_factory=get_debug) - saver: Optional[BaseCheckpointSaver] = None + checkpointer: Optional[BaseCheckpointSaver] = None name: str = "LangGraph" @@ -187,7 +187,7 @@ class Pregel( def config_specs(self) -> list[ConfigurableFieldSpec]: return get_unique_config_specs( [spec for node in self.nodes.values() for spec in node.config_specs] - + (self.saver.config_specs if self.saver is not None else []) + + (self.checkpointer.config_specs if self.checkpointer is not None else []) ) @property @@ -244,7 +244,7 @@ class Pregel( # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one - checkpoint = self.saver.get(config) if self.saver else None + checkpoint = self.checkpointer.get(config) if self.checkpointer else None checkpoint = checkpoint or empty_checkpoint() # create channels from checkpoint with ChannelsManager( @@ -327,18 +327,24 @@ class Pregel( _apply_writes_from_view(checkpoint, channels, step_output) # save end of step checkpoint - if self.saver is not None and self.saver.at == CheckpointAt.END_OF_STEP: + if ( + self.checkpointer is not None + and self.checkpointer.at == CheckpointAt.END_OF_STEP + ): checkpoint = create_checkpoint(checkpoint, channels) - self.saver.put(config, checkpoint) + self.checkpointer.put(config, checkpoint) # interrupt if any channel written to is in interrupt list if any(chan for chan, _ in pending_writes if chan in self.interrupt): break # save end of run checkpoint - if self.saver is not None and self.saver.at == CheckpointAt.END_OF_RUN: + if ( + self.checkpointer is not None + and self.checkpointer.at == CheckpointAt.END_OF_RUN + ): checkpoint = create_checkpoint(checkpoint, channels) - self.saver.put(config, checkpoint) + self.checkpointer.put(config, checkpoint) async def _atransform( self, @@ -368,7 +374,7 @@ class Pregel( # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one - checkpoint = await self.saver.aget(config) if self.saver else None + checkpoint = await self.checkpointer.aget(config) if self.checkpointer else None checkpoint = checkpoint or empty_checkpoint() # create channels from checkpoint async with AsyncChannelsManager(self.channels, checkpoint) as channels: @@ -454,18 +460,24 @@ class Pregel( _apply_writes_from_view(checkpoint, channels, step_output) # save end of step checkpoint - if self.saver is not None and self.saver.at == CheckpointAt.END_OF_STEP: + if ( + self.checkpointer is not None + and self.checkpointer.at == CheckpointAt.END_OF_STEP + ): checkpoint = create_checkpoint(checkpoint, channels) - await self.saver.aput(config, checkpoint) + await self.checkpointer.aput(config, checkpoint) # interrupt if any channel written to is in interrupt list if any(chan for chan, _ in pending_writes if chan in self.interrupt): break # save end of run checkpoint - if self.saver is not None and self.saver.at == CheckpointAt.END_OF_RUN: + if ( + self.checkpointer is not None + and self.checkpointer.at == CheckpointAt.END_OF_RUN + ): checkpoint = create_checkpoint(checkpoint, channels) - await self.saver.aput(config, checkpoint) + await self.checkpointer.aput(config, checkpoint) def invoke( self, diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 65494bd16..961cd370c 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -382,7 +382,7 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one}, channels={"total": BinaryOperatorAggregate(int, operator.add)}, - saver=memory, + checkpointer=memory, ) # total starts out as 0, so output is 0+2=2 diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index e776c512b..012992ecb 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -405,7 +405,7 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one}, channels={"total": BinaryOperatorAggregate(int, operator.add)}, - saver=memory, + checkpointer=memory, ) # total starts out as 0, so output is 0+2=2 From 95b333f474af0b1edb829c0afb37bc8669408217 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 15:30:35 -0800 Subject: [PATCH 12/25] stash --- README.md | 377 +++++++------- .../base.ipynb | 473 ++++++++++++++++-- .../dynamically-returning-directly.ipynb | 471 +++++++++++++---- .../force-calling-a-tool-first.ipynb | 356 ++++++++++--- .../human-in-the-loop.ipynb | 325 ++++++++++-- .../managing-agent-steps.ipynb | 340 ++++++++++--- .../respond-in-format.ipynb | 333 +++++++++--- langgraph/prebuilt/__init__.py | 4 +- langgraph/prebuilt/tool_executor.py | 22 +- 9 files changed, 2115 insertions(+), 586 deletions(-) diff --git a/README.md b/README.md index d3863b8d4..21fe41dfa 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ pip install langgraph ## Quick Start -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. +Here we will go over an example of creating a simple agent that uses chat models and function calling. +This agent will represent all state as a list of messages. -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. +We will need 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 +pip install -U langchain langchain_openai tavily-python ``` We also need to export some environment variables needed for our agent. @@ -47,31 +47,57 @@ export LANGCHAIN_API_KEY=ls__... export LANGCHAIN_ENDPOINT=https://api.langchain.plus ``` -### Define the LangChain Agent +### Set up the tools -This is the LangChain agent. -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). +We will first define the tools we want to use. +For this simple example, we will use a built-in search tool via Tavily. +However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that. ```python -from langchain import hub -from langchain.agents import create_openai_functions_agent -from langchain_openai.chat_models import ChatOpenAI from langchain_community.tools.tavily_search import TavilySearchResults tools = [TavilySearchResults(max_results=1)] - -# Get the prompt to use - you can modify this! -prompt = hub.pull("hwchase17/openai-functions-agent") - -# Choose the LLM that will drive the agent -# We set streaming=True so that we can stream tokens (we will cover this more detail later on) -llm = ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True) - -# Construct the OpenAI Functions agent -agent_runnable = create_openai_functions_agent(llm, tools, prompt) ``` +We can now wrap these tools in a simple ToolExecutor. +This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output. +A ToolInvocation is any class with `tool` and `tool_input` attribute. + +```python +from langgraph.prebuilt import ToolExecutor + +tool_executor = ToolExecutor(tools) +``` + +### Set up the model + +Now we need to load the chat model we want to use. +Importantly, this should satisfy two criteria: + +1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them. +2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface. + +Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example. + +```python +from langchain_openai import ChatOpenAI + +# We will set streaming=True so that we can stream tokens +# See the streaming section for more information on this. +model = ChatOpenAI(temperature=0, streaming=True) +``` + +After we've done this, we should make sure the model knows that it has these tools available to call. +We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class. + +```python +from langchain.tools.render import format_tool_to_openai_function + +functions = [format_tool_to_openai_function(t) for t in tools] +model = model.bind_functions(functions) +``` + + ### Define the agent state The main type of graph in `langgraph` is the `StatefulGraph`. @@ -80,35 +106,18 @@ Each node then returns operations to update that state. These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute. Whether to set or add is denoted by annotating the state object you construct the graph with. -The state for the traditional LangChain agent has a few attributes: - -1. `input`: This is the input string representing the main ask from the user, passed in as input. -2. `chat_history`: This is any previous conversation messages, also passed in as input. -3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent. -4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools. - -Let's make these ideas concrete by create an agent state! +For this example, the state we will track will just be a list of messages. +We want each node to just add messages to that list. +Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to. ```python -from typing import TypedDict, Annotated, Sequence, Union -from langchain_core.agents import AgentAction, AgentFinish -from langchain_core.messages import BaseMessage +from typing import TypedDict, Annotated, Sequence import operator +from langchain_core.messages import BaseMessage class AgentState(TypedDict): - # The input string - input: str - # The list of previous messages in the conversation - chat_history: Sequence[BaseMessage] - # The outcome of a given call to the agent - # Needs `None` as a valid type, since this is what this will start as - agent_outcome: Union[AgentAction, AgentFinish, None] - # List of actions and corresponding observations - # Here we annotate this with `operator.add` to indicate that operations to - # this state should be ADDED to the existing values (not overwrite it) - intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] - + messages: Annotated[Sequence[BaseMessage], operator.add] ``` ### Define the nodes @@ -133,36 +142,45 @@ The path that is taken is not known until that node is run (the LLM decides). Let's define the nodes, as well as a function to decide how what conditional edge to take. ```python -from langchain_core.agents import AgentFinish -from langgraph.prebuilt.tool_executor import ToolExecutor +from langgraph.prebuilt import ToolInvocation +import json +from langchain_core.messages import FunctionMessage -# This a helper class we have that is useful for running tools -# It takes in an agent action and calls that tool and returns the result -tool_executor = ToolExecutor(tools) - -# Define the agent -def run_agent(data): - agent_outcome = agent_runnable.invoke(data) - return {"agent_outcome": agent_outcome} - -# Define the function to execute tools -def execute_tools(data): - # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data['agent_outcome'] - output = tool_executor.invoke(agent_action) - return {"intermediate_steps": [(agent_action, str(output))]} - -# Define logic that will be used to determine which conditional edge to go down -def should_continue(data): - # If the agent outcome is an AgentFinish, then we return `exit` string - # This will be used when setting up the graph to define the flow - if isinstance(data['agent_outcome'], AgentFinish): +# Define the function that determines whether to continue or not +def should_continue(state): + messages = state['messages'] + last_message = messages[-1] + # If there is no function call, then we finish + if "function_call" not in last_message.additional_kwargs: return "end" - # Otherwise, an AgentAction is returned - # Here we return `continue` string - # This will be used when setting up the graph to define the flow + # Otherwise if there is, we continue else: return "continue" + +# Define the function that calls the model +def call_model(state): + messages = state['messages'] + response = model.invoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + +# Define the function to execute tools +def call_tool(state): + messages = state['messages'] + # Based on the continue condition + # we know the last message involves a function call + last_message = messages[-1] + # We construct an ToolInvocation from the function_call + action = ToolInvocation( + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]), + ) + # We call the tool_executor and get back a response + response = tool_executor.invoke(action) + # We use the response to create a FunctionMessage + function_message = FunctionMessage(content=str(response), name=action.tool) + # We return a list, because this will get added to the existing list + return {"messages": [function_message]} ``` ### Define the graph @@ -170,59 +188,65 @@ def should_continue(data): We can now put it all together and define the graph! ```python -from langgraph.graph import END, StateGraph - +from langgraph.graph import StateGraph, END # Define a new graph - workflow = StateGraph(AgentState) +workflow = StateGraph(AgentState) - # Define the two nodes we will cycle between - workflow.add_node("agent", run_agent) - workflow.add_node("action", execute_tools) +# Define the two nodes we will cycle between +workflow.add_node("agent", call_model) +workflow.add_node("action", call_tool) - # Set the entrypoint as `agent` - # This means that this node is the first one called - workflow.set_entry_point("agent") +# Set the entrypoint as `agent` +# This means that this node is the first one called +workflow.set_entry_point("agent") - # We now add a conditional edge - workflow.add_conditional_edges( - # First, we define the start node. We use `agent`. - # This means these are the edges taken after the `agent` node is called. - "agent", - # Next, we pass in the function that will determine which node is called next. - should_continue, - # Finally we pass in a mapping. - # The keys are strings, and the values are other nodes. - # END is a special node marking that the graph should finish. - # What will happen is we will call `should_continue`, and then the output of that - # will be matched against the keys in this mapping. - # Based on which one it matches, that node will then be called. - { - # If `tools`, then we call the tool node. - "continue": "action", - # Otherwise we finish. - "end": END - } - ) +# We now add a conditional edge +workflow.add_conditional_edges( + # First, we define the start node. We use `agent`. + # This means these are the edges taken after the `agent` node is called. + "agent", + # Next, we pass in the function that will determine which node is called next. + should_continue, + # Finally we pass in a mapping. + # The keys are strings, and the values are other nodes. + # END is a special node marking that the graph should finish. + # What will happen is we will call `should_continue`, and then the output of that + # will be matched against the keys in this mapping. + # Based on which one it matches, that node will then be called. + { + # If `tools`, then we call the tool node. + "continue": "action", + # Otherwise we finish. + "end": END + } +) - # We now add a normal edge from `tools` to `agent`. - # This means that after `tools` is called, `agent` node is called next. - workflow.add_edge('action', 'agent') +# We now add a normal edge from `tools` to `agent`. +# This means that after `tools` is called, `agent` node is called next. +workflow.add_edge('action', 'agent') - # Finally, we compile it! - # This compiles it into a LangChain Runnable, - # meaning you can use it as you would any other runnable - chain = workflow.compile() +# Finally, we compile it! +# This compiles it into a LangChain Runnable, +# meaning you can use it as you would any other runnable +app = workflow.compile() ``` ### Use it! We can now use it! -This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables +This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables. +This runnable accepts a list of messages. ```python -chain.invoke({"input": "what is the weather in sf"}) +from langchain_core.messages import HumanMessage + +inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} +app.invoke(inputs) ``` +This may take a little bit - it's making a few calls behind the scenes. +In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that. + ## Streaming LangGraph has support for several different types of streaming. @@ -232,9 +256,8 @@ LangGraph has support for several different types of streaming. One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node. ```python -for output in chain.stream( - {"input": "what is the weather in sf"} -): +inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} +for output in app.stream(inputs): # stream() yields dictionaries with output keyed by node name for key, value in output.items(): print(f"Output from node '{key}':") @@ -246,25 +269,25 @@ for output in chain.stream( ``` Output from node 'agent': --- -{'agent_outcome': 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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})])} +{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n "query": "weather in San Francisco"\n}', 'name': 'tavily_search_results_json'}})]} --- Output from node 'action': --- -{'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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), "[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]")]} +{'messages': [FunctionMessage(content="[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]", name='tavily_search_results_json')]} --- Output from node 'agent': --- -{'agent_outcome': AgentFinish(return_values={'output': "It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."}, log="It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.")} +{'messages': [AIMessage(content="I couldn't find the current weather in San Francisco. However, you can visit [WeatherSpark](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to check the historical weather data for January 2024 in San Francisco.")]} --- Output from node '__end__': --- -{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': "It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."}, log="It seems that I couldn't retrieve the current weather in San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app."), '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': {'arguments': '{"query":"weather in San Francisco"}', 'name': 'tavily_search_results_json'}})]), "[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]")]} +{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n "query": "weather in San Francisco"\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content="[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]", name='tavily_search_results_json'), AIMessage(content="I couldn't find the current weather in San Francisco. However, you can visit [WeatherSpark](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to check the historical weather data for January 2024 in San Francisco.")]} --- ``` @@ -276,10 +299,8 @@ In this case only the "agent" node produces LLM tokens. In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True)`) ```python -async for output in chain.astream_log( - {"input": "what is the weather in sf", "intermediate_steps": []}, - include_types=["llm"], -): +inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} +async for output in app.astream_log(inputs, include_types=["llm"]): # astream_log() yields the requested logs (here LLMs) in JSONPatch format for op in output.ops: if op["path"] == "/streamed_output/-": @@ -294,76 +315,98 @@ async for output in chain.astream_log( ``` content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}} -content='' additional_kwargs={'function_call': {'arguments': '{"', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': '{\n', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': ' "', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': '":"', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': 'current', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': ' weather', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': '":', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': ' "', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}} content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}} -content='' additional_kwargs={'function_call': {'arguments': '"}', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': '"\n', 'name': ''}} +content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}} content='' content='' content='I' -content=' found' -content=' a' -content=' website' -content=' that' -content=' provides' -content=' detailed' -content=' weather' -content=' information' -content=' for' -content=' San' -content=' Francisco' -content='.' -content=' You' -content=' can' -content=' visit' -content=' the' -content=' following' -content=' link' -content=' for' +content="'m" +content=' sorry' +content=',' +content=' but' +content=' I' +content=' couldn' +content="'t" +content=' find' content=' the' content=' current' content=' weather' -content=' report' -content=':' -content=' [' -content='San' +content=' in' +content=' San' content=' Francisco' -content=' Weather' -content=' Report' +content='.' +content=' However' +content=',' +content=' you' +content=' can' +content=' check' +content=' the' +content=' historical' +content=' weather' +content=' data' +content=' for' +content=' January' +content=' ' +content='202' +content='4' +content=' in' +content=' San' +content=' Francisco' +content=' [' +content='here' content='](' content='https' content='://' -content='www' -content='.weather' -content='25' +content='we' +content='athers' +content='park' content='.com' -content='/n' -content='orth' -content='-' -content='amer' -content='ica' +content='/h' +content='/m' content='/' -content='usa' -content='/cal' -content='ifornia' -content='/s' +content='557' +content='/' +content='202' +content='4' +content='/' +content='1' +content='/H' +content='istorical' +content='-' +content='Weather' +content='-in' +content='-Jan' +content='uary' +content='-' +content='202' +content='4' +content='-in' +content='-S' content='an' -content='-fr' +content='-F' +content='r' content='anc' content='isco' -content=')' +content='-Cal' +content='ifornia' +content='-' +content='United' +content='-' +content='States' +content=').' content='' ``` - - - - ## When to Use When should you use this versus [LangChain Expression Language](https://python.langchain.com/docs/expression_language/)? diff --git a/examples/chat_executor_with_function_calling/base.ipynb b/examples/chat_executor_with_function_calling/base.ipynb index 8b6397135..cae3f6212 100644 --- a/examples/chat_executor_with_function_calling/base.ipynb +++ b/examples/chat_executor_with_function_calling/base.ipynb @@ -1,87 +1,244 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that uses function calling from scratch." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, { "cell_type": "code", "execution_count": 1, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", "execution_count": 2, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" ] }, { "cell_type": "code", "execution_count": 3, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], "source": [ - "from langchain.tools.render import format_tool_to_openai_function" + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" ] }, { "cell_type": "code", "execution_count": 4, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], "source": [ - "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" ] }, { "cell_type": "code", "execution_count": 5, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, { "cell_type": "code", - "execution_count": 20, - "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", "from langchain_core.messages import FunctionMessage\n", "\n", @@ -109,25 +266,33 @@ " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", " )\n", " # We call the tool_executor and get back a response\n", " response = tool_executor.invoke(action)\n", " # We use the response to create a FunctionMessage\n", " function_message = FunctionMessage(content=str(response), name=action.tool)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}\n", - "\n" + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" ] }, { "cell_type": "code", - "execution_count": 21, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "execution_count": 7, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -174,38 +339,244 @@ "app = workflow.compile()" ] }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, { "cell_type": "code", - "execution_count": 22, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "execution_count": 8, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='what is the weather in sf'),\n", + " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", + " FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'),\n", + " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "app.invoke(inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", + "metadata": {}, + "source": [ + "This may take a little bit - it's making a few calls behind the scenes.\n", + "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", + "\n", + "## Streaming\n", + "\n", + "LangGraph has support for several different types of streaming.\n", + "\n", + "### Streaming Node Output\n", + "\n", + "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Output from node 'agent':\n", + "---\n", "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n" + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "### Streaming LLM Tokens\n", + "\n", + "You can also access the LLM tokens as they are produced by each node. \n", + "In this case only the \"agent\" node produces LLM tokens.\n", + "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", + "content=''\n", + "content=''\n", + "content='I'\n", + "content=\"'m\"\n", + "content=' sorry'\n", + "content=','\n", + "content=' but'\n", + "content=' I'\n", + "content=' couldn'\n", + "content=\"'t\"\n", + "content=' find'\n", + "content=' the'\n", + "content=' current'\n", + "content=' weather'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content='.'\n", + "content=' However'\n", + "content=','\n", + "content=' you'\n", + "content=' can'\n", + "content=' check'\n", + "content=' the'\n", + "content=' historical'\n", + "content=' weather'\n", + "content=' data'\n", + "content=' for'\n", + "content=' January'\n", + "content=' '\n", + "content='202'\n", + "content='4'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' ['\n", + "content='here'\n", + "content=']('\n", + "content='https'\n", + "content='://'\n", + "content='we'\n", + "content='athers'\n", + "content='park'\n", + "content='.com'\n", + "content='/h'\n", + "content='/m'\n", + "content='/'\n", + "content='557'\n", + "content='/'\n", + "content='202'\n", + "content='4'\n", + "content='/'\n", + "content='1'\n", + "content='/H'\n", + "content='istorical'\n", + "content='-'\n", + "content='Weather'\n", + "content='-in'\n", + "content='-Jan'\n", + "content='uary'\n", + "content='-'\n", + "content='202'\n", + "content='4'\n", + "content='-in'\n", + "content='-S'\n", + "content='an'\n", + "content='-F'\n", + "content='r'\n", + "content='anc'\n", + "content='isco'\n", + "content='-Cal'\n", + "content='ifornia'\n", + "content='-'\n", + "content='United'\n", + "content='-'\n", + "content='States'\n", + "content=').'\n", + "content=''\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb b/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb index 6822c442d..4830192c8 100644 --- a/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb +++ b/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb @@ -1,29 +1,106 @@ { "cells": [ { - "cell_type": "code", - "execution_count": 2, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, - "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "# Dynamically Returning Directly\n", + "\n", + "In this example we will build a chat executor where the LLM can optionally decide to return the result of a tool call as the final answer. This is useful in cases where you have tools that can sometimes generate responses that are acceptable as final answers, and you want to use the LLM to determine when that is the case\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "75bbcaa8-b23a-409c-9745-08d3aca7c3cb", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We overwrite the default schema of the input tool to have an additional parameter for returning directly." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", "metadata": {}, "outputs": [], "source": [ "from langchain_core.pydantic_v1 import BaseModel, Field\n", "\n", - "class SearchTool(search_tool.args_schema):\n", + "class SearchTool(BaseModel):\n", " \"\"\"Look up things online, optionally returning directly\"\"\"\n", " query: str = Field(description=\"query to look up online\")\n", " return_direct: bool = Field(\n", @@ -34,85 +111,182 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "0c93c6e1-532b-472e-b9f7-d374b9d3c89e", + "execution_count": 4, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)" + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n", + "tools = [search_tool]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", - "execution_count": 11, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "execution_count": 5, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [search_tool]\n", - "model = ChatOpenAI()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", - "metadata": {}, - "outputs": [], - "source": [ - "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", + "from langgraph.prebuilt import ToolExecutor\n", + "\n", "tool_executor = ToolExecutor(tools)" ] }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, { "cell_type": "code", - "execution_count": 15, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "execution_count": 6, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, { "cell_type": "code", - "execution_count": 20, - "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "execution_count": 9, + "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", - "from langchain_core.messages import FunctionMessage\n", + "from langchain_core.messages import FunctionMessage" + ] + }, + { + "cell_type": "markdown", + "id": "50bf356c-2dbd-4f66-8fa3-133e9c2e371e", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "We change the `should_continue` function to check whether return_direct was set to True" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function that determines whether to continue or not\n", "def should_continue(state):\n", " messages = state['messages']\n", @@ -126,45 +300,83 @@ " if arguments.get(\"return_direct\", False):\n", " return \"final\"\n", " else:\n", - " return \"continue\"\n", - "\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function that calls the model\n", "def call_model(state):\n", " messages = state['messages']\n", " response = model.invoke(messages)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "markdown", + "id": "8535a36c-3ced-401e-98b5-ec1d1b434bbc", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "We change the tool calling to get rid of the `return_direct` parameter (not used in the actual tool call)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function to execute tools\n", "def call_tool(state):\n", " messages = state['messages']\n", " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", " tool_name = last_message.additional_kwargs[\"function_call\"][\"name\"]\n", " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", " if tool_name == \"tavily_search_results_json\":\n", " if \"return_direct\" in arguments:\n", " del arguments[\"return_direct\"]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " action = ToolInvocation(\n", " tool=tool_name,\n", " tool_input=arguments,\n", - " log=\"\",\n", " )\n", " # We call the tool_executor and get back a response\n", " response = tool_executor.invoke(action)\n", " # We use the response to create a FunctionMessage\n", " function_message = FunctionMessage(content=str(response), name=action.tool)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}\n", - "\n" + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We add a separate node for any tool call where `return_direct=True`. The reason this is needed is that after this node we want to end, while after other tool calls we want to go back to the LLM. " ] }, { "cell_type": "code", - "execution_count": 21, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "execution_count": 13, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -216,63 +428,114 @@ ] }, { - "cell_type": "code", - "execution_count": 22, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content='The current weather in San Francisco is not available. However, you can check the weather history for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The current weather in San Francisco is not available. However, you can check the weather history for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n" - ] - } - ], "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." ] }, { "cell_type": "code", - "execution_count": 27, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "execution_count": 15, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n" + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'final':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf? return this result directly by setting return_direct = True\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "2d2e89d8-0e35-465c-8984-1aa37a132b03", + "id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb b/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb index d4fb92bf5..f6457742a 100644 --- a/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb +++ b/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb @@ -1,87 +1,248 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Force Calling a Tool First\n", + "\n", + "In this example we will build a chat executor that always calls a certain tool first. In this example, we will create an agent with a search tool. However, at the start we will force the agent to call the search tool (and then let it do whatever it wants after). This is useful when you want to force agents to call particular tools, but still want flexibility of what happens after that.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, { "cell_type": "code", "execution_count": 1, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", "execution_count": 2, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" ] }, { "cell_type": "code", "execution_count": 3, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], "source": [ - "from langchain.tools.render import format_tool_to_openai_function" + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" ] }, { "cell_type": "code", "execution_count": 4, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], "source": [ - "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" ] }, { "cell_type": "code", "execution_count": 5, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, { "cell_type": "code", - "execution_count": 7, - "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", "from langchain_core.messages import FunctionMessage\n", "\n", @@ -109,46 +270,33 @@ " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", " )\n", " # We call the tool_executor and get back a response\n", " response = tool_executor.invoke(action)\n", " # We use the response to create a FunctionMessage\n", " function_message = FunctionMessage(content=str(response), name=action.tool)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}\n", - "\n" + " return {\"messages\": [function_message]}" ] }, { - "cell_type": "code", - "execution_count": 9, - "id": "47bf79f1-652c-43dc-aeb3-0f0de4400539", + "cell_type": "markdown", + "id": "7c3e0ac2-0c89-4751-bc2c-f644654841d1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'tavily_search_results_json'" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "tools[0].name" + "**MODIFICATION**\n", + "\n", + "Here we create a node that returns an AIMessage with a tool call - we will use this at the start to force it call a tool" ] }, { "cell_type": "code", - "execution_count": 11, - "id": "ab62da37-9632-4dc8-833b-feb4c62bab37", + "execution_count": 7, + "id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38", "metadata": {}, "outputs": [], "source": [ @@ -158,22 +306,39 @@ "\n", "def first_model(state):\n", " human_input = state['messages'][-1].content\n", - " return {\"messages\": [AIMessage(\n", - " content=\"\", \n", - " additional_kwargs={\n", - " \"function_call\": {\n", - " \"name\": \"tavily_search_results_json\", \n", - " \"arguments\": json.dumps({\"query\": human_input})\n", - " }\n", - " }\n", - " )\n", - " ]}" + " return {\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=\"\", \n", + " additional_kwargs={\n", + " \"function_call\": {\n", + " \"name\": \"tavily_search_results_json\", \n", + " \"arguments\": json.dumps({\"query\": human_input})\n", + " }\n", + " }\n", + " )\n", + " ]\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We will define a `first_agent` node which we will set as the entrypoint." ] }, { "cell_type": "code", - "execution_count": 12, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "execution_count": 8, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -226,38 +391,71 @@ "app = workflow.compile()" ] }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, { "cell_type": "code", - "execution_count": 13, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "execution_count": 9, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Output from node 'first_agent':\n", + "---\n", "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this link](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to see the historical weather data for January 2024 in San Francisco.\")]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can visit [this link](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to see the historical weather data for January 2024 in San Francisco.\")]}\n", - "----\n" + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb b/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb index 5f6219809..2c4de7109 100644 --- a/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb +++ b/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb @@ -1,87 +1,248 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that has a human in the loop. We will use the human to approve specific actions.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, { "cell_type": "code", "execution_count": 1, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", "execution_count": 2, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" ] }, { "cell_type": "code", "execution_count": 3, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], "source": [ - "from langchain.tools.render import format_tool_to_openai_function" + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" ] }, { "cell_type": "code", "execution_count": 4, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], "source": [ - "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" ] }, { "cell_type": "code", "execution_count": 5, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, { "cell_type": "code", - "execution_count": 7, - "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "execution_count": 6, + "id": "b547109f-f9e8-4e77-a7e7-ed2bae7a72ab", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", "from langchain_core.messages import FunctionMessage\n", "\n", @@ -101,20 +262,36 @@ " messages = state['messages']\n", " response = model.invoke(messages)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "markdown", + "id": "ac402f66-4442-4a1f-9f9b-4a5d97532ceb", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "We modify the function that is calling the tool to first ask for user approval to continue. Note that this is a simple example and we could modify it to change the tool input, use some other channel besides input, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "73fd6432-42e8-472a-89ca-bb5ddbbcc35a", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function to execute tools\n", - "# Here we add some lines to add a human-in-the-loop\n", "def call_tool(state):\n", " messages = state['messages']\n", " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", " )\n", " response = input(prompt=f\"[y/n] continue with: {action}?\")\n", " if response == \"n\":\n", @@ -124,14 +301,23 @@ " # We use the response to create a FunctionMessage\n", " function_message = FunctionMessage(content=str(response), name=action.tool)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}\n", - "\n" + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" ] }, { "cell_type": "code", "execution_count": 8, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -178,51 +364,84 @@ "app = workflow.compile()" ] }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, { "cell_type": "code", - "execution_count": 9, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "execution_count": 10, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Output from node 'agent':\n", + "---\n", "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n" + "\n", + "---\n", + "\n" ] }, { "name": "stdin", "output_type": "stream", "text": [ - "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'} log=''? y\n" + "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'}? y\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the historical weather data for San Francisco in January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the historical weather data for San Francisco in January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n" + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb b/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb index b136431f5..0e547d597 100644 --- a/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb +++ b/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb @@ -1,87 +1,248 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that better manages intermediate steps. The base chat executor will just put all messages into the model, but if the intermediate steps an agent is taking start to get long, you may want to modify that. In this example we will only include the ten most recent messages.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, { "cell_type": "code", "execution_count": 1, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", "execution_count": 2, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" ] }, { "cell_type": "code", "execution_count": 3, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], "source": [ - "from langchain.tools.render import format_tool_to_openai_function" + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" ] }, { "cell_type": "code", "execution_count": 4, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], "source": [ - "model = model.bind_functions([format_tool_to_openai_function(t) for t in tools])" + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" ] }, { "cell_type": "code", "execution_count": 5, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, { "cell_type": "code", - "execution_count": 20, - "id": "f422dc99-672e-414f-88b3-1a6dd21b6882", + "execution_count": null, + "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", "from langchain_core.messages import FunctionMessage\n", "\n", @@ -94,42 +255,74 @@ " return \"end\"\n", " # Otherwise if there is, we continue\n", " else:\n", - " return \"continue\"\n", + " return \"continue\"" + ] + }, + { + "cell_type": "markdown", + "id": "a763aa63-701c-40fa-a9d3-9d992ebe7e4d", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "Here we don't pass all messages to the model but rather only pass the five most recent. Note that this is a pretty simplistic way to handle messages, and there may be other methods you may want to look into depending on your use case" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function that calls the model\n", "def call_model(state):\n", - " messages = state['messages']\n", - " if len(messages) > 10:\n", - " messages = messages[-10:]\n", + " messages = state['messages'][-5:]\n", " response = model.invoke(messages)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b3ca9564-63cc-4309-b158-5e8d3e907164", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function to execute tools\n", "def call_tool(state):\n", " messages = state['messages']\n", " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", " )\n", " # We call the tool_executor and get back a response\n", " response = tool_executor.invoke(action)\n", " # We use the response to create a FunctionMessage\n", " function_message = FunctionMessage(content=str(response), name=action.tool)\n", " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}\n", - "\n" + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" ] }, { "cell_type": "code", - "execution_count": 21, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "execution_count": 9, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -176,38 +369,71 @@ "app = workflow.compile()" ] }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, { "cell_type": "code", - "execution_count": 22, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "execution_count": 10, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Output from node 'agent':\n", + "---\n", "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather history for January 2024 in San Francisco on [weatherspark.com](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).')]}\n", - "----\n" + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/chat_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_executor_with_function_calling/respond-in-format.ipynb index 85085b8e1..8ca16498b 100644 --- a/examples/chat_executor_with_function_calling/respond-in-format.ipynb +++ b/examples/chat_executor_with_function_calling/respond-in-format.ipynb @@ -1,112 +1,270 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that responds in a specific format. We will do this by using OpenAI function calling. This is useful when you want to enforce that an agent's response is in a specific format. In this example, we will ask it respond as if a weatherman, so to return the temperature and then any other additional info.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, { "cell_type": "code", "execution_count": 1, - "id": "aa9f9110-ea74-43af-aa72-6b45518abd6e", + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt.messages_executor import create_messages_executor\n", - "from langchain_core.messages import HumanMessage" + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" ] }, { "cell_type": "code", "execution_count": 2, - "id": "0f005b2d-ddc0-4d60-8af9-9eb3dbeb45b7", + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" ] }, { "cell_type": "code", "execution_count": 3, - "id": "02831581-f1ba-4701-ba33-f0f68468907d", + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], "source": [ - "from langchain.tools.render import format_tool_to_openai_function" + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n", + "\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We also want to define a response schema for the language model and bind it to the model as a function as well" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "a3bdf328-f34c-421e-9771-85f2740fbad6", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], "source": [ - "# Here we bind an additional function beside just tools - the response format\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "\n", + "from langchain.tools.render import format_tool_to_openai_function\n", "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function\n", "\n", "class Response(BaseModel):\n", " \"\"\"Final response to the user\"\"\"\n", " temperature: float = Field(description=\"the temperature\")\n", " other_notes: str = Field(description=\"any other notes about the weather\")\n", "\n", - "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function" + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "functions.append(convert_pydantic_to_openai_function(Response))\n", + "model = model.bind_functions(functions)" ] }, { - "cell_type": "code", - "execution_count": 6, - "id": "a5c0ca12-4922-461c-b740-62ff99f8ae56", + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", "metadata": {}, - "outputs": [], "source": [ - "model = model.bind_functions(functions + [convert_pydantic_to_openai_function(Response)])" + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "57547622-ddd8-4179-aa4a-e1b69ca4523c", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "96141ebd-32af-4c9d-a0b0-f46482b8bb88", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], "source": [ "from typing import TypedDict, Annotated, Sequence\n", "import operator\n", "from langchain_core.messages import BaseMessage\n", - "# We create the AgentState that we will pass around\n", - "# This simply involves a list of messages\n", - "# We want steps to return messages to append to the list\n", - "# So we annotate the messages attribute with operator.add\n", + "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]" ] }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We will change the `should_continue` function to check what function was called. If the function `Response` was called - that is the function that is NOT a tool, but rather the formatted response, so we should NOT continue in that case." + ] + }, { "cell_type": "code", - "execution_count": 14, - "id": "0759bddc-010a-4e3f-9f41-3a51c1ea5144", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.agents import AgentAction\n", + "from langgraph.prebuilt import ToolInvocation\n", "import json\n", "from langchain_core.messages import FunctionMessage\n", "\n", "# Define the function that determines whether to continue or not\n", - "# This needs to get updated\n", "def should_continue(state):\n", " messages = state['messages']\n", " last_message = messages[-1]\n", @@ -116,17 +274,10 @@ " # Otherwise if there is, we need to check what type of function call it is\n", " elif last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Response\":\n", " return \"end\"\n", + " # Otherwise we continue\n", " else:\n", - " return \"continue\"" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "2a048e72-525a-4dcb-a91d-efacaddd8848", - "metadata": {}, - "outputs": [], - "source": [ + " return \"continue\"\n", + "\n", "# Define the function that calls the model\n", "def call_model(state):\n", " messages = state['messages']\n", @@ -140,11 +291,10 @@ " # Based on the continue condition\n", " # we know the last message involves a function call\n", " last_message = messages[-1]\n", - " # We construct an AgentAction from the function_call\n", - " action = AgentAction(\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " log=\"\",\n", " )\n", " # We call the tool_executor and get back a response\n", " response = tool_executor.invoke(action)\n", @@ -154,10 +304,20 @@ " return {\"messages\": [function_message]}" ] }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, { "cell_type": "code", - "execution_count": 16, - "id": "1133ec83-7af9-4444-9f88-c793fbdce214", + "execution_count": 7, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ @@ -204,38 +364,71 @@ "app = workflow.compile()" ] }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, { "cell_type": "code", - "execution_count": 17, - "id": "289c648f-bfc6-464f-8df9-50be8b1b9e48", + "execution_count": 8, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Output from node 'agent':\n", + "---\n", "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 80,\\n \"other_notes\": \"Mostly cloudy\"\\n}', 'name': 'Response'}})]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San Francisco80 deg, E Cloud Cover Mostly Cloudy 18,000 ft Mostly Clear 4,000 ft Partly Cloudy 15,000 ft Raw: KSFO 121956Z 08006KT 10SM FEW040 SCT150 BKN180 11/07 A3028 RMK AO2 SLP254 T01110067 This report shows the past weather for San Francisco, providing a weather history for January 2024.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 80,\\n \"other_notes\": \"Mostly cloudy\"\\n}', 'name': 'Response'}})]}\n", - "----\n" + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 50,\\n \"other_notes\": \"Tolerable weather\"\\n}', 'name': 'Response'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 50,\\n \"other_notes\": \"Tolerable weather\"\\n}', 'name': 'Response'}})]}\n", + "\n", + "---\n", + "\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "4cd4b39f-831a-4818-bf56-99cf301b0555", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] diff --git a/langgraph/prebuilt/__init__.py b/langgraph/prebuilt/__init__.py index 89dfa34a3..e85922374 100644 --- a/langgraph/prebuilt/__init__.py +++ b/langgraph/prebuilt/__init__.py @@ -1,5 +1,5 @@ from langgraph.prebuilt.agent_executor import create_agent_executor from langgraph.prebuilt import chat_executor -from langgraph.prebuilt.tool_executor import ToolExecutor +from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation -__all__ = ["create_agent_executor", "chat_executor", "ToolExecutor"] +__all__ = ["create_agent_executor", "chat_executor", "ToolExecutor", ToolInvocation] diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py index 121ef0855..82fe5e88a 100644 --- a/langgraph/prebuilt/tool_executor.py +++ b/langgraph/prebuilt/tool_executor.py @@ -1,14 +1,30 @@ from typing import Any, Sequence -from langchain_core.agents import AgentAction +from typing import Union from langchain_core.runnables import RunnableBinding, RunnableLambda from langchain_core.tools import BaseTool +from langchain_core.load.serializable import Serializable INVALID_TOOL_MSG_TEMPLATE = ( "{requested_tool_name} is not a valid tool, " "try one of [{available_tool_names_str}]." ) + +class ToolInvocationInterface: + """Interface for invoking a tool""" + tool: str + tool_input: Union[str, dict] + + +class ToolInvocation(Serializable): + """Information about how to invoke a tool.""" + + tool: str + """The name of the Tool to execute.""" + tool_input: Union[str, dict] + """The input to pass in to the Tool.""" + class ToolExecutor(RunnableBinding): tools: Sequence[BaseTool] @@ -19,7 +35,7 @@ class ToolExecutor(RunnableBinding): bound = RunnableLambda(self._execute, afunc=self._aexecute) super().__init__(bound=bound, tools=tools, tool_map ={t.name: t for t in tools}, invalid_tool_msg_template=invalid_tool_msg_template, **kwargs) - def _execute(self, tool_invocation: AgentAction) -> Any: + def _execute(self, tool_invocation: ToolInvocationInterface) -> Any: if tool_invocation.tool not in self.tool_map: return self.invalid_tool_msg_template.format( requested_tool_name=tool_invocation.tool, @@ -30,7 +46,7 @@ class ToolExecutor(RunnableBinding): output = tool.invoke(tool_invocation.tool_input) return output - async def _aexecute(self, tool_invocation: AgentAction) -> Any: + async def _aexecute(self, tool_invocation: ToolInvocationInterface) -> Any: if tool_invocation.tool not in self.tool_map: return self.invalid_tool_msg_template.format( requested_tool_name=tool_invocation.tool, From 1ffdd7b93dee42afbce34d319d67736c894c12b8 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 15:34:17 -0800 Subject: [PATCH 13/25] cr --- README.md | 8 ++++---- .../high-level.ipynb | 4 ++-- .../respond-in-format.ipynb | 16 ++++------------ langgraph/prebuilt/__init__.py | 4 ++-- .../{chat_executor.py => chat_agent_executor.py} | 0 5 files changed, 12 insertions(+), 20 deletions(-) rename langgraph/prebuilt/{chat_executor.py => chat_agent_executor.py} (100%) diff --git a/README.md b/README.md index 21fe41dfa..823ffb483 100644 --- a/README.md +++ b/README.md @@ -618,10 +618,10 @@ tool_executor = ToolExecutor(tools) It then exposes a [runnable interface](https://python.langchain.com/docs/expression_language/interface). It can be used to call tools: you can pass in an [AgentAction](https://python.langchain.com/docs/modules/agents/concepts#agentaction) and it will look up the relevant tool and call it with the appropriate input. -### chat_executor.create_function_calling_executor +### chat_agent_executor.create_function_calling_executor ```python -from langgraph.prebuilt import chat_executor +from langgraph.prebuilt import chat_agent_executor ``` This is a helper function for creating a graph that works with a chat model that utilizes function calling. @@ -631,13 +631,13 @@ The model must be one that supports OpenAI function calling. ```python from langchain_openai import ChatOpenAI from langchain_community.tools.tavily_search import TavilySearchResults -from langgraph.prebuilt import chat_executor +from langgraph.prebuilt import chat_agent_executor from langchain_core.messages import HumanMessage tools = [TavilySearchResults(max_results=1)] model = ChatOpenAI() -app = chat_executor.create_function_calling_executor(model, tools) +app = chat_agent_executor.create_function_calling_executor(model, tools) inputs = {"messages": [HumanMessage(content="what is the weather in sf")]} for s in app.stream(inputs): diff --git a/examples/chat_executor_with_function_calling/high-level.ipynb b/examples/chat_executor_with_function_calling/high-level.ipynb index 9eed3d07c..f4fc724fc 100644 --- a/examples/chat_executor_with_function_calling/high-level.ipynb +++ b/examples/chat_executor_with_function_calling/high-level.ipynb @@ -32,7 +32,7 @@ "source": [ "from langchain_openai import ChatOpenAI\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt import chat_executor\n", + "from langgraph.prebuilt import chat_agent_executor\n", "from langchain_core.messages import HumanMessage" ] }, @@ -64,7 +64,7 @@ "metadata": {}, "outputs": [], "source": [ - "app = chat_executor.create_function_calling_executor(model, tools)" + "app = chat_agent_executor.create_function_calling_executor(model, tools)" ] }, { diff --git a/examples/chat_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_executor_with_function_calling/respond-in-format.ipynb index 8ca16498b..08e3a3ced 100644 --- a/examples/chat_executor_with_function_calling/respond-in-format.ipynb +++ b/examples/chat_executor_with_function_calling/respond-in-format.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], @@ -393,19 +393,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 50,\\n \"other_notes\": \"Tolerable weather\"\\n}', 'name': 'Response'}})]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 50,\\n \"other_notes\": \"Tolerable weather\"\\n}', 'name': 'Response'}})]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", "\n", "---\n", "\n" @@ -424,14 +424,6 @@ " print(value)\n", " print(\"\\n---\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/langgraph/prebuilt/__init__.py b/langgraph/prebuilt/__init__.py index e85922374..fb2340d42 100644 --- a/langgraph/prebuilt/__init__.py +++ b/langgraph/prebuilt/__init__.py @@ -1,5 +1,5 @@ from langgraph.prebuilt.agent_executor import create_agent_executor -from langgraph.prebuilt import chat_executor +from langgraph.prebuilt import chat_agent_executor from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation -__all__ = ["create_agent_executor", "chat_executor", "ToolExecutor", ToolInvocation] +__all__ = ["create_agent_executor", "chat_agent_executor", "ToolExecutor", ToolInvocation] diff --git a/langgraph/prebuilt/chat_executor.py b/langgraph/prebuilt/chat_agent_executor.py similarity index 100% rename from langgraph/prebuilt/chat_executor.py rename to langgraph/prebuilt/chat_agent_executor.py From 7bfeb1758afa7f0843ccbe32bb027dd76b6a779a Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 15:35:11 -0800 Subject: [PATCH 14/25] cr --- .../base.ipynb | 606 ------------------ .../dynamically-returning-directly.ipynb | 565 ---------------- .../force-calling-a-tool-first.ipynb | 485 -------------- .../high-level.ipynb | 136 ---- .../human-in-the-loop.ipynb | 471 -------------- .../managing-agent-steps.ipynb | 463 ------------- .../respond-in-format.ipynb | 450 ------------- 7 files changed, 3176 deletions(-) delete mode 100644 examples/chat_executor_with_function_calling/base.ipynb delete mode 100644 examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb delete mode 100644 examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb delete mode 100644 examples/chat_executor_with_function_calling/high-level.ipynb delete mode 100644 examples/chat_executor_with_function_calling/human-in-the-loop.ipynb delete mode 100644 examples/chat_executor_with_function_calling/managing-agent-steps.ipynb delete mode 100644 examples/chat_executor_with_function_calling/respond-in-format.ipynb diff --git a/examples/chat_executor_with_function_calling/base.ipynb b/examples/chat_executor_with_function_calling/base.ipynb deleted file mode 100644 index cae3f6212..000000000 --- a/examples/chat_executor_with_function_calling/base.ipynb +++ /dev/null @@ -1,606 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Chat Executor\n", - "\n", - "In this example we will build a chat executor that uses function calling from scratch." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " action = ToolInvocation(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content='what is the weather in sf'),\n", - " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", - " FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'),\n", - " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "app.invoke(inputs)" - ] - }, - { - "cell_type": "markdown", - "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", - "metadata": {}, - "source": [ - "This may take a little bit - it's making a few calls behind the scenes.\n", - "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", - "\n", - "## Streaming\n", - "\n", - "LangGraph has support for several different types of streaming.\n", - "\n", - "### Streaming Node Output\n", - "\n", - "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", - "metadata": {}, - "source": [ - "### Streaming LLM Tokens\n", - "\n", - "You can also access the LLM tokens as they are produced by each node. \n", - "In this case only the \"agent\" node produces LLM tokens.\n", - "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "cfd140f0-a5a6-4697-8115-322242f197b5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", - "content=''\n", - "content=''\n", - "content='I'\n", - "content=\"'m\"\n", - "content=' sorry'\n", - "content=','\n", - "content=' but'\n", - "content=' I'\n", - "content=' couldn'\n", - "content=\"'t\"\n", - "content=' find'\n", - "content=' the'\n", - "content=' current'\n", - "content=' weather'\n", - "content=' in'\n", - "content=' San'\n", - "content=' Francisco'\n", - "content='.'\n", - "content=' However'\n", - "content=','\n", - "content=' you'\n", - "content=' can'\n", - "content=' check'\n", - "content=' the'\n", - "content=' historical'\n", - "content=' weather'\n", - "content=' data'\n", - "content=' for'\n", - "content=' January'\n", - "content=' '\n", - "content='202'\n", - "content='4'\n", - "content=' in'\n", - "content=' San'\n", - "content=' Francisco'\n", - "content=' ['\n", - "content='here'\n", - "content=']('\n", - "content='https'\n", - "content='://'\n", - "content='we'\n", - "content='athers'\n", - "content='park'\n", - "content='.com'\n", - "content='/h'\n", - "content='/m'\n", - "content='/'\n", - "content='557'\n", - "content='/'\n", - "content='202'\n", - "content='4'\n", - "content='/'\n", - "content='1'\n", - "content='/H'\n", - "content='istorical'\n", - "content='-'\n", - "content='Weather'\n", - "content='-in'\n", - "content='-Jan'\n", - "content='uary'\n", - "content='-'\n", - "content='202'\n", - "content='4'\n", - "content='-in'\n", - "content='-S'\n", - "content='an'\n", - "content='-F'\n", - "content='r'\n", - "content='anc'\n", - "content='isco'\n", - "content='-Cal'\n", - "content='ifornia'\n", - "content='-'\n", - "content='United'\n", - "content='-'\n", - "content='States'\n", - "content=').'\n", - "content=''\n" - ] - } - ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", - " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", - " for op in output.ops:\n", - " if op[\"path\"] == \"/streamed_output/-\":\n", - " # this is the output from .stream()\n", - " ...\n", - " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", - " \"/streamed_output/-\"\n", - " ):\n", - " # because we chose to only include LLMs, these are LLM tokens\n", - " print(op[\"value\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb b/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb deleted file mode 100644 index 4830192c8..000000000 --- a/examples/chat_executor_with_function_calling/dynamically-returning-directly.ipynb +++ /dev/null @@ -1,565 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Dynamically Returning Directly\n", - "\n", - "In this example we will build a chat executor where the LLM can optionally decide to return the result of a tool call as the final answer. This is useful in cases where you have tools that can sometimes generate responses that are acceptable as final answers, and you want to use the LLM to determine when that is the case\n", - "\n", - "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", - "\n", - "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n", - "\n", - "**MODIFICATION**\n", - "\n", - "We overwrite the default schema of the input tool to have an additional parameter for returning directly." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "class SearchTool(BaseModel):\n", - " \"\"\"Look up things online, optionally returning directly\"\"\"\n", - " query: str = Field(description=\"query to look up online\")\n", - " return_direct: bool = Field(\n", - " description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\", \n", - " default = False\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n", - "tools = [search_tool]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage" - ] - }, - { - "cell_type": "markdown", - "id": "50bf356c-2dbd-4f66-8fa3-133e9c2e371e", - "metadata": {}, - "source": [ - "**MODIFICATION**\n", - "\n", - "We change the `should_continue` function to check whether return_direct was set to True" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we check if it's suppose to return direct\n", - " else:\n", - " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", - " if arguments.get(\"return_direct\", False):\n", - " return \"final\"\n", - " else:\n", - " return \"continue\"" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] - }, - { - "cell_type": "markdown", - "id": "8535a36c-3ced-401e-98b5-ec1d1b434bbc", - "metadata": {}, - "source": [ - "**MODIFICATION**\n", - "\n", - "We change the tool calling to get rid of the `return_direct` parameter (not used in the actual tool call)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_name = last_message.additional_kwargs[\"function_call\"][\"name\"]\n", - " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", - " if tool_name == \"tavily_search_results_json\":\n", - " if \"return_direct\" in arguments:\n", - " del arguments[\"return_direct\"]\n", - " action = ToolInvocation(\n", - " tool=tool_name,\n", - " tool_input=arguments,\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!\n", - "\n", - "**MODIFICATION**\n", - "\n", - "We add a separate node for any tool call where `return_direct=True`. The reason this is needed is that after this node we want to end, while after other tool calls we want to go back to the LLM. " - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "workflow.add_node(\"final\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Final call\n", - " \"final\": \"final\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "workflow.add_edge('final', END)\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'final':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf? return this result directly by setting return_direct = True\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb b/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb deleted file mode 100644 index f6457742a..000000000 --- a/examples/chat_executor_with_function_calling/force-calling-a-tool-first.ipynb +++ /dev/null @@ -1,485 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Force Calling a Tool First\n", - "\n", - "In this example we will build a chat executor that always calls a certain tool first. In this example, we will create an agent with a search tool. However, at the start we will force the agent to call the search tool (and then let it do whatever it wants after). This is useful when you want to force agents to call particular tools, but still want flexibility of what happens after that.\n", - "\n", - "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", - "\n", - "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " action = ToolInvocation(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "7c3e0ac2-0c89-4751-bc2c-f644654841d1", - "metadata": {}, - "source": [ - "**MODIFICATION**\n", - "\n", - "Here we create a node that returns an AIMessage with a tool call - we will use this at the start to force it call a tool" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38", - "metadata": {}, - "outputs": [], - "source": [ - "# This is the new first - the first call of the model we want to explicitly hard-code some action\n", - "from langchain_core.messages import AIMessage\n", - "import json\n", - "\n", - "def first_model(state):\n", - " human_input = state['messages'][-1].content\n", - " return {\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=\"\", \n", - " additional_kwargs={\n", - " \"function_call\": {\n", - " \"name\": \"tavily_search_results_json\", \n", - " \"arguments\": json.dumps({\"query\": human_input})\n", - " }\n", - " }\n", - " )\n", - " ]\n", - " }" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!\n", - "\n", - "**MODIFICATION**\n", - "\n", - "We will define a `first_agent` node which we will set as the entrypoint." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the new entrypoint\n", - "workflow.add_node(\"first_agent\", first_model)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"first_agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "# After we call the first agent, we know we want to go to action\n", - "workflow.add_edge('first_agent', 'action')\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'first_agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/high-level.ipynb b/examples/chat_executor_with_function_calling/high-level.ipynb deleted file mode 100644 index f4fc724fc..000000000 --- a/examples/chat_executor_with_function_calling/high-level.ipynb +++ /dev/null @@ -1,136 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8bcd1a3d-7c50-4f58-be4e-1ed654aa33be", - "metadata": {}, - "source": [ - "# Chat Executor: with function calling\n", - "\n", - "This notebook walks through an example creating a chat executor that uses function calling.\n", - "This is useful for getting started quickly.\n", - "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder." - ] - }, - { - "cell_type": "markdown", - "id": "e130cf70-a30e-47d7-8fd5-464f1a92e374", - "metadata": {}, - "source": [ - "## Set up the chat model and tools\n", - "\n", - "Here we will define the chat model and tools that we want to use.\n", - "Importantly, this model MUST support OpenAI function calling." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "efb7e3c0-c63f-40f6-93ce-19681d650fc2", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langgraph.prebuilt import chat_agent_executor\n", - "from langchain_core.messages import HumanMessage" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "a7025f33-3160-41cf-868b-17ebc916fb1d", - "metadata": {}, - "outputs": [], - "source": [ - "tools = [TavilySearchResults(max_results=1)]\n", - "model = ChatOpenAI()" - ] - }, - { - "cell_type": "markdown", - "id": "43064805-2ac9-4b5a-850c-a68dd7282350", - "metadata": {}, - "source": [ - "## Create executor\n", - "\n", - "We can now use the high level interface to create the executor" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "32b4ae66-f667-4a8b-a602-503fd0effcd9", - "metadata": {}, - "outputs": [], - "source": [ - "app = chat_agent_executor.create_function_calling_executor(model, tools)" - ] - }, - { - "cell_type": "markdown", - "id": "d63dbfc7-a5c1-4a03-991c-f0789ba52c52", - "metadata": {}, - "source": [ - "We can now invoke this executor. The input to this must be a dictionary with a single `messsages` key that contains a list of messages." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "0abc5655-d772-450c-832f-1fee1111a5f6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "----\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "----\n", - "{'messages': [AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "----\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "----\n" - ] - } - ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb b/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb deleted file mode 100644 index 2c4de7109..000000000 --- a/examples/chat_executor_with_function_calling/human-in-the-loop.ipynb +++ /dev/null @@ -1,471 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Chat Executor\n", - "\n", - "In this example we will build a chat executor that has a human in the loop. We will use the human to approve specific actions.\n", - "\n", - "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", - "\n", - "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "b547109f-f9e8-4e77-a7e7-ed2bae7a72ab", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] - }, - { - "cell_type": "markdown", - "id": "ac402f66-4442-4a1f-9f9b-4a5d97532ceb", - "metadata": {}, - "source": [ - "**MODIFICATION**\n", - "\n", - "We modify the function that is calling the tool to first ask for user approval to continue. Note that this is a simple example and we could modify it to change the tool input, use some other channel besides input, etc." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "73fd6432-42e8-472a-89ca-bb5ddbbcc35a", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " action = ToolInvocation(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " )\n", - " response = input(prompt=f\"[y/n] continue with: {action}?\")\n", - " if response == \"n\":\n", - " raise ValueError\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n" - ] - }, - { - "name": "stdin", - "output_type": "stream", - "text": [ - "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'}? y\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb b/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb deleted file mode 100644 index 0e547d597..000000000 --- a/examples/chat_executor_with_function_calling/managing-agent-steps.ipynb +++ /dev/null @@ -1,463 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Chat Executor\n", - "\n", - "In this example we will build a chat executor that better manages intermediate steps. The base chat executor will just put all messages into the model, but if the intermediate steps an agent is taking start to get long, you may want to modify that. In this example we will only include the ten most recent messages.\n", - "\n", - "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", - "\n", - "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"" - ] - }, - { - "cell_type": "markdown", - "id": "a763aa63-701c-40fa-a9d3-9d992ebe7e4d", - "metadata": {}, - "source": [ - "**MODIFICATION**\n", - "\n", - "Here we don't pass all messages to the model but rather only pass the five most recent. Note that this is a pretty simplistic way to handle messages, and there may be other methods you may want to look into depending on your use case" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages'][-5:]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b3ca9564-63cc-4309-b158-5e8d3e907164", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " action = ToolInvocation(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/chat_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_executor_with_function_calling/respond-in-format.ipynb deleted file mode 100644 index 08e3a3ced..000000000 --- a/examples/chat_executor_with_function_calling/respond-in-format.ipynb +++ /dev/null @@ -1,450 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# Chat Executor\n", - "\n", - "In this example we will build a chat executor that responds in a specific format. We will do this by using OpenAI function calling. This is useful when you want to enforce that an agent's response is in a specific format. In this example, we will ask it respond as if a weatherman, so to return the temperature and then any other additional info.\n", - "\n", - "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", - "\n", - "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import getpass\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] - }, - { - "cell_type": "markdown", - "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", - "metadata": {}, - "source": [ - "## Set up the tools\n", - "\n", - "We will first define the tools we want to use.\n", - "For this simple example, we will use a built-in search tool via Tavily.\n", - "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] - }, - { - "cell_type": "markdown", - "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", - "metadata": {}, - "source": [ - "We can now wrap these tools in a simple ToolExecutor.\n", - "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] - }, - { - "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", - "metadata": {}, - "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] - }, - { - "cell_type": "markdown", - "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", - "metadata": {}, - "source": [ - "\n", - "After we've done this, we should make sure the model knows that it has these tools available to call.\n", - "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n", - "\n", - "\n", - "**MODIFICATION**\n", - "\n", - "We also want to define a response schema for the language model and bind it to the model as a function as well" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.tools.render import format_tool_to_openai_function\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function\n", - "\n", - "class Response(BaseModel):\n", - " \"\"\"Final response to the user\"\"\"\n", - " temperature: float = Field(description=\"the temperature\")\n", - " other_notes: str = Field(description=\"any other notes about the weather\")\n", - "\n", - "\n", - "functions = [format_tool_to_openai_function(t) for t in tools]\n", - "functions.append(convert_pydantic_to_openai_function(Response))\n", - "model = model.bind_functions(functions)" - ] - }, - { - "cell_type": "markdown", - "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", - "metadata": {}, - "source": [ - "## Define the agent state\n", - "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", - "This graph is parameterized by a state object that it passes around to each node.\n", - "Each node then returns operations to update that state.\n", - "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", - "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", - "\n", - "For this example, the state we will track will just be a list of messages.\n", - "We want each node to just add messages to that list.\n", - "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ea793afa-2eab-4901-910d-6eed90cd6564", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated, Sequence\n", - "import operator\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] - }, - { - "cell_type": "markdown", - "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", - "metadata": {}, - "source": [ - "## Define the nodes\n", - "\n", - "We now need to define a few different nodes in our graph.\n", - "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", - "There are two main nodes we need for this:\n", - "\n", - "1. The agent: responsible for deciding what (if any) actions to take.\n", - "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", - "\n", - "We will also need to define some edges.\n", - "Some of these edges may be conditional.\n", - "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", - "The path that is taken is not known until that node is run (the LLM decides).\n", - "\n", - "1. Conditional Edge: after the agent is called, we should either:\n", - " a. If the agent said to take an action, then the function to invoke tools should be called\n", - " b. If the agent said that it was finished, then it should finish\n", - "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", - "\n", - "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", - "\n", - "**MODIFICATION**\n", - "\n", - "We will change the `should_continue` function to check what function was called. If the function `Response` was called - that is the function that is NOT a tool, but rather the formatted response, so we should NOT continue in that case." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolInvocation\n", - "import json\n", - "from langchain_core.messages import FunctionMessage\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if \"function_call\" not in last_message.additional_kwargs:\n", - " return \"end\"\n", - " # Otherwise if there is, we need to check what type of function call it is\n", - " elif last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Response\":\n", - " return \"end\"\n", - " # Otherwise we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state['messages']\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " action = ToolInvocation(\n", - " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", - " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = FunctionMessage(content=str(response), name=action.tool)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] - }, - { - "cell_type": "markdown", - "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", - "metadata": {}, - "source": [ - "## Define the graph\n", - "\n", - "We can now put it all together and define the graph!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.graph import StateGraph, END\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END\n", - " }\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge('action', 'agent')\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] - }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\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.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 47dfc5f8c8666517e565c443b0d528c14bf50897 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 15:35:19 -0800 Subject: [PATCH 15/25] cr --- .../base.ipynb | 606 ++++++++++++++++++ .../dynamically-returning-directly.ipynb | 565 ++++++++++++++++ .../force-calling-a-tool-first.ipynb | 485 ++++++++++++++ .../high-level.ipynb | 136 ++++ .../human-in-the-loop.ipynb | 471 ++++++++++++++ .../managing-agent-steps.ipynb | 463 +++++++++++++ .../respond-in-format.ipynb | 450 +++++++++++++ 7 files changed, 3176 insertions(+) create mode 100644 examples/chat_agent_executor_with_function_calling/base.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/high-level.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb create mode 100644 examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb diff --git a/examples/chat_agent_executor_with_function_calling/base.ipynb b/examples/chat_agent_executor_with_function_calling/base.ipynb new file mode 100644 index 000000000..cae3f6212 --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/base.ipynb @@ -0,0 +1,606 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that uses function calling from scratch." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='what is the weather in sf'),\n", + " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", + " FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'),\n", + " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "app.invoke(inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", + "metadata": {}, + "source": [ + "This may take a little bit - it's making a few calls behind the scenes.\n", + "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", + "\n", + "## Streaming\n", + "\n", + "LangGraph has support for several different types of streaming.\n", + "\n", + "### Streaming Node Output\n", + "\n", + "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "### Streaming LLM Tokens\n", + "\n", + "You can also access the LLM tokens as they are produced by each node. \n", + "In this case only the \"agent\" node produces LLM tokens.\n", + "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", + "content=''\n", + "content=''\n", + "content='I'\n", + "content=\"'m\"\n", + "content=' sorry'\n", + "content=','\n", + "content=' but'\n", + "content=' I'\n", + "content=' couldn'\n", + "content=\"'t\"\n", + "content=' find'\n", + "content=' the'\n", + "content=' current'\n", + "content=' weather'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content='.'\n", + "content=' However'\n", + "content=','\n", + "content=' you'\n", + "content=' can'\n", + "content=' check'\n", + "content=' the'\n", + "content=' historical'\n", + "content=' weather'\n", + "content=' data'\n", + "content=' for'\n", + "content=' January'\n", + "content=' '\n", + "content='202'\n", + "content='4'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' ['\n", + "content='here'\n", + "content=']('\n", + "content='https'\n", + "content='://'\n", + "content='we'\n", + "content='athers'\n", + "content='park'\n", + "content='.com'\n", + "content='/h'\n", + "content='/m'\n", + "content='/'\n", + "content='557'\n", + "content='/'\n", + "content='202'\n", + "content='4'\n", + "content='/'\n", + "content='1'\n", + "content='/H'\n", + "content='istorical'\n", + "content='-'\n", + "content='Weather'\n", + "content='-in'\n", + "content='-Jan'\n", + "content='uary'\n", + "content='-'\n", + "content='202'\n", + "content='4'\n", + "content='-in'\n", + "content='-S'\n", + "content='an'\n", + "content='-F'\n", + "content='r'\n", + "content='anc'\n", + "content='isco'\n", + "content='-Cal'\n", + "content='ifornia'\n", + "content='-'\n", + "content='United'\n", + "content='-'\n", + "content='States'\n", + "content=').'\n", + "content=''\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb new file mode 100644 index 000000000..4830192c8 --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb @@ -0,0 +1,565 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Dynamically Returning Directly\n", + "\n", + "In this example we will build a chat executor where the LLM can optionally decide to return the result of a tool call as the final answer. This is useful in cases where you have tools that can sometimes generate responses that are acceptable as final answers, and you want to use the LLM to determine when that is the case\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We overwrite the default schema of the input tool to have an additional parameter for returning directly." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "class SearchTool(BaseModel):\n", + " \"\"\"Look up things online, optionally returning directly\"\"\"\n", + " query: str = Field(description=\"query to look up online\")\n", + " return_direct: bool = Field(\n", + " description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\", \n", + " default = False\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n", + "tools = [search_tool]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage" + ] + }, + { + "cell_type": "markdown", + "id": "50bf356c-2dbd-4f66-8fa3-133e9c2e371e", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "We change the `should_continue` function to check whether return_direct was set to True" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we check if it's suppose to return direct\n", + " else:\n", + " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", + " if arguments.get(\"return_direct\", False):\n", + " return \"final\"\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "markdown", + "id": "8535a36c-3ced-401e-98b5-ec1d1b434bbc", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "We change the tool calling to get rid of the `return_direct` parameter (not used in the actual tool call)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_name = last_message.additional_kwargs[\"function_call\"][\"name\"]\n", + " arguments = json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"])\n", + " if tool_name == \"tavily_search_results_json\":\n", + " if \"return_direct\" in arguments:\n", + " del arguments[\"return_direct\"]\n", + " action = ToolInvocation(\n", + " tool=tool_name,\n", + " tool_input=arguments,\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We add a separate node for any tool call where `return_direct=True`. The reason this is needed is that after this node we want to end, while after other tool calls we want to go back to the LLM. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "workflow.add_node(\"final\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Final call\n", + " \"final\": \"final\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "workflow.add_edge('final', END)\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'final':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf? return this result directly by setting return_direct = True\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb new file mode 100644 index 000000000..f6457742a --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb @@ -0,0 +1,485 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Force Calling a Tool First\n", + "\n", + "In this example we will build a chat executor that always calls a certain tool first. In this example, we will create an agent with a search tool. However, at the start we will force the agent to call the search tool (and then let it do whatever it wants after). This is useful when you want to force agents to call particular tools, but still want flexibility of what happens after that.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "7c3e0ac2-0c89-4751-bc2c-f644654841d1", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "Here we create a node that returns an AIMessage with a tool call - we will use this at the start to force it call a tool" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38", + "metadata": {}, + "outputs": [], + "source": [ + "# This is the new first - the first call of the model we want to explicitly hard-code some action\n", + "from langchain_core.messages import AIMessage\n", + "import json\n", + "\n", + "def first_model(state):\n", + " human_input = state['messages'][-1].content\n", + " return {\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=\"\", \n", + " additional_kwargs={\n", + " \"function_call\": {\n", + " \"name\": \"tavily_search_results_json\", \n", + " \"arguments\": json.dumps({\"query\": human_input})\n", + " }\n", + " }\n", + " )\n", + " ]\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We will define a `first_agent` node which we will set as the entrypoint." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the new entrypoint\n", + "workflow.add_node(\"first_agent\", first_model)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"first_agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# After we call the first agent, we know we want to go to action\n", + "workflow.add_edge('first_agent', 'action')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'first_agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/high-level.ipynb b/examples/chat_agent_executor_with_function_calling/high-level.ipynb new file mode 100644 index 000000000..f4fc724fc --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/high-level.ipynb @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8bcd1a3d-7c50-4f58-be4e-1ed654aa33be", + "metadata": {}, + "source": [ + "# Chat Executor: with function calling\n", + "\n", + "This notebook walks through an example creating a chat executor that uses function calling.\n", + "This is useful for getting started quickly.\n", + "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder." + ] + }, + { + "cell_type": "markdown", + "id": "e130cf70-a30e-47d7-8fd5-464f1a92e374", + "metadata": {}, + "source": [ + "## Set up the chat model and tools\n", + "\n", + "Here we will define the chat model and tools that we want to use.\n", + "Importantly, this model MUST support OpenAI function calling." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "efb7e3c0-c63f-40f6-93ce-19681d650fc2", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langgraph.prebuilt import chat_agent_executor\n", + "from langchain_core.messages import HumanMessage" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a7025f33-3160-41cf-868b-17ebc916fb1d", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [TavilySearchResults(max_results=1)]\n", + "model = ChatOpenAI()" + ] + }, + { + "cell_type": "markdown", + "id": "43064805-2ac9-4b5a-850c-a68dd7282350", + "metadata": {}, + "source": [ + "## Create executor\n", + "\n", + "We can now use the high level interface to create the executor" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "32b4ae66-f667-4a8b-a602-503fd0effcd9", + "metadata": {}, + "outputs": [], + "source": [ + "app = chat_agent_executor.create_function_calling_executor(model, tools)" + ] + }, + { + "cell_type": "markdown", + "id": "d63dbfc7-a5c1-4a03-991c-f0789ba52c52", + "metadata": {}, + "source": [ + "We can now invoke this executor. The input to this must be a dictionary with a single `messsages` key that contains a list of messages." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0abc5655-d772-450c-832f-1fee1111a5f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "----\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "----\n", + "{'messages': [AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "----\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can check historical weather data for January 2024 [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "----\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb new file mode 100644 index 000000000..2c4de7109 --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that has a human in the loop. We will use the human to approve specific actions.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b547109f-f9e8-4e77-a7e7-ed2bae7a72ab", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "markdown", + "id": "ac402f66-4442-4a1f-9f9b-4a5d97532ceb", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "We modify the function that is calling the tool to first ask for user approval to continue. Note that this is a simple example and we could modify it to change the tool input, use some other channel besides input, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "73fd6432-42e8-472a-89ca-bb5ddbbcc35a", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " response = input(prompt=f\"[y/n] continue with: {action}?\")\n", + " if response == \"n\":\n", + " raise ValueError\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'}? y\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb new file mode 100644 index 000000000..0e547d597 --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb @@ -0,0 +1,463 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that better manages intermediate steps. The base chat executor will just put all messages into the model, but if the intermediate steps an agent is taking start to get long, you may want to modify that. In this example we will only include the ten most recent messages.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"" + ] + }, + { + "cell_type": "markdown", + "id": "a763aa63-701c-40fa-a9d3-9d992ebe7e4d", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "Here we don't pass all messages to the model but rather only pass the five most recent. Note that this is a pretty simplistic way to handle messages, and there may be other methods you may want to look into depending on your use case" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages'][-5:]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b3ca9564-63cc-4309-b158-5e8d3e907164", + "metadata": {}, + "outputs": [], + "source": [ + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb new file mode 100644 index 000000000..08e3a3ced --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb @@ -0,0 +1,450 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor\n", + "\n", + "In this example we will build a chat executor that responds in a specific format. We will do this by using OpenAI function calling. This is useful when you want to enforce that an agent's response is in a specific format. In this example, we will ask it respond as if a weatherman, so to return the temperature and then any other additional info.\n", + "\n", + "This examples builds off the base chat executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n", + "\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We also want to define a response schema for the language model and bind it to the model as a function as well" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_core.utils.function_calling import convert_pydantic_to_openai_function\n", + "\n", + "class Response(BaseModel):\n", + " \"\"\"Final response to the user\"\"\"\n", + " temperature: float = Field(description=\"the temperature\")\n", + " other_notes: str = Field(description=\"any other notes about the weather\")\n", + "\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "functions.append(convert_pydantic_to_openai_function(Response))\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We will change the `should_continue` function to check what function was called. If the function `Response` was called - that is the function that is NOT a tool, but rather the formatted response, so we should NOT continue in that case." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we need to check what type of function call it is\n", + " elif last_message.additional_kwargs[\"function_call\"][\"name\"] == \"Response\":\n", + " return \"end\"\n", + " # Otherwise we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state['messages']\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "for output in app.stream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 3539fef0cccc8b2663a4a117a587ef6c1ddba4c0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 15:35:27 -0800 Subject: [PATCH 16/25] remove arg --- langgraph/prebuilt/agent_executor.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py index 181f86468..70322a4d3 100644 --- a/langgraph/prebuilt/agent_executor.py +++ b/langgraph/prebuilt/agent_executor.py @@ -1,22 +1,16 @@ import operator -from typing import Annotated, TypedDict, Union, Sequence +from typing import Annotated, Sequence, TypedDict, Union from langchain_core.agents import AgentAction, AgentFinish from langchain_core.messages import BaseMessage - -from typing import Annotated, Optional, TypedDict - -from langchain_core.agents import AgentAction, AgentFinish - -from langgraph.checkpoint import BaseCheckpointSaver from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor - -def _get_agent_state(input_schema= None): +def _get_agent_state(input_schema=None): if input_schema is None: + class AgentState(TypedDict): # The input string input: str @@ -31,6 +25,7 @@ def _get_agent_state(input_schema= None): intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] else: + class AgentState(input_schema): # The outcome of a given call to the agent # Needs `None` as a valid type, since this is what this will start as @@ -43,12 +38,7 @@ def _get_agent_state(input_schema= None): return AgentState -def create_agent_executor( - agent_runnable, - tools, - input_schema=None, - checkpointer: Optional[BaseCheckpointSaver] = None, -): +def create_agent_executor(agent_runnable, tools, input_schema=None): if isinstance(tools, ToolExecutor): tool_executor = tools else: @@ -119,4 +109,4 @@ def create_agent_executor( # Finally, we compile it! # This compiles it into a LangChain Runnable, # meaning you can use it as you would any other runnable - return workflow.compile(checkpointer=checkpointer) + return workflow.compile() From b2b536581465068104559603d466565c00306506 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 15:45:17 -0800 Subject: [PATCH 17/25] Add async impls --- langgraph/prebuilt/agent_executor.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/langgraph/prebuilt/agent_executor.py b/langgraph/prebuilt/agent_executor.py index 70322a4d3..aabda460d 100644 --- a/langgraph/prebuilt/agent_executor.py +++ b/langgraph/prebuilt/agent_executor.py @@ -3,6 +3,7 @@ from typing import Annotated, Sequence, TypedDict, Union from langchain_core.agents import AgentAction, AgentFinish from langchain_core.messages import BaseMessage +from langchain_core.runnables import RunnableLambda from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor @@ -63,6 +64,10 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): agent_outcome = agent_runnable.invoke(data) return {"agent_outcome": agent_outcome} + async def arun_agent(data): + agent_outcome = await agent_runnable.ainvoke(data) + return {"agent_outcome": agent_outcome} + # Define the function to execute tools def execute_tools(data): # Get the most recent agent_outcome - this is the key added in the `agent` above @@ -70,12 +75,18 @@ def create_agent_executor(agent_runnable, tools, input_schema=None): output = tool_executor.invoke(agent_action) return {"intermediate_steps": [(agent_action, str(output))]} + async def aexecute_tools(data): + # Get the most recent agent_outcome - this is the key added in the `agent` above + agent_action = data["agent_outcome"] + output = await tool_executor.ainvoke(agent_action) + return {"intermediate_steps": [(agent_action, str(output))]} + # Define a new graph workflow = StateGraph(state) # Define the two nodes we will cycle between - workflow.add_node("agent", run_agent) - workflow.add_node("action", execute_tools) + workflow.add_node("agent", RunnableLambda(run_agent, arun_agent)) + workflow.add_node("action", RunnableLambda(execute_tools, aexecute_tools)) # Set the entrypoint as `agent` # This means that this node is the first one called From c5e66270af961055bb9faee37487f58f0a9d8a5e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 16:01:34 -0800 Subject: [PATCH 18/25] Add async for chat agent exec --- langgraph/prebuilt/chat_agent_executor.py | 47 +++++++++++++++++------ langgraph/prebuilt/tool_executor.py | 29 +++++++++----- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index eebb6998c..253473416 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -5,6 +5,7 @@ from typing import Annotated, Sequence, TypedDict from langchain.tools.render import format_tool_to_openai_function from langchain_core.agents import AgentAction from langchain_core.messages import BaseMessage, FunctionMessage +from langchain_core.runnables import RunnableLambda from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor @@ -17,11 +18,13 @@ def create_function_calling_executor(model, tools): else: tool_executor = ToolExecutor(tools) tool_classes = tools - model = model.bind_functions([format_tool_to_openai_function(t) for t in tool_classes]) + model = model.bind_functions( + [format_tool_to_openai_function(t) for t in tool_classes] + ) # Define the function that determines whether to continue or not def should_continue(state): - messages = state['messages'] + messages = state["messages"] last_message = messages[-1] # If there is no function call, then we finish if "function_call" not in last_message.additional_kwargs: @@ -32,23 +35,34 @@ def create_function_calling_executor(model, tools): # Define the function that calls the model def call_model(state): - messages = state['messages'] + messages = state["messages"] response = model.invoke(messages) # We return a list, because this will get added to the existing list return {"messages": [response]} + async def acall_model(state): + messages = state["messages"] + response = await model.ainvoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + # Define the function to execute tools - def call_tool(state): - messages = state['messages'] + def _get_action(state): + messages = state["messages"] # Based on the continue condition # we know the last message involves a function call last_message = messages[-1] # We construct an AgentAction from the function_call - action = AgentAction( + return AgentAction( tool=last_message.additional_kwargs["function_call"]["name"], - tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]), + tool_input=json.loads( + last_message.additional_kwargs["function_call"]["arguments"] + ), log="", ) + + def call_tool(state): + action = _get_action(state) # We call the tool_executor and get back a response response = tool_executor.invoke(action) # We use the response to create a FunctionMessage @@ -56,6 +70,15 @@ def create_function_calling_executor(model, tools): # We return a list, because this will get added to the existing list return {"messages": [function_message]} + async def acall_tool(state): + action = _get_action(state) + # We call the tool_executor and get back a response + response = await tool_executor.ainvoke(action) + # We use the response to create a FunctionMessage + function_message = FunctionMessage(content=str(response), name=action.tool) + # We return a list, because this will get added to the existing list + return {"messages": [function_message]} + # We create the AgentState that we will pass around # This simply involves a list of messages # We want steps to return messages to append to the list @@ -67,8 +90,8 @@ def create_function_calling_executor(model, tools): workflow = StateGraph(AgentState) # Define the two nodes we will cycle between - workflow.add_node("agent", call_model) - workflow.add_node("action", call_tool) + workflow.add_node("agent", RunnableLambda(call_model, acall_model)) + workflow.add_node("action", RunnableLambda(call_tool, acall_tool)) # Set the entrypoint as `agent` # This means that this node is the first one called @@ -91,13 +114,13 @@ def create_function_calling_executor(model, tools): # If `tools`, then we call the tool node. "continue": "action", # Otherwise we finish. - "end": END - } + "end": END, + }, ) # We now add a normal edge from `tools` to `agent`. # This means that after `tools` is called, `agent` node is called next. - workflow.add_edge('action', 'agent') + workflow.add_edge("action", "agent") # Finally, we compile it! # This compiles it into a LangChain Runnable, diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py index 82fe5e88a..b82e15653 100644 --- a/langgraph/prebuilt/tool_executor.py +++ b/langgraph/prebuilt/tool_executor.py @@ -1,9 +1,8 @@ -from typing import Any, Sequence +from typing import Any, Sequence, Union -from typing import Union +from langchain_core.load.serializable import Serializable from langchain_core.runnables import RunnableBinding, RunnableLambda from langchain_core.tools import BaseTool -from langchain_core.load.serializable import Serializable INVALID_TOOL_MSG_TEMPLATE = ( "{requested_tool_name} is not a valid tool, " @@ -13,6 +12,7 @@ INVALID_TOOL_MSG_TEMPLATE = ( class ToolInvocationInterface: """Interface for invoking a tool""" + tool: str tool_input: Union[str, dict] @@ -25,21 +25,32 @@ class ToolInvocation(Serializable): tool_input: Union[str, dict] """The input to pass in to the Tool.""" -class ToolExecutor(RunnableBinding): +class ToolExecutor(RunnableBinding): tools: Sequence[BaseTool] tool_map: dict invalid_tool_msg_template: str - def __init__(self, tools: Sequence[BaseTool], invalid_tool_msg_template: str = INVALID_TOOL_MSG_TEMPLATE, **kwargs: Any) -> None: + def __init__( + self, + tools: Sequence[BaseTool], + invalid_tool_msg_template: str = INVALID_TOOL_MSG_TEMPLATE, + **kwargs: Any, + ) -> None: bound = RunnableLambda(self._execute, afunc=self._aexecute) - super().__init__(bound=bound, tools=tools, tool_map ={t.name: t for t in tools}, invalid_tool_msg_template=invalid_tool_msg_template, **kwargs) + super().__init__( + bound=bound, + tools=tools, + tool_map={t.name: t for t in tools}, + invalid_tool_msg_template=invalid_tool_msg_template, + **kwargs, + ) def _execute(self, tool_invocation: ToolInvocationInterface) -> Any: if tool_invocation.tool not in self.tool_map: return self.invalid_tool_msg_template.format( requested_tool_name=tool_invocation.tool, - available_tool_names_str=", ".join([t.name for t in self.tools]) + available_tool_names_str=", ".join([t.name for t in self.tools]), ) else: tool = self.tool_map[tool_invocation.tool] @@ -50,9 +61,9 @@ class ToolExecutor(RunnableBinding): if tool_invocation.tool not in self.tool_map: return self.invalid_tool_msg_template.format( requested_tool_name=tool_invocation.tool, - available_tool_names_str=", ".join([t.name for t in self.tools]) + available_tool_names_str=", ".join([t.name for t in self.tools]), ) else: tool = self.tool_map[tool_invocation.tool] output = await tool.ainvoke(tool_invocation.tool_input) - return output \ No newline at end of file + return output From 7fb7c846e06e608172f590f8128b83e9d7aad49e Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 16:10:56 -0800 Subject: [PATCH 19/25] cr --- README.md | 31 +++- .../force-calling-a-tool-first.ipynb | 142 ++++++++++++----- .../agent_executor/human-in-the-loop.ipynb | 147 ++++++++++++----- .../agent_executor/managing-agent-steps.ipynb | 149 +++++++++++++----- 4 files changed, 357 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 823ffb483..c19cfab94 100644 --- a/README.md +++ b/README.md @@ -419,10 +419,39 @@ Langchain Expression Language allows you to easily define chains (DAGs) but does ## Examples -### ChatExecutor: with function calling +### ChatAgentExecutor: with function calling + +This agent executor takes a list of messages as input and outputs a list of messages. +All agent state is represented as a list of messages. +This specifically uses OpenAI function calling. +This is recommended agent executor for newer chat based models that support function calling. + +- [Getting Started Notebook](examples/chat_agent_executor_with_function_calling/base.ipynb): Walks through creating this type of executor from scratch +- [High Level Entrypoint](examples/chat_agent_executor_with_function_calling/high-level.ipynb): Walks through how to use the high level entrypoint for the chat agent executor. + +**Modifications** + +We also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the [getting started notebook](examples/chat_agent_executor_with_function_calling/base.ipynb) so it is recommended you start with that first. +- [Human-in-the-loop](examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb): How to add a human-in-the-loop component +- [Force calling a tool first](examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb): How to always call a specific tool first +- [Respond in a specific format](examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb): How to force the agent to respond in a specific format +- [Dynamically returning tool output directly](examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb): How to dynamically let the agent choose whether to return the result of a tool directly to the user +- [Managing agent steps](examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb): How to more explicitly manage intermediate steps that an agent takes ### AgentExecutor +This agent executor uses existing LangChain agents. + +- [Getting Started Notebook](examples/agent_executor/base.ipynb): Walks through creating this type of executor from scratch +- [High Level Entrypoint](examples/agent_executor/high-level.ipynb): Walks through how to use the high level entrypoint for the chat agent executor. + +**Modifications** + +We also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the [getting started notebook](examples/agent_executor/base.ipynb) so it is recommended you start with that first. +- [Human-in-the-loop](examples/agent_executor/human-in-the-loop.ipynb): How to add a human-in-the-loop component +- [Force calling a tool first](examples/agent_executor/force-calling-a-tool-first.ipynb): How to always call a specific tool first +- [Managing agent steps](examples/agent_executor/managing-agent-steps.ipynb): How to more explicitly manage intermediate steps that an agent takes + ## Documentation diff --git a/examples/agent_executor/force-calling-a-tool-first.ipynb b/examples/agent_executor/force-calling-a-tool-first.ipynb index 70712a949..b36121bb5 100644 --- a/examples/agent_executor/force-calling-a-tool-first.ipynb +++ b/examples/agent_executor/force-calling-a-tool-first.ipynb @@ -1,5 +1,29 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "f725852e-71ef-4615-8cac-011a516fbe72", + "metadata": {}, + "source": [ + "# Agent Executor From Scratch\n", + "\n", + "In this notebook we will create an agent with a search tool. However, at the start we will force the agent to call the search tool (and then let it do whatever it wants after). This is useful when you want to force agents to call particular tools, but still want flexibility of what happens after that.\n", + "\n", + "This examples builds off the base agent executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", + "metadata": {}, + "source": [ + "## Create the LangChain agent\n", + "\n", + "First, we will create the LangChain agent. For more information on LangChain agents, see [this documentation](https://python.langchain.com/docs/modules/agents/)" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -24,6 +48,21 @@ "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" ] }, + { + "cell_type": "markdown", + "id": "972e58b3-fe3c-449d-b3c4-8fa2217afd07", + "metadata": {}, + "source": [ + "## Define the graph state\n", + "\n", + "We now define the graph state. The state for the traditional LangChain agent has a few attributes:\n", + "\n", + "1. `input`: This is the input string representing the main ask from the user, passed in as input.\n", + "2. `chat_history`: This is any previous conversation messages, also passed in as input.\n", + "3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent.\n", + "4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools.\n" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -51,6 +90,33 @@ " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" ] }, + { + "cell_type": "markdown", + "id": "cd27b281-cc9a-49c9-be78-8b98a7d905c4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take." + ] + }, { "cell_type": "code", "execution_count": 3, @@ -90,10 +156,20 @@ " return \"continue\"" ] }, + { + "cell_type": "markdown", + "id": "02437e83-5485-4827-87e6-7ad1d02cf9be", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", + "\n", + "Here we create a node that returns an AgentAction that just calls the Tavily search with the input" + ] + }, { "cell_type": "code", "execution_count": 4, - "id": "4883e47a-0a15-429c-bf31-1e8afe982a77", + "id": "2ed8463e-73e5-417d-9fab-be6bcee87835", "metadata": {}, "outputs": [ { @@ -114,7 +190,7 @@ { "cell_type": "code", "execution_count": 5, - "id": "fb16db55-ff1a-4e16-94c1-dcb8b2a8f0ba", + "id": "df25d899-2338-4f31-a8bf-0582a2eec325", "metadata": {}, "outputs": [], "source": [ @@ -132,6 +208,20 @@ " return {\"agent_outcome\": action}" ] }, + { + "cell_type": "markdown", + "id": "c0b211f4-0c5c-4792-b18d-cd70907c71e7", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We now add a new `first_agent` node which we set as the entrypoint." + ] + }, { "cell_type": "code", "execution_count": 7, @@ -178,17 +268,18 @@ "# This means that after `tools` is called, `agent` node is called next.\n", "workflow.add_edge('action', 'agent')\n", "\n", + "# After the first agent, we want to take an action\n", "workflow.add_edge('first_agent', 'action')\n", "\n", "# Finally, we compile it!\n", "# This compiles it into a LangChain Runnable,\n", "# meaning you can use it as you would any other runnable\n", - "chain = workflow.compile()" + "app = workflow.compile()" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "214ae46e-c297-465d-86db-2b0312ed3530", "metadata": {}, "outputs": [ @@ -196,43 +287,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Output from node 'first_agent':\n", - "---\n", "{'agent_outcome': AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[])}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\")]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"}, log=\"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\")}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"}, log=\"I'm sorry, but I couldn't find the current weather in San Francisco. If you'd like, I can try another source to get the current weather for you.\"), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\")]}\n", - "\n", - "---\n", - "\n" + "----\n", + "{'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\")]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': 'The weather in San Francisco in January is typically tolerable, with temperatures ranging from 45°F to 52°F. If you need more specific and up-to-date information about the current weather in San Francisco, I can look it up for you.'}, log='The weather in San Francisco in January is typically tolerable, with temperatures ranging from 45°F to 52°F. If you need more specific and up-to-date information about the current weather in San Francisco, I can look it up for you.')}\n", + "----\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': 'The weather in San Francisco in January is typically tolerable, with temperatures ranging from 45°F to 52°F. If you need more specific and up-to-date information about the current weather in San Francisco, I can look it up for you.'}, log='The weather in San Francisco in January is typically tolerable, with temperatures ranging from 45°F to 52°F. If you need more specific and up-to-date information about the current weather in San Francisco, I can look it up for you.'), 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input='what is the weather in sf', log='', message_log=[]), \"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerableWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 15-01-2023 50°F to 52°F. 16-01-2023 45°F to 52°F. 17-01-2023 45°F to ...'}]\")]}\n", + "----\n" ] } ], "source": [ - "for output in chain.stream(\n", - " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" + "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" ] }, { diff --git a/examples/agent_executor/human-in-the-loop.ipynb b/examples/agent_executor/human-in-the-loop.ipynb index 3aa623330..c127aee6a 100644 --- a/examples/agent_executor/human-in-the-loop.ipynb +++ b/examples/agent_executor/human-in-the-loop.ipynb @@ -1,5 +1,29 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "f725852e-71ef-4615-8cac-011a516fbe72", + "metadata": {}, + "source": [ + "# Human in the Loop\n", + "\n", + "In this notebook we will go over how to add a human-in-the-loop workflow to the base agent executor. We will use the human to approve\n", + "\n", + "This examples builds off the base agent executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", + "metadata": {}, + "source": [ + "## Create the LangChain agent\n", + "\n", + "First, we will create the LangChain agent. For more information on LangChain agents, see [this documentation](https://python.langchain.com/docs/modules/agents/)" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -24,6 +48,21 @@ "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" ] }, + { + "cell_type": "markdown", + "id": "972e58b3-fe3c-449d-b3c4-8fa2217afd07", + "metadata": {}, + "source": [ + "## Define the graph state\n", + "\n", + "We now define the graph state. The state for the traditional LangChain agent has a few attributes:\n", + "\n", + "1. `input`: This is the input string representing the main ask from the user, passed in as input.\n", + "2. `chat_history`: This is any previous conversation messages, also passed in as input.\n", + "3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent.\n", + "4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools.\n" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -51,10 +90,37 @@ " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" ] }, + { + "cell_type": "markdown", + "id": "cd27b281-cc9a-49c9-be78-8b98a7d905c4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take." + ] + }, { "cell_type": "code", - "execution_count": 6, - "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "execution_count": 3, + "id": "2b757f84-1175-445e-8f8c-e5aeb765a03d", "metadata": {}, "outputs": [], "source": [ @@ -68,8 +134,26 @@ "# Define the agent\n", "def run_agent(data):\n", " agent_outcome = agent_runnable.invoke(data)\n", - " return {\"agent_outcome\": agent_outcome}\n", + " return {\"agent_outcome\": agent_outcome}" + ] + }, + { + "cell_type": "markdown", + "id": "35ace508-d5fe-4139-a0f8-887e38047401", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "We modify the function that is calling the tool to first ask for user approval to continue. Note that this is a simple example and we could modify it to change the tool input, use some other channel besides input, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2fecf5e0-9604-4992-9c82-b9627466cd32", + "metadata": {}, + "outputs": [], + "source": [ "# Define the function to execute tools\n", "def execute_tools(data):\n", " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", @@ -93,9 +177,19 @@ " return \"continue\"" ] }, + { + "cell_type": "markdown", + "id": "c0b211f4-0c5c-4792-b18d-cd70907c71e7", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], @@ -141,12 +235,12 @@ "# Finally, we compile it!\n", "# This compiles it into a LangChain Runnable,\n", "# meaning you can use it as you would any other runnable\n", - "chain = workflow.compile()" + "app = workflow.compile()" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "214ae46e-c297-465d-86db-2b0312ed3530", "metadata": {}, "outputs": [ @@ -154,12 +248,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Output from node 'agent':\n", - "---\n", "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", - "\n", - "---\n", - "\n" + "----\n" ] }, { @@ -173,37 +263,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "Output from node 'action':\n", - "---\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"}, log=\"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\")}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"}, log=\"It seems that I couldn't retrieve the current weather for San Francisco. However, you can easily check the current weather in San Francisco by using a weather website or app.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "\n", - "---\n", - "\n" + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"It seems that I didn't find the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date information.\"}, log=\"It seems that I didn't find the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date information.\")}\n", + "----\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"It seems that I didn't find the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date information.\"}, log=\"It seems that I didn't find the current weather information for San Francisco. I recommend checking a reliable weather website or using a weather app to get the most up-to-date information.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "----\n" ] } ], "source": [ - "for output in chain.stream(\n", - " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" + "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" ] }, { diff --git a/examples/agent_executor/managing-agent-steps.ipynb b/examples/agent_executor/managing-agent-steps.ipynb index 7d7ffa498..b53b7e91a 100644 --- a/examples/agent_executor/managing-agent-steps.ipynb +++ b/examples/agent_executor/managing-agent-steps.ipynb @@ -1,5 +1,29 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "f725852e-71ef-4615-8cac-011a516fbe72", + "metadata": {}, + "source": [ + "# Managing Agent Steps\n", + "\n", + "In this notebook we will go over how to build a basic agent executor where we custom handle how to manage the intermediate steps. Normally, all previous steps are passed to the agent at future iterations, but in long-running cases that could lead to an overly large amount of steps that you may want to trim\n", + "\n", + "This examples builds off the base agent executor. It is highly recommended you learn about that executor before going through this notebook. You can find documentation for that example [here](./base.ipynb).\n", + "\n", + "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." + ] + }, + { + "cell_type": "markdown", + "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", + "metadata": {}, + "source": [ + "## Create the LangChain agent\n", + "\n", + "First, we will create the LangChain agent. For more information on LangChain agents, see [this documentation](https://python.langchain.com/docs/modules/agents/)" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -24,6 +48,21 @@ "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" ] }, + { + "cell_type": "markdown", + "id": "972e58b3-fe3c-449d-b3c4-8fa2217afd07", + "metadata": {}, + "source": [ + "## Define the graph state\n", + "\n", + "We now define the graph state. The state for the traditional LangChain agent has a few attributes:\n", + "\n", + "1. `input`: This is the input string representing the main ask from the user, passed in as input.\n", + "2. `chat_history`: This is any previous conversation messages, also passed in as input.\n", + "3. `intermediate_steps`: This is list of actions and corresponding observations that the agent takes over time. This is updated each iteration of the agent.\n", + "4. `agent_outcome`: This is the response from the agent, either an AgentAction or AgentFinish. The AgentExecutor should finish when this is an AgentFinish, otherwise it should call the requested tools.\n" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -51,10 +90,37 @@ " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]\n" ] }, + { + "cell_type": "markdown", + "id": "cd27b281-cc9a-49c9-be78-8b98a7d905c4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take." + ] + }, { "cell_type": "code", - "execution_count": 3, - "id": "d61a970d-edf4-4eef-9678-28bab7c72331", + "execution_count": 6, + "id": "77e3c059-e31f-4c8f-81bf-edb58688e12b", "metadata": {}, "outputs": [], "source": [ @@ -63,8 +129,26 @@ "\n", "# This a helper class we have that is useful for running tools\n", "# It takes in an agent action and calls that tool and returns the result\n", - "tool_executor = ToolExecutor(tools)\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "4c804a34-d384-4ca9-b9fc-dc86d678ab39", + "metadata": {}, + "source": [ + "**MODIFICATION**\n", "\n", + "Here, we modify the agent to only look at the last five intermediate steps. This is a relatively simple example of shortening the intermediate step history." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a9f66a3e-aba1-4893-95b1-a433c7091d5e", + "metadata": {}, + "outputs": [], + "source": [ "# Define the agent\n", "def run_agent(data):\n", " inputs = data.copy()\n", @@ -93,9 +177,19 @@ " return \"continue\"" ] }, + { + "cell_type": "markdown", + "id": "c0b211f4-0c5c-4792-b18d-cd70907c71e7", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], @@ -141,12 +235,12 @@ "# Finally, we compile it!\n", "# This compiles it into a LangChain Runnable,\n", "# meaning you can use it as you would any other runnable\n", - "chain = workflow.compile()" + "app = workflow.compile()" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 9, "id": "214ae46e-c297-465d-86db-2b0312ed3530", "metadata": {}, "outputs": [ @@ -154,43 +248,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Output from node 'agent':\n", - "---\n", "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"}, log=\"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\")}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"}, log=\"I'm sorry, I couldn't find the current weather for San Francisco. If you'd like, I can search for the current weather using a different method.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", - "\n", - "---\n", - "\n" + "----\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San Francisco you can find all information about the weather in San Francisco in January:Data: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\")]}\n", + "----\n", + "{'agent_outcome': AgentFinish(return_values={'output': \"The weather in San Francisco varies by month. In January, the average minimum temperature is 9.6°C (49.2°F), and the average maximum temperature is 14°C (57.3°F). The city experiences an average of 113mm of precipitation and has an average of 6 rainy days in January. If you'd like to know more about the weather in other months, feel free to ask!\"}, log=\"The weather in San Francisco varies by month. In January, the average minimum temperature is 9.6°C (49.2°F), and the average maximum temperature is 14°C (57.3°F). The city experiences an average of 113mm of precipitation and has an average of 6 rainy days in January. If you'd like to know more about the weather in other months, feel free to ask!\")}\n", + "----\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"The weather in San Francisco varies by month. In January, the average minimum temperature is 9.6°C (49.2°F), and the average maximum temperature is 14°C (57.3°F). The city experiences an average of 113mm of precipitation and has an average of 6 rainy days in January. If you'd like to know more about the weather in other months, feel free to ask!\"}, log=\"The weather in San Francisco varies by month. In January, the average minimum temperature is 9.6°C (49.2°F), and the average maximum temperature is 14°C (57.3°F). The city experiences an average of 113mm of precipitation and has an average of 6 rainy days in January. If you'd like to know more about the weather in other months, feel free to ask!\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/january-1/', 'content': 'San Francisco Weather in January San Francisco weather in January San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3) 113 San Francisco weather in January // weather averages Airport close to San Francisco you can find all information about the weather in San Francisco in January:Data: 1991 - 2021 Min. Temperature °C (°F), Max. Temperature °C (°F), Precipitation / Rainfall mm (in), Humidity, Rainy days. Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months San Francisco in February San Francisco in March San Francisco in April San Francisco in May San Francisco in June San Francisco in July'}]\")]}\n", + "----\n" ] } ], "source": [ - "for output in chain.stream(\n", - " {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" + "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", + "for s in app.stream(inputs):\n", + " print(list(s.values())[0])\n", + " print(\"----\")" ] }, { From 412d49ccd3557428b7a764874daf6e74bf7f1d50 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 15 Jan 2024 16:16:15 -0800 Subject: [PATCH 20/25] Add async notebook --- .../async.ipynb | 616 ++++++++++++++++++ 1 file changed, 616 insertions(+) create mode 100644 examples/chat_agent_executor_with_function_calling/async.ipynb diff --git a/examples/chat_agent_executor_with_function_calling/async.ipynb b/examples/chat_agent_executor_with_function_calling/async.ipynb new file mode 100644 index 000000000..e9c21021e --- /dev/null +++ b/examples/chat_agent_executor_with_function_calling/async.ipynb @@ -0,0 +1,616 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Chat Executor: Async\n", + "\n", + "In this example we will build a chat executor with native async implementations of the core logic. This enables taking advantage of Chat Models which have async clients, removing the need for calling the model in a separate thread." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" + ] + } + ], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "OpenAI API Key: ········\n", + "Tavily API Key: ········\n" + ] + } + ], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We define each node as an async function." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "async def call_model(state):\n", + " messages = state['messages']\n", + " response = await model.ainvoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "async def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = await tool_executor.ainvoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='what is the weather in sf'),\n", + " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", + " FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'),\n", + " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "await app.ainvoke(inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", + "metadata": {}, + "source": [ + "This may take a little bit - it's making a few calls behind the scenes.\n", + "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", + "\n", + "## Streaming\n", + "\n", + "LangGraph has support for several different types of streaming.\n", + "\n", + "### Streaming Node Output\n", + "\n", + "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "### Streaming LLM Tokens\n", + "\n", + "You can also access the LLM tokens as they are produced by each node. \n", + "In this case only the \"agent\" node produces LLM tokens.\n", + "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", + "content=''\n", + "content=''\n", + "content='The'\n", + "content=' current'\n", + "content=' weather'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' is'\n", + "content=' '\n", + "content='52'\n", + "content='.'\n", + "content='0'\n", + "content=' °'\n", + "content='F'\n", + "content=' with'\n", + "content=' a'\n", + "content=' light'\n", + "content=' breeze'\n", + "content=' of'\n", + "content=' '\n", + "content='6'\n", + "content='.'\n", + "content='9'\n", + "content=' mph'\n", + "content=' coming'\n", + "content=' from'\n", + "content=' the'\n", + "content=' east'\n", + "content='.'\n", + "content=' The'\n", + "content=' sky'\n", + "content=' is'\n", + "content=' mostly'\n", + "content=' cloudy'\n", + "content=' with'\n", + "content=' cloud'\n", + "content=' cover'\n", + "content=' at'\n", + "content=' '\n", + "content='18'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content=','\n", + "content=' mostly'\n", + "content=' clear'\n", + "content=' at'\n", + "content=' '\n", + "content='4'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content=','\n", + "content=' and'\n", + "content=' partly'\n", + "content=' cloudy'\n", + "content=' at'\n", + "content=' '\n", + "content='15'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content='.'\n", + "content=''\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 2dfe20d9c6589e8897ce1da2e78986f922c95cb4 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 16:35:46 -0800 Subject: [PATCH 21/25] cr --- examples/agent_executor/base.ipynb | 60 ++++++++++++++++++ .../force-calling-a-tool-first.ipynb | 61 +++++++++++++++++++ examples/agent_executor/high-level.ipynb | 61 +++++++++++++++++++ .../agent_executor/human-in-the-loop.ipynb | 61 +++++++++++++++++++ .../agent_executor/managing-agent-steps.ipynb | 61 +++++++++++++++++++ 5 files changed, 304 insertions(+) diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb index 83b17b20e..2e4ba7a83 100644 --- a/examples/agent_executor/base.ipynb +++ b/examples/agent_executor/base.ipynb @@ -10,6 +10,66 @@ "In this notebook we will go over how to build a basic agent executor from scratch." ] }, + { + "cell_type": "markdown", + "id": "c0860511-03c2-49bb-937b-035f84142b7e", + "metadata": {}, + "source": [ + "## Setup¶\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdd4ce41-4152-423b-b3f7-be3b4d568cf4", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "5f4179ce-48fa-4aaf-a5a1-027b5229be1a", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6398c4c1-da78-4595-8a5a-051ed2d1de72", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "37943b1c-2b0a-4c09-bfbd-5dc24b839e3c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcbf79ad-4de5-43b0-a3a1-25b33711e46c", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, { "cell_type": "markdown", "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", diff --git a/examples/agent_executor/force-calling-a-tool-first.ipynb b/examples/agent_executor/force-calling-a-tool-first.ipynb index b36121bb5..49dccc319 100644 --- a/examples/agent_executor/force-calling-a-tool-first.ipynb +++ b/examples/agent_executor/force-calling-a-tool-first.ipynb @@ -14,6 +14,67 @@ "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." ] }, + { + "cell_type": "markdown", + "id": "6821de30-6eeb-4f70-b0a7-e05d3187b14b", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "694cfc4c-22a7-495d-930d-56b21d850ff9", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "dc039752-6d34-4ad4-aa31-9a10f4d4d597", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30c06a84-291a-4f58-9d31-53d3b56a3def", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "5e7f4767-54fb-4b6e-bd9a-3d433df924fb", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8fb285a-7e6e-46fc-a273-43ab1a676189", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, { "cell_type": "markdown", "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", diff --git a/examples/agent_executor/high-level.ipynb b/examples/agent_executor/high-level.ipynb index f2d853d5a..1922e5ab7 100644 --- a/examples/agent_executor/high-level.ipynb +++ b/examples/agent_executor/high-level.ipynb @@ -12,6 +12,67 @@ "However, it is highly likely you will want to customize the logic - for information on that, check out the other examples in this folder." ] }, + { + "cell_type": "markdown", + "id": "e6dd032b-bfe9-458c-a8ef-a14c78e0ad3f", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1759bc06-8af3-4b73-abbf-0be3fa4c31fb", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "fa08bd1a-efaa-46f5-adf8-47a84f738381", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb8e51dc-028b-4ea5-9847-f22fcbed6dac", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "9242c0d7-b1da-41a0-9a3e-ed3afab3528e", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5db4438c-7802-4050-9dd9-14a6cac21a91", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, { "cell_type": "markdown", "id": "6ae180d9-abd3-4a44-8fb1-a2c89434fbeb", diff --git a/examples/agent_executor/human-in-the-loop.ipynb b/examples/agent_executor/human-in-the-loop.ipynb index c127aee6a..d52ad06e2 100644 --- a/examples/agent_executor/human-in-the-loop.ipynb +++ b/examples/agent_executor/human-in-the-loop.ipynb @@ -14,6 +14,67 @@ "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." ] }, + { + "cell_type": "markdown", + "id": "f7714f98-eb0e-43dd-8ae7-4a32ef2e72de", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fa9e224-2f00-49e2-bca3-e9cb8d9f3d41", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "2dd8be50-2f92-478b-a918-6d9e4ad66dd6", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d180f0d0-385f-4ce3-994c-11e1d64595b5", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "31d59506-f33f-42ad-b072-9a344c4af2e6", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72ad0539-ecd8-4eb1-b2c1-2242e5fc556f", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, { "cell_type": "markdown", "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", diff --git a/examples/agent_executor/managing-agent-steps.ipynb b/examples/agent_executor/managing-agent-steps.ipynb index b53b7e91a..d509d8f8d 100644 --- a/examples/agent_executor/managing-agent-steps.ipynb +++ b/examples/agent_executor/managing-agent-steps.ipynb @@ -14,6 +14,67 @@ "Any modifications of that example are called below with **MODIFICATION**, so if you are looking for the differences you can just search for that." ] }, + { + "cell_type": "markdown", + "id": "bd763d4e-fd5e-4ce4-aa3a-54ab895d10a6", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa752131-27e3-4bd8-9f21-d6749a7e74f4", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "dbbfe916-5c23-4bf4-a5fa-5048e676dae3", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5732e68f-4ae2-4db9-bf9c-454b4cc9ec01", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "4141f30e-4e5a-4b98-9fd8-b95e859d203a", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "652d4600-8f95-493f-b9b9-d4095aed9218", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, { "cell_type": "markdown", "id": "5dace4a9-7c9e-4da2-bf7b-e58d0d05581e", From f124705d6e3d00cfa7fc1c5ca647b4eb8c3d0812 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 17:40:09 -0800 Subject: [PATCH 22/25] cr --- examples/agent_executor/base.ipynb | 14 +- examples/async.ipynb | 618 ++++++++++++++++++ .../async.ipynb | 115 ++-- .../base.ipynb | 81 +-- .../dynamically-returning-directly.ipynb | 36 +- .../force-calling-a-tool-first.ipynb | 6 +- .../human-in-the-loop.ipynb | 46 +- .../managing-agent-steps.ipynb | 10 +- .../respond-in-format.ipynb | 16 +- 9 files changed, 761 insertions(+), 181 deletions(-) create mode 100644 examples/async.ipynb diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb index 2e4ba7a83..1e2077740 100644 --- a/examples/agent_executor/base.ipynb +++ b/examples/agent_executor/base.ipynb @@ -82,7 +82,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 10, "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", "metadata": {}, "outputs": [], @@ -121,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 11, "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", "metadata": {}, "outputs": [], @@ -224,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], @@ -275,7 +275,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 13, "id": "214ae46e-c297-465d-86db-2b0312ed3530", "metadata": {}, "outputs": [ @@ -285,11 +285,11 @@ "text": [ "{'agent_outcome': 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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})])}\n", "----\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\")]}\n", "----\n", - "{'agent_outcome': AgentFinish(return_values={'output': \"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"}, log=\"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\")}\n", + "{'agent_outcome': AgentFinish(return_values={'output': 'I found some information about the weather in San Francisco in January 2024, but it seems that the search results are not specific to the current weather. Would you like me to try a different search method to get the current weather in San Francisco?'}, log='I found some information about the weather in San Francisco in January 2024, but it seems that the search results are not specific to the current weather. Would you like me to try a different search method to get the current weather in San Francisco?')}\n", "----\n", - "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': \"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"}, log=\"I'm unable to provide the current weather in San Francisco at the moment. If you'd like, I can look up a reliable source to find the current weather for you.\"), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\")]}\n", + "{'input': 'what is the weather in sf', 'chat_history': [], 'agent_outcome': AgentFinish(return_values={'output': 'I found some information about the weather in San Francisco in January 2024, but it seems that the search results are not specific to the current weather. Would you like me to try a different search method to get the current weather in San Francisco?'}, log='I found some information about the weather in San Francisco in January 2024, but it seems that the search results are not specific to the current weather. Would you like me to try a different search method to get the current weather in San Francisco?'), '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': {'arguments': '{\"query\":\"weather in San Francisco\"}', 'name': 'tavily_search_results_json'}})]), \"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\")]}\n", "----\n" ] } diff --git a/examples/async.ipynb b/examples/async.ipynb new file mode 100644 index 000000000..2fae3e3f9 --- /dev/null +++ b/examples/async.ipynb @@ -0,0 +1,618 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# Async\n", + "\n", + "In this example we will build a chat executor with native async implementations of the core logic. This enables taking advantage of Chat Models which have async clients, removing the need for calling the model in a separate thread.\n", + "\n", + "For more information" + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" + ] + } + ], + "source": [ + "!pip install --quiet -U langchain langchain_openai tavily-python" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "OpenAI API Key: ········\n", + "Tavily API Key: ········\n" + ] + } + ], + "source": [ + "import os\n", + "import getpass\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", + "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" + ] + }, + { + "cell_type": "markdown", + "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "We will first define the tools we want to use.\n", + "For this simple example, we will use a built-in search tool via Tavily.\n", + "However, it is really easy to create your own tools - see documentation [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools) on how to do that.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]" + ] + }, + { + "cell_type": "markdown", + "id": "01885785-b71a-44d1-b1d6-7b5b14d53b58", + "metadata": {}, + "source": [ + "We can now wrap these tools in a simple ToolExecutor.\n", + "This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", + "A ToolInvocation is any class with `tool` and `tool_input` attribute.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "tool_executor = ToolExecutor(tools)" + ] + }, + { + "cell_type": "markdown", + "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we need to load the chat model we want to use.\n", + "Importantly, this should satisfy two criteria:\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", + "\n", + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "# We will set streaming=True so that we can stream tokens\n", + "# See the streaming section for more information on this.\n", + "model = ChatOpenAI(temperature=0, streaming=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a77995c0-bae2-4cee-a036-8688a90f05b9", + "metadata": {}, + "source": [ + "\n", + "After we've done this, we should make sure the model knows that it has these tools available to call.\n", + "We can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.tools.render import format_tool_to_openai_function\n", + "\n", + "functions = [format_tool_to_openai_function(t) for t in tools]\n", + "model = model.bind_functions(functions)" + ] + }, + { + "cell_type": "markdown", + "id": "8e8b9211-93d0-4ad5-aa7a-9c09099c53ff", + "metadata": {}, + "source": [ + "## Define the agent state\n", + "\n", + "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "This graph is parameterized by a state object that it passes around to each node.\n", + "Each node then returns operations to update that state.\n", + "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", + "Whether to set or add is denoted by annotating the state object you construct the graph with.\n", + "\n", + "For this example, the state we will track will just be a list of messages.\n", + "We want each node to just add messages to that list.\n", + "Therefore, we will use a `TypedDict` with one key (`messages`) and annotate it so that the `messages` attribute is always added to.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ea793afa-2eab-4901-910d-6eed90cd6564", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Annotated, Sequence\n", + "import operator\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] + }, + { + "cell_type": "markdown", + "id": "e03c5094-9297-4d19-a04e-3eedc75cefb4", + "metadata": {}, + "source": [ + "## Define the nodes\n", + "\n", + "We now need to define a few different nodes in our graph.\n", + "In `langgraph`, a node can be either a function or a [runnable](https://python.langchain.com/docs/expression_language/).\n", + "There are two main nodes we need for this:\n", + "\n", + "1. The agent: responsible for deciding what (if any) actions to take.\n", + "2. A function to invoke tools: if the agent decides to take an action, this node will then execute that action.\n", + "\n", + "We will also need to define some edges.\n", + "Some of these edges may be conditional.\n", + "The reason they are conditional is that based on the output of a node, one of several paths may be taken.\n", + "The path that is taken is not known until that node is run (the LLM decides).\n", + "\n", + "1. Conditional Edge: after the agent is called, we should either:\n", + " a. If the agent said to take an action, then the function to invoke tools should be called\n", + " b. If the agent said that it was finished, then it should finish\n", + "2. Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next\n", + "\n", + "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", + "\n", + "**MODIFICATION**\n", + "\n", + "We define each node as an async function." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import ToolInvocation\n", + "import json\n", + "from langchain_core.messages import FunctionMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state['messages']\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if \"function_call\" not in last_message.additional_kwargs:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "async def call_model(state):\n", + " messages = state['messages']\n", + " response = await model.ainvoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function to execute tools\n", + "async def call_tool(state):\n", + " messages = state['messages']\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " action = ToolInvocation(\n", + " tool=last_message.additional_kwargs[\"function_call\"][\"name\"],\n", + " tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]),\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = await tool_executor.ainvoke(action)\n", + " # We use the response to create a FunctionMessage\n", + " function_message = FunctionMessage(content=str(response), name=action.tool)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [function_message]}" + ] + }, + { + "cell_type": "markdown", + "id": "ffd6e892-946c-4899-8cc0-7c9291c1f73b", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together and define the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END\n", + " }\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge('action', 'agent')\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "547c3931-3dae-4281-ad4e-4b51305594d4", + "metadata": {}, + "source": [ + "## Use it!\n", + "\n", + "We can now use it!\n", + "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='what is the weather in sf'),\n", + " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", + " FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'),\n", + " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "await app.ainvoke(inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", + "metadata": {}, + "source": [ + "This may take a little bit - it's making a few calls behind the scenes.\n", + "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", + "\n", + "## Streaming\n", + "\n", + "LangGraph has support for several different types of streaming.\n", + "\n", + "### Streaming Node Output\n", + "\n", + "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", + "\n", + "---\n", + "\n", + "Output from node 'action':\n", + "---\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", + "\n", + "---\n", + "\n", + "Output from node 'agent':\n", + "---\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "\n", + "---\n", + "\n", + "Output from node '__end__':\n", + "---\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream(inputs):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "### Streaming LLM Tokens\n", + "\n", + "You can also access the LLM tokens as they are produced by each node. \n", + "In this case only the \"agent\" node produces LLM tokens.\n", + "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", + "content=''\n", + "content=''\n", + "content='The'\n", + "content=' current'\n", + "content=' weather'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' is'\n", + "content=' '\n", + "content='52'\n", + "content='.'\n", + "content='0'\n", + "content=' °'\n", + "content='F'\n", + "content=' with'\n", + "content=' a'\n", + "content=' light'\n", + "content=' breeze'\n", + "content=' of'\n", + "content=' '\n", + "content='6'\n", + "content='.'\n", + "content='9'\n", + "content=' mph'\n", + "content=' coming'\n", + "content=' from'\n", + "content=' the'\n", + "content=' east'\n", + "content='.'\n", + "content=' The'\n", + "content=' sky'\n", + "content=' is'\n", + "content=' mostly'\n", + "content=' cloudy'\n", + "content=' with'\n", + "content=' cloud'\n", + "content=' cover'\n", + "content=' at'\n", + "content=' '\n", + "content='18'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content=','\n", + "content=' mostly'\n", + "content=' clear'\n", + "content=' at'\n", + "content=' '\n", + "content='4'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content=','\n", + "content=' and'\n", + "content=' partly'\n", + "content=' cloudy'\n", + "content=' at'\n", + "content=' '\n", + "content='15'\n", + "content=','\n", + "content='000'\n", + "content=' ft'\n", + "content='.'\n", + "content=''\n" + ] + } + ], + "source": [ + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/chat_agent_executor_with_function_calling/async.ipynb b/examples/chat_agent_executor_with_function_calling/async.ipynb index e9c21021e..e9cb177c8 100644 --- a/examples/chat_agent_executor_with_function_calling/async.ipynb +++ b/examples/chat_agent_executor_with_function_calling/async.ipynb @@ -104,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], @@ -126,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], @@ -154,7 +154,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], @@ -178,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], @@ -209,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], @@ -256,7 +256,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], @@ -314,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], @@ -375,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", "metadata": {}, "outputs": [ @@ -388,7 +388,7 @@ " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}" ] }, - "execution_count": 10, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -419,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ @@ -435,19 +435,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, in January, the average daytime maximum temperature is around 13°C with an average of 6 hours of sunshine per day. Please note that this information is based on long-term weather averages and may vary.')]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, in January, the average daytime maximum temperature is around 13°C with an average of 6 hours of sunshine per day. Please note that this information is based on long-term weather averages and may vary.')]}\n", "\n", "---\n", "\n" @@ -479,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "id": "cfd140f0-a5a6-4697-8115-322242f197b5", "metadata": {}, "outputs": [ @@ -503,66 +503,57 @@ "content=''\n", "content=''\n", "content='The'\n", - "content=' current'\n", "content=' weather'\n", "content=' in'\n", "content=' San'\n", "content=' Francisco'\n", "content=' is'\n", - "content=' '\n", - "content='52'\n", + "content=' currently'\n", + "content=' not'\n", + "content=' available'\n", "content='.'\n", - "content='0'\n", - "content=' °'\n", - "content='F'\n", + "content=' However'\n", + "content=','\n", + "content=' in'\n", + "content=' January'\n", + "content=','\n", + "content=' the'\n", + "content=' average'\n", + "content=' daytime'\n", + "content=' maximum'\n", + "content=' temperature'\n", + "content=' is'\n", + "content=' around'\n", + "content=' '\n", + "content='13'\n", + "content='°C'\n", "content=' with'\n", - "content=' a'\n", - "content=' light'\n", - "content=' breeze'\n", + "content=' an'\n", + "content=' average'\n", "content=' of'\n", "content=' '\n", "content='6'\n", + "content=' hours'\n", + "content=' of'\n", + "content=' sunshine'\n", + "content=' per'\n", + "content=' day'\n", "content='.'\n", - "content='9'\n", - "content=' mph'\n", - "content=' coming'\n", - "content=' from'\n", - "content=' the'\n", - "content=' east'\n", - "content='.'\n", - "content=' The'\n", - "content=' sky'\n", + "content=' Please'\n", + "content=' note'\n", + "content=' that'\n", + "content=' this'\n", + "content=' information'\n", "content=' is'\n", - "content=' mostly'\n", - "content=' cloudy'\n", - "content=' with'\n", - "content=' cloud'\n", - "content=' cover'\n", - "content=' at'\n", - "content=' '\n", - "content='18'\n", - "content=','\n", - "content='000'\n", - "content=' ft'\n", - "content=','\n", - "content=' mostly'\n", - "content=' clear'\n", - "content=' at'\n", - "content=' '\n", - "content='4'\n", - "content=','\n", - "content='000'\n", - "content=' ft'\n", - "content=','\n", + "content=' based'\n", + "content=' on'\n", + "content=' long'\n", + "content='-term'\n", + "content=' weather'\n", + "content=' averages'\n", "content=' and'\n", - "content=' partly'\n", - "content=' cloudy'\n", - "content=' at'\n", - "content=' '\n", - "content='15'\n", - "content=','\n", - "content='000'\n", - "content=' ft'\n", + "content=' may'\n", + "content=' vary'\n", "content='.'\n", "content=''\n" ] @@ -608,7 +599,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/examples/chat_agent_executor_with_function_calling/base.ipynb b/examples/chat_agent_executor_with_function_calling/base.ipynb index cae3f6212..d6dd885e1 100644 --- a/examples/chat_agent_executor_with_function_calling/base.ipynb +++ b/examples/chat_agent_executor_with_function_calling/base.ipynb @@ -5,7 +5,7 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# Chat Executor\n", + "# Chat Agent Executor\n", "\n", "In this example we will build a chat executor that uses function calling from scratch." ] @@ -291,7 +291,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", "metadata": {}, "outputs": [ @@ -361,11 +361,11 @@ "text/plain": [ "{'messages': [HumanMessage(content='what is the weather in sf'),\n", " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", - " FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'),\n", - " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}" + " FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'),\n", + " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ @@ -412,19 +412,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", "\n", "---\n", "\n" @@ -456,7 +456,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 16, "id": "cfd140f0-a5a6-4697-8115-322242f197b5", "metadata": {}, "outputs": [ @@ -501,65 +501,28 @@ "content=' can'\n", "content=' check'\n", "content=' the'\n", - "content=' historical'\n", "content=' weather'\n", - "content=' data'\n", + "content=' forecast'\n", "content=' for'\n", - "content=' January'\n", - "content=' '\n", - "content='202'\n", - "content='4'\n", - "content=' in'\n", "content=' San'\n", "content=' Francisco'\n", - "content=' ['\n", - "content='here'\n", - "content=']('\n", - "content='https'\n", - "content='://'\n", - "content='we'\n", - "content='athers'\n", - "content='park'\n", + "content=' on'\n", + "content=' websites'\n", + "content=' like'\n", + "content=' Weather'\n", "content='.com'\n", - "content='/h'\n", - "content='/m'\n", - "content='/'\n", - "content='557'\n", - "content='/'\n", - "content='202'\n", - "content='4'\n", - "content='/'\n", - "content='1'\n", - "content='/H'\n", - "content='istorical'\n", - "content='-'\n", + "content=' or'\n", + "content=' Acc'\n", + "content='u'\n", "content='Weather'\n", - "content='-in'\n", - "content='-Jan'\n", - "content='uary'\n", - "content='-'\n", - "content='202'\n", - "content='4'\n", - "content='-in'\n", - "content='-S'\n", - "content='an'\n", - "content='-F'\n", - "content='r'\n", - "content='anc'\n", - "content='isco'\n", - "content='-Cal'\n", - "content='ifornia'\n", - "content='-'\n", - "content='United'\n", - "content='-'\n", - "content='States'\n", - "content=').'\n", + "content='.'\n", "content=''\n" ] } ], "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf?\")]}\n", + "\n", "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", " for op in output.ops:\n", diff --git a/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb index 4830192c8..0b42ce534 100644 --- a/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb +++ b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb @@ -93,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", "metadata": {}, "outputs": [], @@ -111,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], @@ -134,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], @@ -162,7 +162,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], @@ -186,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], @@ -217,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], @@ -260,7 +260,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", "metadata": {}, "outputs": [], @@ -282,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", "metadata": {}, "outputs": [], @@ -305,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", "metadata": {}, "outputs": [], @@ -330,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", "metadata": {}, "outputs": [], @@ -375,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], @@ -440,7 +440,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 12, "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", "metadata": {}, "outputs": [ @@ -456,19 +456,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in january59°F Clear/Sunny 47% of time 26% 14% 9% Weather at 6pm 54°F Clear/Sunny 50% of time 23% 14%'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather in San Francisco in January 2024 on this website: [San Francisco Weather in January 2024](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).')]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in january59°F Clear/Sunny 47% of time 26% 14% 9% Weather at 6pm 54°F Clear/Sunny 50% of time 23% 14%'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, you can check the weather in San Francisco in January 2024 on this website: [San Francisco Weather in January 2024](https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/).')]}\n", "\n", "---\n", "\n" @@ -490,7 +490,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 13, "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [ @@ -506,13 +506,13 @@ "\n", "Output from node 'final':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf? return this result directly by setting return_direct = True'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\",\\n \"return_direct\": true\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n" diff --git a/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb index f6457742a..05096c966 100644 --- a/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb +++ b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb @@ -420,19 +420,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerable49°F Clear/Sunny 47% of time 24% 13% 8% Weather at 12pm 59°F Clear/Sunny 47% of time 26% 14% 9% Weather at 6pm 54°F Clear/Sunny 50% of time 23% 14%'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [AIMessage(content=\"I couldn't find the current weather in San Francisco. However, the average weather in January is around 49°F (9.4°C) during the day and 54°F (12.2°C) in the evening. It is mostly clear and sunny, with a 47% chance of clear/sunny weather during the day and 50% chance in the evening.\")]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoJanuary 2024 Weather History in San Francisco California, United States. The data for this report comes from the San Francisco International Airport. ... frigid 15°F freezing 32°F very cold 45°F cold 55°F cool 65°F comfortable 75°F warm 85°F hot 95°F sweltering. The hourly reported temperature, color coded into bands. ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco on this [website](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\": \"what is the weather in sf\"}'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january The climate of San Francisco in january is tolerable49°F Clear/Sunny 47% of time 24% 13% 8% Weather at 12pm 59°F Clear/Sunny 47% of time 26% 14% 9% Weather at 6pm 54°F Clear/Sunny 50% of time 23% 14%'}]\", name='tavily_search_results_json'), AIMessage(content=\"I couldn't find the current weather in San Francisco. However, the average weather in January is around 49°F (9.4°C) during the day and 54°F (12.2°C) in the evening. It is mostly clear and sunny, with a 47% chance of clear/sunny weather during the day and 50% chance in the evening.\")]}\n", "\n", "---\n", "\n" diff --git a/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb index 2c4de7109..c2e919376 100644 --- a/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb +++ b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb @@ -5,7 +5,7 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# Chat Executor\n", + "# Human-in-the-loop\n", "\n", "In this example we will build a chat executor that has a human in the loop. We will use the human to approve specific actions.\n", "\n", @@ -397,31 +397,31 @@ "name": "stdin", "output_type": "stream", "text": [ - "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'}? y\n" + "[y/n] continue with: tool='tavily_search_results_json' tool_input={'query': 'weather in San Francisco'}? n\n" ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", - "\n", - "---\n", - "\n" + "ename": "ValueError", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 4\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mlangchain_core\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmessages\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m HumanMessage\n\u001b[1;32m 3\u001b[0m inputs \u001b[38;5;241m=\u001b[39m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmessages\u001b[39m\u001b[38;5;124m\"\u001b[39m: [HumanMessage(content\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwhat is the weather in sf\u001b[39m\u001b[38;5;124m\"\u001b[39m)]}\n\u001b[0;32m----> 4\u001b[0m \u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mapp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\u001b[43minputs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# stream() yields dictionaries with output keyed by node name\u001b[39;49;00m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43moutput\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mitems\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mOutput from node \u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mkey\u001b[49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m:\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/workplace/permchain/langgraph/pregel/__init__.py:528\u001b[0m, in \u001b[0;36mPregel.transform\u001b[0;34m(self, input, config, output_keys, input_keys, **kwargs)\u001b[0m\n\u001b[1;32m 519\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mtransform\u001b[39m(\n\u001b[1;32m 520\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 521\u001b[0m \u001b[38;5;28minput\u001b[39m: Iterator[Union[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any], Any]],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 526\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Any,\n\u001b[1;32m 527\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Iterator[Union[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any], Any]]:\n\u001b[0;32m--> 528\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform_stream_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 529\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 530\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_transform\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 531\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 532\u001b[0m \u001b[43m \u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43moutput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 533\u001b[0m \u001b[43m \u001b[49m\u001b[43minput_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 534\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 535\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 536\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:1226\u001b[0m, in \u001b[0;36mRunnable._transform_stream_with_config\u001b[0;34m(self, input, transformer, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 1224\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1225\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m-> 1226\u001b[0m chunk: Output \u001b[38;5;241m=\u001b[39m context\u001b[38;5;241m.\u001b[39mrun(\u001b[38;5;28mnext\u001b[39m, iterator) \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[1;32m 1227\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m chunk\n\u001b[1;32m 1228\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m final_output_supported:\n", + "File \u001b[0;32m~/workplace/permchain/langgraph/pregel/__init__.py:313\u001b[0m, in \u001b[0;36mPregel._transform\u001b[0;34m(self, input, run_manager, config, input_keys, output_keys)\u001b[0m\n\u001b[1;32m 303\u001b[0m done, inflight \u001b[38;5;241m=\u001b[39m concurrent\u001b[38;5;241m.\u001b[39mfutures\u001b[38;5;241m.\u001b[39mwait(\n\u001b[1;32m 304\u001b[0m [\n\u001b[1;32m 305\u001b[0m executor\u001b[38;5;241m.\u001b[39msubmit(proc\u001b[38;5;241m.\u001b[39minvoke, \u001b[38;5;28minput\u001b[39m, config)\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 309\u001b[0m timeout\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstep_timeout,\n\u001b[1;32m 310\u001b[0m )\n\u001b[1;32m 312\u001b[0m \u001b[38;5;66;03m# interrupt on failure or timeout\u001b[39;00m\n\u001b[0;32m--> 313\u001b[0m \u001b[43m_interrupt_or_proceed\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdone\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minflight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstep\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 315\u001b[0m \u001b[38;5;66;03m# apply writes to channels\u001b[39;00m\n\u001b[1;32m 316\u001b[0m _apply_writes(checkpoint, channels, pending_writes, config, step \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m)\n", + "File \u001b[0;32m~/workplace/permchain/langgraph/pregel/__init__.py:611\u001b[0m, in \u001b[0;36m_interrupt_or_proceed\u001b[0;34m(done, inflight, step)\u001b[0m\n\u001b[1;32m 609\u001b[0m inflight\u001b[38;5;241m.\u001b[39mpop()\u001b[38;5;241m.\u001b[39mcancel()\n\u001b[1;32m 610\u001b[0m \u001b[38;5;66;03m# raise the exception\u001b[39;00m\n\u001b[0;32m--> 611\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m exc\n\u001b[1;32m 612\u001b[0m \u001b[38;5;66;03m# TODO this is where retry of an entire step would happen\u001b[39;00m\n\u001b[1;32m 614\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m inflight:\n\u001b[1;32m 615\u001b[0m \u001b[38;5;66;03m# if we got here means we timed out\u001b[39;00m\n", + "File \u001b[0;32m~/.pyenv/versions/3.11.1/lib/python3.11/concurrent/futures/thread.py:58\u001b[0m, in \u001b[0;36m_WorkItem.run\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[1;32m 57\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m---> 58\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 59\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfuture\u001b[38;5;241m.\u001b[39mset_exception(exc)\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:3596\u001b[0m, in \u001b[0;36mRunnableBindingBase.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 3590\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21minvoke\u001b[39m(\n\u001b[1;32m 3591\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 3592\u001b[0m \u001b[38;5;28minput\u001b[39m: Input,\n\u001b[1;32m 3593\u001b[0m config: Optional[RunnableConfig] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 3594\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: Optional[Any],\n\u001b[1;32m 3595\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Output:\n\u001b[0;32m-> 3596\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbound\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3597\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3598\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_merge_configs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3599\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3600\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:1774\u001b[0m, in \u001b[0;36mRunnableSequence.invoke\u001b[0;34m(self, input, config)\u001b[0m\n\u001b[1;32m 1772\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1773\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, step \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msteps):\n\u001b[0;32m-> 1774\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mstep\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1775\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1776\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# mark each step as a child run\u001b[39;49;00m\n\u001b[1;32m 1777\u001b[0m \u001b[43m \u001b[49m\u001b[43mpatch_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1778\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrun_manager\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_child\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43mf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mseq:step:\u001b[39;49m\u001b[38;5;132;43;01m{\u001b[39;49;00m\u001b[43mi\u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[38;5;132;43;01m}\u001b[39;49;00m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1779\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1780\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1781\u001b[0m \u001b[38;5;66;03m# finish the root run\u001b[39;00m\n\u001b[1;32m 1782\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:3074\u001b[0m, in \u001b[0;36mRunnableLambda.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m 3072\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Invoke this runnable synchronously.\"\"\"\u001b[39;00m\n\u001b[1;32m 3073\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfunc\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m-> 3074\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3075\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_invoke\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3076\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3077\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3078\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3079\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3080\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 3081\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\n\u001b[1;32m 3082\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot invoke a coroutine function synchronously.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 3083\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUse `ainvoke` instead.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 3084\u001b[0m )\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:975\u001b[0m, in \u001b[0;36mRunnable._call_with_config\u001b[0;34m(self, func, input, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m 971\u001b[0m context \u001b[38;5;241m=\u001b[39m copy_context()\n\u001b[1;32m 972\u001b[0m context\u001b[38;5;241m.\u001b[39mrun(var_child_runnable_config\u001b[38;5;241m.\u001b[39mset, child_config)\n\u001b[1;32m 973\u001b[0m output \u001b[38;5;241m=\u001b[39m cast(\n\u001b[1;32m 974\u001b[0m Output,\n\u001b[0;32m--> 975\u001b[0m \u001b[43mcontext\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 976\u001b[0m \u001b[43m \u001b[49m\u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 977\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 978\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m 979\u001b[0m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 980\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 981\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 982\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 983\u001b[0m )\n\u001b[1;32m 984\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 985\u001b[0m run_manager\u001b[38;5;241m.\u001b[39mon_chain_error(e)\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/config.py:326\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m 324\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m 325\u001b[0m kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/base.py:2950\u001b[0m, in \u001b[0;36mRunnableLambda._invoke\u001b[0;34m(self, input, run_manager, config, **kwargs)\u001b[0m\n\u001b[1;32m 2948\u001b[0m output \u001b[38;5;241m=\u001b[39m chunk\n\u001b[1;32m 2949\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 2950\u001b[0m output \u001b[38;5;241m=\u001b[39m \u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2951\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 2952\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2953\u001b[0m \u001b[38;5;66;03m# If the output is a runnable, invoke it\u001b[39;00m\n\u001b[1;32m 2954\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(output, Runnable):\n", + "File \u001b[0;32m~/workplace/langchain/libs/core/langchain_core/runnables/config.py:326\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m 324\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m 325\u001b[0m kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 326\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[7], line 14\u001b[0m, in \u001b[0;36mcall_tool\u001b[0;34m(state)\u001b[0m\n\u001b[1;32m 12\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28minput\u001b[39m(prompt\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m[y/n] continue with: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00maction\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m?\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m response \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m---> 14\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;66;03m# We call the tool_executor and get back a response\u001b[39;00m\n\u001b[1;32m 16\u001b[0m response \u001b[38;5;241m=\u001b[39m tool_executor\u001b[38;5;241m.\u001b[39minvoke(action)\n", + "\u001b[0;31mValueError\u001b[0m: " ] } ], diff --git a/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb index 0e547d597..c0b7089a3 100644 --- a/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb +++ b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb @@ -5,7 +5,7 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# Chat Executor\n", + "# Managing Agent Steps\n", "\n", "In this example we will build a chat executor that better manages intermediate steps. The base chat executor will just put all messages into the model, but if the intermediate steps an agent is taking start to get long, you may want to modify that. In this example we will only include the ten most recent messages.\n", "\n", @@ -237,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", "metadata": {}, "outputs": [], @@ -398,19 +398,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the historical weather data for January 2024 in San Francisco [here](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States).\")]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}\n", "\n", "---\n", "\n" diff --git a/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb index 08e3a3ced..ea61b18a4 100644 --- a/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb +++ b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb @@ -5,7 +5,7 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# Chat Executor\n", + "# Respond in a format\n", "\n", "In this example we will build a chat executor that responds in a specific format. We will do this by using OpenAI function calling. This is useful when you want to enforce that an agent's response is in a specific format. In this example, we will ask it respond as if a weatherman, so to return the temperature and then any other additional info.\n", "\n", @@ -393,19 +393,19 @@ "\n", "Output from node 'action':\n", "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}\n", + "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json')]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 45,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", "\n", "---\n", "\n", "Output from node '__end__':\n", "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 60,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", + "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"temperature\": 45,\\n \"other_notes\": \"Partly cloudy\"\\n}', 'name': 'Response'}})]}\n", "\n", "---\n", "\n" @@ -424,6 +424,14 @@ " print(value)\n", " print(\"\\n---\\n\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eed4360d-2cdf-497b-b03f-8bc51062f780", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From b5cbaad301bff00df4747a5933de868dfb71148f Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Mon, 15 Jan 2024 17:51:33 -0800 Subject: [PATCH 23/25] cr --- README.md | 9 + examples/async.ipynb | 4 +- .../async.ipynb => streaming-tokens.ipynb} | 195 ++++-------------- 3 files changed, 53 insertions(+), 155 deletions(-) rename examples/{chat_agent_executor_with_function_calling/async.ipynb => streaming-tokens.ipynb} (67%) diff --git a/README.md b/README.md index c19cfab94..87aab4f13 100644 --- a/README.md +++ b/README.md @@ -452,6 +452,15 @@ We also have a lot of examples highlighting how to slightly modify the base chat - [Force calling a tool first](examples/agent_executor/force-calling-a-tool-first.ipynb): How to always call a specific tool first - [Managing agent steps](examples/agent_executor/managing-agent-steps.ipynb): How to more explicitly manage intermediate steps that an agent takes +### Async + +If you are running LangGraph in async workflows, you may want to create the nodes to be async by default. +In order for a walkthrough on how to do that, see [this documentation](examples/async.ipynb) + +### Streaming Tokens + +Sometimes language models take a while to respond and you may want to stream tokens to end users. +For a guide on how to do this, see [this documentation](examples/streaming-tokens.ipynb) ## Documentation diff --git a/examples/async.ipynb b/examples/async.ipynb index 2fae3e3f9..7d4a8081c 100644 --- a/examples/async.ipynb +++ b/examples/async.ipynb @@ -7,9 +7,7 @@ "source": [ "# Async\n", "\n", - "In this example we will build a chat executor with native async implementations of the core logic. This enables taking advantage of Chat Models which have async clients, removing the need for calling the model in a separate thread.\n", - "\n", - "For more information" + "In this example we will build a chat executor with native async implementations of the core logic. This enables taking advantage of Chat Models which have async clients, removing the need for calling the model in a separate thread." ] }, { diff --git a/examples/chat_agent_executor_with_function_calling/async.ipynb b/examples/streaming-tokens.ipynb similarity index 67% rename from examples/chat_agent_executor_with_function_calling/async.ipynb rename to examples/streaming-tokens.ipynb index e9cb177c8..d1dfed602 100644 --- a/examples/chat_agent_executor_with_function_calling/async.ipynb +++ b/examples/streaming-tokens.ipynb @@ -5,9 +5,14 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# Chat Executor: Async\n", + "# Streaming Tokens\n", "\n", - "In this example we will build a chat executor with native async implementations of the core logic. This enables taking advantage of Chat Models which have async clients, removing the need for calling the model in a separate thread." + "In this example we will focus on explaining how to stream tokens from a language model that is powering an agent. We will use a chat agent executor as an example. There a few specific things we need to do in order to properly stream tokens. They are: \n", + "\n", + "1. Set `streaming=True` when creating the LLM\n", + "2. Create nodes with [async methods](./async.ipynb) - this is best practice because in order to stream tokens we will use the `async_log` method.\n", + "\n", + "we will call them out with the **STREAMING** tag below (if you just want to search for those)." ] }, { @@ -149,7 +154,11 @@ "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n", + "\n", + "**STREAMING**\n", + "\n", + "Here, we set `streaming=True` when creating the model." ] }, { @@ -249,7 +258,7 @@ "\n", "Let's define the nodes, as well as a function to decide how what conditional edge to take.\n", "\n", - "**MODIFICATION**\n", + "**STREAMING**\n", "\n", "We define each node as an async function." ] @@ -362,117 +371,14 @@ "app = workflow.compile()" ] }, - { - "cell_type": "markdown", - "id": "547c3931-3dae-4281-ad4e-4b51305594d4", - "metadata": {}, - "source": [ - "## Use it!\n", - "\n", - "We can now use it!\n", - "This now exposes the [same interface](https://python.langchain.com/docs/expression_language/) as all other LangChain runnables." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'messages': [HumanMessage(content='what is the weather in sf'),\n", - " AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}),\n", - " FunctionMessage(content=\"[{'url': 'https://www.whereandwhen.net/when/north-america/california/san-francisco-ca/january/', 'content': 'Best time to go to San Francisco? Weather in San Francisco in january 2024 How was the weather last january? Here is the day by day recorded weather in San Francisco in january 2023: Seasonal average climate and temperature of San Francisco in january 8% 46% 29% 12% 8% Evolution of daily average temperature and precipitation in San Francisco in januaryWeather in San Francisco in january 2024. The weather in San Francisco in january comes from statistical datas on the past years. You can view the weather statistics the entire month, but also by using the tabs for the beginning, the middle and the end of the month. ... 16-01-2023 45°F to 52°F. 17-01-2023 45°F to 54°F. 18-01-2023 47°F to ...'}]\", name='tavily_search_results_json'),\n", - " AIMessage(content=\"I'm sorry, but I couldn't find the current weather in San Francisco. However, you can check the weather forecast for San Francisco on websites like Weather.com or AccuWeather.\")]}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "await app.ainvoke(inputs)" - ] - }, - { - "cell_type": "markdown", - "id": "5a9e8155-70c5-4973-912c-dc55104b2acf", - "metadata": {}, - "source": [ - "This may take a little bit - it's making a few calls behind the scenes.\n", - "In order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.\n", - "\n", - "## Streaming\n", - "\n", - "LangGraph has support for several different types of streaming.\n", - "\n", - "### Streaming Node Output\n", - "\n", - "One of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f544977e-31f7-41f0-88c4-ec9c27b8cecb", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='The weather in San Francisco is currently not available. However, in January, the average daytime maximum temperature is around 13°C with an average of 6 hours of sunshine per day. Please note that this information is based on long-term weather averages and may vary.')]}\n", - "\n", - "---\n", - "\n", - "Output from node '__end__':\n", - "---\n", - "{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://www.weather2travel.com/california/san-francisco/january/', 'content': 'San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. San Francisco January sunrise & sunset times How sunny is it in San Francisco in January?San Francisco weather in January 2024 Expect 13°C daytime maximum temperatures in the shade with on average 6 hours of sunshine per day in San Francisco in January. Check more long-term weather averages for San Francisco in January before you book your next holiday to California in 2024/2025. 13 13°C max day temperature 6'}]\", name='tavily_search_results_json'), AIMessage(content='The weather in San Francisco is currently not available. However, in January, the average daytime maximum temperature is around 13°C with an average of 6 hours of sunshine per day. Please note that this information is based on long-term weather averages and may vary.')]}\n", - "\n", - "---\n", - "\n" - ] - } - ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "async for output in app.astream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] - }, { "cell_type": "markdown", "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", "metadata": {}, "source": [ - "### Streaming LLM Tokens\n", + "## Streaming LLM Tokens\n", "\n", - "You can also access the LLM tokens as they are produced by each node. \n", + "You can access the LLM tokens as they are produced by each node. \n", "In this case only the \"agent\" node produces LLM tokens.\n", "In order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. `ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)`)\n" ] @@ -502,64 +408,49 @@ "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", "content=''\n", "content=''\n", - "content='The'\n", + "content='I'\n", + "content=\"'m\"\n", + "content=' sorry'\n", + "content=','\n", + "content=' but'\n", + "content=' I'\n", + "content=' couldn'\n", + "content=\"'t\"\n", + "content=' find'\n", + "content=' the'\n", + "content=' current'\n", "content=' weather'\n", "content=' in'\n", "content=' San'\n", "content=' Francisco'\n", - "content=' is'\n", - "content=' currently'\n", - "content=' not'\n", - "content=' available'\n", "content='.'\n", "content=' However'\n", "content=','\n", - "content=' in'\n", - "content=' January'\n", - "content=','\n", + "content=' you'\n", + "content=' can'\n", + "content=' check'\n", "content=' the'\n", - "content=' average'\n", - "content=' daytime'\n", - "content=' maximum'\n", - "content=' temperature'\n", - "content=' is'\n", - "content=' around'\n", - "content=' '\n", - "content='13'\n", - "content='°C'\n", - "content=' with'\n", - "content=' an'\n", - "content=' average'\n", - "content=' of'\n", - "content=' '\n", - "content='6'\n", - "content=' hours'\n", - "content=' of'\n", - "content=' sunshine'\n", - "content=' per'\n", - "content=' day'\n", - "content='.'\n", - "content=' Please'\n", - "content=' note'\n", - "content=' that'\n", - "content=' this'\n", - "content=' information'\n", - "content=' is'\n", - "content=' based'\n", - "content=' on'\n", - "content=' long'\n", - "content='-term'\n", "content=' weather'\n", - "content=' averages'\n", - "content=' and'\n", - "content=' may'\n", - "content=' vary'\n", + "content=' forecast'\n", + "content=' for'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' on'\n", + "content=' websites'\n", + "content=' like'\n", + "content=' Weather'\n", + "content='.com'\n", + "content=' or'\n", + "content=' Acc'\n", + "content='u'\n", + "content='Weather'\n", "content='.'\n", "content=''\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", From 8ea24435feb49edbf6eceb9310ea2f6b319a5a5e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 16 Jan 2024 10:46:21 -0800 Subject: [PATCH 24/25] Add more validation --- langgraph/pregel/__init__.py | 17 +++++++++++++++-- langgraph/pregel/validate.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 49187d1f4..4ec34ae32 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -61,7 +61,7 @@ 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.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite WriteValue = Union[ @@ -179,7 +179,12 @@ class Pregel( @root_validator(skip_on_failure=True) def validate_pregel(cls, values: dict[str, Any]) -> dict[str, Any]: validate_graph( - values["nodes"], values["channels"], values["input"], values["output"] + values["nodes"], + values["channels"], + values["input"], + values["output"], + values["hidden"], + values["interrupt"], ) return values @@ -239,8 +244,12 @@ class Pregel( # assign defaults if output_keys is None: output_keys = [chan for chan in self.channels if chan not in self.hidden] + else: + validate_keys(output_keys, self.channels) if input_keys is None: input_keys = self.input + else: + validate_keys(input_keys, self.channels) # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one @@ -369,8 +378,12 @@ class Pregel( # assign defaults if output_keys is None: output_keys = [chan for chan in self.channels if chan not in self.hidden] + else: + validate_keys(output_keys, self.channels) if input_keys is None: input_keys = self.input + else: + validate_keys(input_keys, self.channels) # copy nodes to ignore mutations during execution processes = {**self.nodes} # get checkpoint from saver, or create an empty one diff --git a/langgraph/pregel/validate.py b/langgraph/pregel/validate.py index 096061665..1186bb8be 100644 --- a/langgraph/pregel/validate.py +++ b/langgraph/pregel/validate.py @@ -11,6 +11,8 @@ def validate_graph( channels: dict[str, BaseChannel], input: Union[str, Sequence[str]], output: Union[str, Sequence[str]], + hidden: Sequence[str], + interrupt: Sequence[str], ) -> None: subscribed_channels = set[str]() for node in nodes.values(): @@ -52,3 +54,19 @@ def validate_graph( for chan in ReservedChannels: if chan not in channels: channels[chan] = LastValue(Any) # type: ignore[arg-type] + + validate_keys(hidden, channels) + validate_keys(interrupt, channels) + + +def validate_keys( + keys: Union[str, Sequence[str]], + channels: dict[str, BaseChannel], +) -> None: + if isinstance(keys, str): + if keys not in channels: + raise ValueError(f"Key {keys} not in channels") + else: + for chan in keys: + if chan not in channels: + raise ValueError(f"Key {chan} not in channels") From efe83776e4783ca238664db511bd78225e6bbf2a Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 16 Jan 2024 12:30:53 -0800 Subject: [PATCH 25/25] Update langgraph/prebuilt/tool_executor.py Co-authored-by: Eugene Yurtsev --- langgraph/prebuilt/tool_executor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/langgraph/prebuilt/tool_executor.py b/langgraph/prebuilt/tool_executor.py index b82e15653..3c1bb20d9 100644 --- a/langgraph/prebuilt/tool_executor.py +++ b/langgraph/prebuilt/tool_executor.py @@ -34,6 +34,7 @@ class ToolExecutor(RunnableBinding): def __init__( self, tools: Sequence[BaseTool], + *, invalid_tool_msg_template: str = INVALID_TOOL_MSG_TEMPLATE, **kwargs: Any, ) -> None: