mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
33 KiB
33 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 [6]:
from typing import List, Tuple
from typing_extensions import Annotated
from langchain_core.documents import Document
from langchain_core.pydantic_v1 import BaseModel
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
@tool(parse_docstring=True, response_format="content_and_artifact")
def get_context(question: List[str]) -> 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, InjectedState]
) -> 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 [9]:
cite_context_sources.get_input_schema().schema()Out [9]:
{'title': 'cite_context_sourcesSchema',
'description': 'Cite which source a claim was based on.',
'type': 'object',
'properties': {'claim': {'title': 'Claim',
'description': 'The claim that was made.',
'type': 'string'},
'state': {'title': 'State', 'type': 'object'}},
'required': ['claim', 'state']}In [11]:
cite_context_sources.tool_call_schema.schema()Out [11]:
{'title': 'cite_context_sources',
'description': 'Cite which source a claim was based on.',
'type': 'object',
'properties': {'claim': {'title': 'Claim',
'description': 'The claim that was made.',
'type': 'string'}},
'required': ['claim']}In [12]:
import operator
from typing import Annotated, Sequence, TypedDict
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]In [18]:
from copy import deepcopy
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode
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]
# 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]}
# ToolNode will automatically take care of injecting state into tools
tool_node = ToolNode(tools)In [19]:
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", tool_node)
# 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 [20]:
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 [21]:
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_BidVTw5NiW2wp8Ez7m8dDoHI', 'function': {'arguments': '{"question":["latest news about FooBar"]}', 'name': 'get_context'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 87, 'total_tokens': 106}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fcac1b73-563e-4f4c-b1b0-626f55d377be-0', tool_calls=[{'name': 'get_context', 'args': {'question': ['latest news about FooBar']}, 'id': 'call_BidVTw5NiW2wp8Ez7m8dDoHI', 'type': 'tool_call'}], usage_metadata={'input_tokens': 87, 'output_tokens': 19, 'total_tokens': 106})]}
---
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_BidVTw5NiW2wp8Ez7m8dDoHI', 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 has just raised 1 billion dollars!', response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 150, 'total_tokens': 169}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'stop', 'logprobs': None}, id='run-a8407471-7715-4c16-bd46-c29e5751e882-0', usage_metadata={'input_tokens': 150, 'output_tokens': 19, 'total_tokens': 169})]}
---
In [22]:
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_EB0zaQypXMqEUzaqwflUr0zH', 'function': {'arguments': '{"claim":"FooBar company just raised 1 Billion dollars!"}', 'name': 'cite_context_sources'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 25, 'prompt_tokens': 183, 'total_tokens': 208}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b4952777-e2b3-4448-be87-200e6e80981b-0', tool_calls=[{'name': 'cite_context_sources', 'args': {'claim': 'FooBar company just raised 1 Billion dollars!'}, 'id': 'call_EB0zaQypXMqEUzaqwflUr0zH', 'type': 'tool_call'}], usage_metadata={'input_tokens': 183, 'output_tokens': 25, 'total_tokens': 208})]}
---
Output from node 'action':
---
{'messages': [ToolMessage(content='twitter', name='cite_context_sources', tool_call_id='call_EB0zaQypXMqEUzaqwflUr0zH', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!')])]}
---
Output from node 'agent':
---
{'messages': [AIMessage(content='The information that FooBar company just raised 1 billion dollars comes from Twitter.', response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 218, 'total_tokens': 235}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_400f27fa1f', 'finish_reason': 'stop', 'logprobs': None}, id='run-a0dede05-dadd-46f6-8654-746520d4cef8-0', usage_metadata={'input_tokens': 218, 'output_tokens': 17, 'total_tokens': 235})]}
---