mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-07 18:27:52 +02:00
34 KiB
34 KiB
In [41]:
%%capture --no-stderr
%pip install --quiet -U langgraph langchain langchain-openaiIn [1]:
import getpass
import os
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY") or getpass.getpass(
"OpenAI API Key:"
)In [2]:
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = os.environ.get(
"LANGCHAIN_API_KEY"
) or getpass.getpass("LangSmith API Key:")In [63]:
from typing import List, Tuple
from langchain_core.documents import Document
from langchain_core.pydantic_v1 import BaseModel
from langchain_core.tools import InjectedToolArg, tool
from typing_extensions import Annotated
@tool(parse_docstring=True, response_format="content_and_artifact")
def get_context(
question: List[str], state: Annotated[dict, InjectedToolArg]
) -> Tuple[str, List[Document]]:
"""Get context on the question.
Args:
question: The user question
"""
# return constant dummy output
docs = [
Document(
"FooBar company just raised 1 Billion dollars!",
metadata={"source": "twitter"},
),
Document(
"FooBar company is now only hiring AI's", metadata={"source": "twitter"}
),
Document(
"FooBar company was founded in 2019", metadata={"source": "wikipedia"}
),
Document(
"FooBar company makes friendly robots", metadata={"source": "wikipedia"}
),
]
return "\n\n".join(doc.page_content for doc in docs), docs
@tool(parse_docstring=True, response_format="content_and_artifact")
def cite_context_sources(
claim: str, state: Annotated[dict, InjectedToolArg]
) -> Tuple[str, List[Document]]:
"""Cite which source a claim was based on.
Args:
claim: The claim that was made.
"""
docs = []
# We get the potentially cited docs from past ToolMessages in our state.
for msg in state["messages"]:
if isinstance(msg, ToolMessage) and msg.name == "get_context":
docs.extend(msg.artifact)
class Cite(BaseModel):
"""Return the index(es) of the documents that justify the claim"""
indexes: List[int]
structured_model = model.with_structured_output(Cite)
system = f"Which of the following documents best justifies the claim:\n\n{claim}"
context = "\n\n".join(
f"Document {i}:\n" + doc.page_content for i, doc in enumerate(docs)
)
citation = structured_model.invoke([("system", system), ("human", context)])
cited_docs = [docs[i] for i in citation.indexes]
sources = ", ".join(doc.metadata["source"] for doc in cited_docs)
return sources, cited_docsIn [64]:
get_context.get_input_schema().schema()Out [64]:
{'title': 'get_contextSchema',
'description': 'Get context on the question.',
'type': 'object',
'properties': {'question': {'title': 'Question',
'description': 'The user question',
'type': 'array',
'items': {'type': 'string'}},
'state': {'title': 'State', 'type': 'object'}},
'required': ['question', 'state']}In [65]:
get_context.tool_call_schema.schema()Out [65]:
{'title': 'get_context',
'description': 'Get context on the question.',
'type': 'object',
'properties': {'question': {'title': 'Question',
'description': 'The user question',
'type': 'array',
'items': {'type': 'string'}}},
'required': ['question']}In [66]:
import operator
from typing import Annotated, Sequence, TypedDict
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]In [67]:
from copy import deepcopy
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolExecutor, ToolInvocation
model = ChatOpenAI(model="gpt-4o", temperature=0)
# Define the function that determines whether to continue or not
def should_continue(state, config):
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
tools = [get_context, cite_context_sources]
tool_map = {tool_.name: tool_ for tool_ in tools}
# Define the function that calls the model
def call_model(state, config):
messages = state["messages"]
model_with_tools = model.bind_tools(tools)
response = model_with_tools.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Helper function for adding state to each tool call's arguments
def inject_state(message, state):
tool_calls = []
for tool_call in message.tool_calls:
tool_call_copy = deepcopy(tool_call)
tool_call_copy["args"]["state"] = state
tool_calls.append(tool_call_copy)
return tool_calls
# Define the function to execute tools
def call_tool(state, config):
messages = state["messages"]
last_message = messages[-1]
tool_messages = []
for tool_call in inject_state(last_message, state):
tool_messages.append(tool_map[tool_call["name"]].invoke(tool_call, config))
# We return a list, because this will get added to the existing list
return {"messages": tool_messages}In [68]:
from langgraph.graph import END, START, StateGraph
# 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.add_edge(START, "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
app = workflow.compile()In [69]:
from IPython.display import Image, display
try:
display(Image(app.get_graph(xray=True).draw_mermaid_png()))
except Exception:
# This requires some extra dependencies and is optional
passIn [70]:
from langchain_core.messages import HumanMessage
messages = [HumanMessage("what's the latest news about FooBar")]
for output in app.stream({"messages": messages}):
# stream() yields dictionaries with output keyed by node name
for key, value in output.items():
print(f"Output from node '{key}':")
print("---")
print(value)
messages.extend(value["messages"])
print("\n---\n")Output from node 'agent':
---
{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'function': {'arguments': '{"question":["what\'s the latest news about FooBar"]}', 'name': 'get_context'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 87, 'total_tokens': 109}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-adf99f00-a903-49f2-b0c3-37b84b9b801f-0', tool_calls=[{'name': 'get_context', 'args': {'question': ["what's the latest news about FooBar"]}, 'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'type': 'tool_call'}], usage_metadata={'input_tokens': 87, 'output_tokens': 22, 'total_tokens': 109})]}
---
Output from node 'action':
---
{'messages': [ToolMessage(content="FooBar company just raised 1 Billion dollars!\n\nFooBar company is now only hiring AI's\n\nFooBar company was founded in 2019\n\nFooBar company makes friendly robots", name='get_context', tool_call_id='call_aFUFt3TdazRnmD3FTZfxFAgL', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!'), Document(metadata={'source': 'twitter'}, page_content="FooBar company is now only hiring AI's"), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company was founded in 2019'), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company makes friendly robots')])]}
---
Output from node 'agent':
---
{'messages': [AIMessage(content='The latest news about FooBar is that the company just raised 1 billion dollars!', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 153, 'total_tokens': 171}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'stop', 'logprobs': None}, id='run-c229a397-fda3-415b-a188-1416fd5f21b7-0', usage_metadata={'input_tokens': 153, 'output_tokens': 18, 'total_tokens': 171})]}
---
In [71]:
messages.append(HumanMessage("where did you get this information?"))
for output in app.stream({"messages": messages}):
# stream() yields dictionaries with output keyed by node name
for key, value in output.items():
print(f"Output from node '{key}':")
print("---")
print(value)
print("\n---\n")Output from node 'agent':
---
{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'function': {'arguments': '{"claim":"The latest news about FooBar is that the company just raised 1 billion dollars!"}', 'name': 'cite_context_sources'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 185, 'total_tokens': 217}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-686d4706-81c9-4ca0-8f09-d9af02f4ad7f-0', tool_calls=[{'name': 'cite_context_sources', 'args': {'claim': 'The latest news about FooBar is that the company just raised 1 billion dollars!'}, 'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'type': 'tool_call'}], usage_metadata={'input_tokens': 185, 'output_tokens': 32, 'total_tokens': 217})]}
---
Output from node 'action':
---
{'messages': [ToolMessage(content='twitter', name='cite_context_sources', tool_call_id='call_qqB4kucZnVhrZ5mJSH1dF8Lb', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!')])]}
---
Output from node 'agent':
---
{'messages': [AIMessage(content='The information about FooBar raising 1 billion dollars came from Twitter.', response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 227, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_18cc0f1fa0', 'finish_reason': 'stop', 'logprobs': None}, id='run-343ad465-9a62-4d72-91bf-ab29c4fe8781-0', usage_metadata={'input_tokens': 227, 'output_tokens': 15, 'total_tokens': 242})]}
---