mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
57 KiB
57 KiB
In [ ]:
# %%capture --no-stderr
%pip install -U langchain-openai langgraph
# Or do langchain-{groq|anthropic|etc.} for another package with tool callingIn [ ]:
import getpass
import os
def _set_env(var: str):
if not os.environ.get(var):
os.environ[var] = getpass.getpass(f"{var}: ")
_set_env("OPENAI_API_KEY")
# Recommended to visualize the retry steps
_set_env("LANGCHAIN_API_KEY")
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Extraction Notebook"In [4]:
import asyncio
import operator
import uuid
from typing import (
Annotated,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Sequence,
Tuple,
Type,
Union,
cast,
)
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
ToolCall,
ToolMessage,
)
from langchain_core.prompt_values import PromptValue
from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError
from langchain_core.runnables import (
Runnable,
RunnableConfig,
RunnableLambda,
chain as as_runnable,
)
from langchain_core.runnables.config import get_executor_for_config
from langchain_core.tools import BaseTool, create_schema_from_function
from pydantic import BaseModel as BaseModelV2
from typing_extensions import TypedDict
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.utils import RunnableCallable
def _default_format_error(
error: BaseException, call: ToolCall, schema: Type[BaseModel]
):
return f"{repr(error)}\n\nRespond after fixing all validation errors."
def _default_aggregator(messages: Sequence[AnyMessage]) -> AIMessage:
for m in messages[::-1]:
if m.type == "ai":
return m
raise ValueError("No AI message found in the sequence.")
class ValidationNode(RunnableCallable):
"""
A node that runs the tools requested in the last AIMessage. It can be used
either in StateGraph with a "messages" key or in MessageGraph. If multiple
tool calls are requested, they will be run in parallel. The output will be
a list of ToolMessages, one for each tool call.
Args:
schemas: A list of schemas to validate the tool calls with. These can be
any of the following:
- A pydantic BaseModel
- A BaseTool (the args_schema will be used)
- A function (we will create a schema from the function signature)
name: The name of the node.
format_error: A function that takes an exception and a schema and returns
a string. By default, it returns the exception repr and a message to
respond after fixing all validation errors.
tags: A list of tags to add to the node.
"""
def __init__(
self,
schemas: Sequence[Union[BaseTool, BaseModel, Callable]],
*,
format_error: Optional[
Callable[[BaseException, ToolCall, Type[BaseModel]], str]
] = None,
name: str = "validation",
tags: Optional[list[str]] = None,
) -> None:
super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
self._format_error = format_error or _default_format_error
self.schemas_by_name: Dict[str, Type[BaseModel]] = {}
for schema in schemas:
if isinstance(schema, BaseTool):
if schema.args_schema is None:
raise ValueError(
f"Tool {schema.name} does not have an args_schema defined."
)
self.schemas_by_name[schema.name] = schema.args_schema
elif isinstance(schema, type) and issubclass(
schema, (BaseModel, BaseModelV2)
):
self.schemas_by_name[schema.__name__] = cast(Type[BaseModel], schema)
elif callable(schema):
# Assume it's a function
base_model = create_schema_from_function("Validation", schema)
self.schemas_by_name[schema.__name__] = base_model
else:
raise ValueError(
f"Unsupported input to ValidationNode. Expected BaseModel, tool or function. Got: {type(schema)}."
)
def _get_message(
self, input: Union[list[AnyMessage], dict[str, Any]]
) -> Tuple[str, AIMessage]:
if isinstance(input, list):
output_type = "list"
messages: list = input
elif messages := input.get("messages", []):
output_type = "dict"
else:
raise ValueError("No message found in input")
message: AnyMessage = messages[-1]
if not isinstance(message, AIMessage):
raise ValueError("Last message is not an AIMessage")
return output_type, message
def _func(
self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig
) -> Any:
output_type, message = self._get_message(input)
@as_runnable
def run_one(call: ToolCall):
schema = self.schemas_by_name[call["name"]]
try:
output = schema.validate(call["args"])
return ToolMessage(
content=output.json(),
name=call["name"],
tool_call_id=cast(str, call["id"]),
)
except ValidationError as e:
return ToolMessage(
content=self._format_error(e, call, schema),
name=call["name"],
tool_call_id=cast(str, call["id"]),
additional_kwargs={"is_error": True},
)
with get_executor_for_config(config) as executor:
outputs = [
*executor.map(lambda x: run_one.invoke(x, config), message.tool_calls)
]
if output_type == "list":
return outputs
else:
return {"messages": outputs}
async def _afunc(
self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig
) -> Any:
output_type, message = self._get_message(input)
@as_runnable
async def run_one(call: ToolCall):
schema = self.schemas_by_name[call["name"]]
try:
output = schema.validate(call["args"])
return ToolMessage(
content=output.json(),
name=call["name"],
tool_call_id=cast(str, call["id"]),
)
except ValidationError as e:
return ToolMessage(
content=self._format_error(e, call, schema),
name=call["name"],
tool_call_id=cast(str, call["id"]),
additional_kwargs={"is_error": True},
)
outputs = await asyncio.gather(
*(run_one.ainvoke(call, config) for call in message.tool_calls)
)
if output_type == "list":
return outputs
else:
return {"messages": outputs}
class RetryStrategy(TypedDict, total=False):
"""The retry strategy for a tool call."""
max_attempts: int
"""The maximum number of attempts to make."""
fallback: Optional[
Union[
Runnable[Sequence[AnyMessage], AIMessage],
Runnable[Sequence[AnyMessage], BaseMessage],
Callable[[Sequence[AnyMessage]], AIMessage],
]
]
"""The function to use once validation fails."""
aggregate_messages: Optional[Callable[[Sequence[AnyMessage]], AIMessage]]
def _bind_validator_with_retries(
llm: Union[
Runnable[Sequence[AnyMessage], AIMessage],
Runnable[Sequence[BaseMessage], BaseMessage],
],
*,
validator: ValidationNode,
retry_strategy: RetryStrategy,
tool_choice: Optional[str] = None,
) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:
"""Binds a tool validators + retry logic to create a runnable validation graph.
LLMs that support tool calling can generate structured JSON. However, they may not always
perfectly follow your requested schema, especially if the schema is nested or has complex
validation rules. This method allows you to bind a validation function to the LLM's output,
so that any time the LLM generates a message, the validation function is run on it. If
the validation fails, the method will retry the LLM with a fallback strategy, the simplest
being just to add a message to the output with the validation errors and a request to fix them.
The resulting runnable expects a list of messages as input and returns a single AI message.
By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into
your existing chat bot. You can specify a tool_choice to force the validator to be run on
the outputs.
Args:
llm (Runnable): The llm that will generate the initial messages (and optionally fallba)
validator (ValidationNode): The validation logic.
retry_strategy (RetryStrategy): The retry strategy to use.
Possible keys:
- max_attempts: The maximum number of attempts to make.
- fallback: The LLM or function to use in case of validation failure.
- aggregate_messages: A function to aggregate the messages over multiple turns.
Defaults to fetching the last AI message.
tool_choice: If provided, always run the validator on the tool output.
Returns:
Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.
"""
def add_or_overwrite_messages(left: list, right: Union[list, dict]) -> list:
"""Append messages. If the update is a 'finalized' output, replace the whole list."""
if isinstance(right, dict) and "finalize" in right:
finalized = right["finalize"]
if not isinstance(finalized, list):
finalized = [finalized]
for m in finalized:
if m.id is None:
m.id = str(uuid.uuid4())
return finalized
res = add_messages(left, right)
if not isinstance(res, list):
return [res]
return res
class State(TypedDict):
messages: Annotated[list, add_or_overwrite_messages]
attempt_number: Annotated[int, operator.add]
initial_num_messages: int
input_format: Literal["list", "dict"]
builder = StateGraph(State)
def dedict(x: State) -> list:
"""Get the messages from the state."""
return x["messages"]
model = dedict | llm | (lambda msg: {"messages": [msg], "attempt_number": 1})
fbrunnable = retry_strategy.get("fallback")
if fbrunnable is None:
fb_runnable = llm
elif isinstance(fbrunnable, Runnable):
fb_runnable = fbrunnable # type: ignore
else:
fb_runnable = RunnableLambda(fbrunnable)
fallback = (
dedict | fb_runnable | (lambda msg: {"messages": [msg], "attempt_number": 1})
)
def count_messages(state: State) -> dict:
return {"initial_num_messages": len(state.get("messages", []))}
builder.add_node("count_messages", count_messages)
builder.add_node("llm", model)
builder.add_node("fallback", fallback)
# To support patch-based retries, we need to be able to
# aggregate the messages over multiple turns.
# The next sequece selects only the relevant messages
# and then applies the validator
select_messages = retry_strategy.get("aggregate_messages") or _default_aggregator
def select_generated_messages(state: State) -> list:
"""Select only the messages generated within this loop."""
selected = state["messages"][state["initial_num_messages"] :]
return [select_messages(selected)]
def endict_validator_output(x: Sequence[AnyMessage]) -> dict:
if tool_choice and not x:
return {
"messages": [
HumanMessage(
content=f"ValidationError: please respond with a valid tool call [tool_choice={tool_choice}].",
additional_kwargs={"is_error": True},
)
]
}
return {"messages": x}
validator_runnable = select_generated_messages | validator | endict_validator_output
builder.add_node("validator", validator_runnable)
class Finalizer:
"""Pick the final message to return from the retry loop."""
def __init__(self, aggregator: Optional[Callable[[list], AIMessage]] = None):
self._aggregator = aggregator or _default_aggregator
def __call__(self, state: State) -> dict:
"""Return just the AI message."""
initial_num_messages = state["initial_num_messages"]
generated_messages = state["messages"][initial_num_messages:]
return {
"messages": {
"finalize": self._aggregator(generated_messages),
}
}
# We only want to emit the final message
builder.add_node("finalizer", Finalizer(retry_strategy.get("aggregate_messages")))
# Define the connectivity
builder.set_entry_point("count_messages")
builder.add_edge("count_messages", "llm")
def route_validator(state: State) -> Literal["validator", "__end__"]:
if state["messages"][-1].tool_calls or tool_choice is not None:
return "validator"
return "__end__"
builder.add_conditional_edges("llm", route_validator)
builder.add_edge("fallback", "validator")
max_attempts = retry_strategy.get("max_attempts", 3)
def route_validation(state: State) -> Literal["finalizer", "fallback"]:
if state["attempt_number"] > max_attempts:
raise ValueError(
f"Could not extract a valid value in {max_attempts} attempts."
)
for m in state["messages"][::-1]:
if m.type == "ai":
break
if m.additional_kwargs.get("is_error"):
return "fallback"
return "finalizer"
builder.add_conditional_edges("validator", route_validation)
builder.set_finish_point("finalizer")
# These functions let the step be used in a MessageGraph
# or a StateGraph with 'messages' as the key.
def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:
"""Ensure the input is the correct format."""
if isinstance(x, PromptValue):
return {"messages": x.to_messages(), "input_format": "list"}
if isinstance(x, list):
return {"messages": x, "input_format": "list"}
raise ValueError(f"Unexpected input type: {type(x)}")
def decode(x: State) -> AIMessage:
"""Ensure the output is in the expected format."""
return x["messages"][-1]
return (
encode | builder.compile().with_config(run_name="ValidationGraph") | decode
).with_config(run_name="ValidateWithRetries")
def bind_validator_with_retries(
llm: BaseChatModel,
*,
tools: list,
tool_choice: Optional[str] = None,
max_attempts: int = 3,
) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:
"""Binds validators + retry logic ensure validity of generated tool calls.
LLMs that support tool calling are good at generating structured JSON. However, they may
not always perfectly follow your requested schema, especially if the schema is nested or
has complex validation rules. This method allows you to bind a validation function to
the LLM's output, so that any time the LLM generates a message, the validation function
is run on it. If the validation fails, the method will retry the LLM with a fallback
strategy, the simples being just to add a message to the output with the validation
errors and a request to fix them.
The resulting runnable expects a list of messages as input and returns a single AI message.
By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into
your existing chat bot. You can specify a tool_choice to force the validator to be run on
the outputs.
Args:
llm (Runnable): The llm that will generate the initial messages (and optionally fallba)
validator (ValidationNode): The validation logic.
retry_strategy (RetryStrategy): The retry strategy to use.
Possible keys:
- max_attempts: The maximum number of attempts to make.
- fallback: The LLM or function to use in case of validation failure.
- aggregate_messages: A function to aggregate the messages over multiple turns.
Defaults to fetching the last AI message.
tool_choice: If provided, always run the validator on the tool output.
Returns:
Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.
"""
bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)
retry_strategy = RetryStrategy(max_attempts=max_attempts)
validator = ValidationNode(tools)
return _bind_validator_with_retries(
bound_llm,
validator=validator,
tool_choice=tool_choice,
retry_strategy=retry_strategy,
).with_config(metadata={"retry_strategy": "default"})In [55]:
from langchain_core.pydantic_v1 import BaseModel, Field, validator
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo")
class Respond(BaseModel):
"""Use to generate the response. Always use when responding to the user"""
reason: str = Field(description="Step-by-step justification for the answer.")
answer: str
@validator("answer")
def reason_contains_apology(cls, answer: str):
if "llama" not in answer.lower():
raise ValueError(
f"You MUST start with a gimicky, rhyming advertisement for using a Llama V3 (an LLM) in your **answer** field."
" Must be an instant hit. Must be weaved into the answer."
)
tools = [Respond]In [56]:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Or you can use ChatGroq, ChatAnthropic, ChatGoogleGemini, ChatCohere, etc.
# See https://python.langchain.com/v0.1/docs/integrations/chat/ for more info on tool calling
llm = ChatOpenAI(model="gpt-4-turbo")
bound_llm = bind_validator_with_retries(llm, tools=tools, tool_choice=Respond.__name__)
prompt = ChatPromptTemplate.from_messages(
[
("system", "Respond directly by calling the Respond function."),
("placeholder", "{messages}"),
]
)
chain = prompt | bound_llmIn [58]:
results = chain.invoke({"messages": [("user", "Does P = NP?")]})
results.pretty_print()==================================[1m Ai Message [0m================================== Tool Calls: Respond (call_TcEFGur9ygpLbrEUQA24MraI) Call ID: call_TcEFGur9ygpLbrEUQA24MraI Args: reason: The question of whether P equals NP is one of the most significant unsolved problems in computer science. This question asks whether every problem whose solution can be quickly verified by a computer can also be quickly solved by a computer. To date, no one has been able to prove definitively whether P equals NP or not, and it remains an open problem. answer: Need an answer pronto? Consult a Llama V3 for a response that's key. As for P = NP, it's a mystery, a riddle unsolved in computer science history!
In [59]:
from typing import List, Optional
class OutputFormat(BaseModel):
sources: str = Field(
...,
description="The raw transcript / span you could cite to justify the choice.",
)
content: str = Field(..., description="The chosen value.")
class Moment(BaseModel):
quote: str = Field(..., description="The relevant quote from the transcript.")
description: str = Field(..., description="A description of the moment.")
expressed_preference: OutputFormat = Field(
..., description="The preference expressed in the moment."
)
class BackgroundInfo(BaseModel):
factoid: OutputFormat = Field(
..., description="Important factoid about the member."
)
professions: list
why: str = Field(..., description="Why this is important.")
class KeyMoments(BaseModel):
topic: str = Field(..., description="The topic of the key moments.")
happy_moments: List[Moment] = Field(
..., description="A list of key moments related to the topic."
)
tense_moments: List[Moment] = Field(
..., description="Moments where things were a bit tense."
)
sad_moments: List[Moment] = Field(
..., description="Moments where things where everyone was downtrodden."
)
background_info: list[BackgroundInfo]
moments_summary: str = Field(..., description="A summary of the key moments.")
class Member(BaseModel):
name: OutputFormat = Field(..., description="The name of the member.")
role: Optional[str] = Field(None, description="The role of the member.")
age: Optional[int] = Field(None, description="The age of the member.")
background_details: List[BackgroundInfo] = Field(
..., description="A list of background details about the member."
)
class InsightfulQuote(BaseModel):
quote: OutputFormat = Field(
..., description="An insightful quote from the transcript."
)
speaker: str = Field(..., description="The name of the speaker who said the quote.")
analysis: str = Field(
..., description="An analysis of the quote and its significance."
)
class TranscriptMetadata(BaseModel):
title: str = Field(..., description="The title of the transcript.")
location: OutputFormat = Field(
..., description="The location where the interview took place."
)
duration: str = Field(..., description="The duration of the interview.")
class TranscriptSummary(BaseModel):
metadata: TranscriptMetadata = Field(
..., description="Metadata about the transcript."
)
participants: List[Member] = Field(
..., description="A list of participants in the interview."
)
key_moments: List[KeyMoments] = Field(
..., description="A list of key moments from the interview."
)
insightful_quotes: List[InsightfulQuote] = Field(
..., description="A list of insightful quotes from the interview."
)
overall_summary: str = Field(
..., description="An overall summary of the interview."
)
next_steps: List[str] = Field(
..., description="A list of next steps or action items based on the interview."
)
other_stuff: List[OutputFormat]In [60]:
transcript = [
(
"Pete",
"Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.",
),
(
"Xu",
"No problem. As its my job, I've got some thoughts on this beef.",
),
(
"Laura",
"Yeah, I've got some insider info so this should be interesting.",
),
("Pete", "Dope. So, when do you think this whole thing started?"),
(
"Pete",
"Definitely was Kendrick's 'Control' verse that kicked it off.",
),
(
"Laura",
"Truth, but Drake never went after him directly. Just some subtle jabs here and there.",
),
(
"Xu",
"That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.",
),
(
"Pete",
"For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.",
),
(
"Laura",
"I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.",
),
(
"Pete",
"I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.",
),
(
"Xu",
"It's wild how this beef is shaping fans.",
),
("Pete", "do you think these beefs can actually be good for hip-hop?"),
(
"Xu",
"Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.",
),
("Laura", "eh"),
("Pete", "So, where do you see this beef going?"),
(
"Laura",
"Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.",
),
("Laura", "ehhhhhh not sure"),
(
"Pete",
"I feel that. I just want both of them to keep dropping heat, beef or no beef.",
),
(
"Xu",
"I'm curious. May influence a lot of people. Make things more competitive. Bring on a whole new wave of lyricism.",
),
(
"Pete",
"Word. Hey, thanks for chopping it up with me, Xu and Laura. This was dope.",
),
("Xu", "Where are you going so fast?"),
(
"Laura",
"For real, I had a good time. Nice to get different perspectives on the situation.",
),
]
formatted = "\n".join(f"{x[0]}: {x[1]}" for x in transcript)In [61]:
tools = [TranscriptSummary]
bound_llm = bind_validator_with_retries(
llm, tools=tools, tool_choice=TranscriptSummary.__name__
)
prompt = ChatPromptTemplate.from_messages(
[
("system", "Respond directly using the TranscriptSummary function."),
("placeholder", "{messages}"),
]
)
chain = prompt | bound_llm
results = chain.invoke(
{
"messages": [
(
"user",
f"Extract the summary from the following conversation:\n\n<convo>\n{formatted}\n</convo>"
"\n\nRemember to respond using the TranscriptSummary function.",
)
]
},
)
results.pretty_print()==================================[1m Ai Message [0m================================== Tool Calls: TranscriptSummary (call_FEhm49kk06xCQpG4PodvD6YC) Call ID: call_FEhm49kk06xCQpG4PodvD6YC Args: metadata: {'title': "Discussion on Drake and Kendrick's Rivalry", 'location': {'sources': '<convo>', 'content': 'Video Call'}, 'duration': 'Approximately 10 minutes'} participants: [{'name': {'sources': '<convo>', 'content': 'Pete'}, 'background_details': [{'factoid': {'sources': '<convo>', 'content': 'Host of the call'}, 'professions': [], 'why': 'Shows initiative and interest in the topic.'}]}, {'name': {'sources': '<convo>', 'content': 'Xu'}, 'background_details': [{'factoid': {'sources': '<convo>', 'content': 'Music industry professional'}, 'professions': [], 'why': 'Brings expert insights into the discussion.'}]}, {'name': {'sources': '<convo>', 'content': 'Laura'}, 'background_details': [{'factoid': {'sources': '<convo>', 'content': 'Has insider information'}, 'professions': [], 'why': 'Adds depth to the discussion with exclusive information.'}]}] key_moments: [{'topic': 'Origin and Impact of the Rivalry', 'happy_moments': [], 'tense_moments': [], 'sad_moments': [], 'background_info': [], 'moments_summary': 'The conversation highlighted the origins and impacts of the rivalry between Drake and Kendrick Lamar, focusing on their different approaches to music and fanbase reactions.'}] insightful_quotes: [{'quote': {'sources': '<convo>', 'content': "When it's done right, a beef can push the genre forward and make artists level up."}, 'speaker': 'Xu', 'analysis': 'Xu emphasizes the potential positive effects of musical rivalries on the development of hip-hop.'}] overall_summary: The conversation delved into the rivalry between Drake and Kendrick Lamar, discussing its origins, fan reactions, and potential impacts on hip-hop. The participants agreed that while the rivalry is subtle, it encourages competition and artistic development. next_steps: ['Monitor any new developments in the rivalry.', 'Discuss potential impacts on hip-hop in future conversations.'] other_stuff: []
In [64]:
%%capture --no-stderr
%pip install -U jsonpatchIn [65]:
def bind_validator_with_jsonpatch_retries(
llm: BaseChatModel,
*,
tools: list,
tool_choice: Optional[str] = None,
max_attempts: int = 3,
) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:
"""Binds validators + retry logic ensure validity of generated tool calls.
This method is similar to `bind_validator_with_retries`, but uses JSONPatch to correct
validation errors caused by passing in incorrect or incomplete parameters in a previous
tool call. This method requires the 'jsonpatch' library to be installed.
Using patch-based function healing can be more efficient than repopulating the entire
tool call from scratch, and it can be an easier task for the LLM to perform, since it typically
only requires a few small changes to the existing tool call.
Args:
llm (Runnable): The llm that will generate the initial messages (and optionally fallba)
tools (list): The tools to bind to the LLM.
tool_choice (Optional[str]): The tool choice to use.
max_attempts (int): The number of attempts to make.
Returns:
Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.
"""
try:
import jsonpatch # type: ignore[import-untyped]
except ImportError:
raise ImportError(
"The 'jsonpatch' library is required for JSONPatch-based retries."
" Please install it with 'pip install -U jsonpatch'."
)
class JsonPatch(BaseModel):
"""A JSON Patch document represents an operation to be performed on a JSON document.
Note that the op and path are ALWAYS required. Value is required for ALL operations except 'remove'.
Examples:
```json
{"op": "add", "path": "/a/b/c", "patch_value": 1}
{"op": "replace", "path": "/a/b/c", "patch_value": 2}
{"op": "remove", "path": "/a/b/c"}
```
"""
op: Literal["add", "remove", "replace"] = Field(
...,
description="The operation to be performed. Must be one of 'add', 'remove', 'replace'.",
)
path: str = Field(
...,
description="A JSON Pointer path that references a location within the target document where the operation is performed.",
)
value: Any = Field(
...,
description="The value to be used within the operation. REQUIRED for 'add', 'replace', and 'test' operations.",
)
class PatchFunctionParameters(BaseModel):
"""Respond with all JSONPatch operation to correct validation errors caused by passing in incorrect or incomplete parameters in a previous tool call."""
tool_call_id: str = Field(
...,
description="The ID of the tool call that generated the error.",
)
reasoning: str = Field(
...,
description="Think step-by-step, listing each validation error and the"
" JSONPatch operation needed to correct it. "
"Cite the fields in the JSONSchema you referenced in developing this plan.",
)
patches: list[JsonPatch] = Field(
...,
description="A list of JSONPatch operations to be applied to the previous tool call's response.",
)
bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)
fallback_llm = llm.bind_tools(
[PatchFunctionParameters], tool_choice=PatchFunctionParameters.__name__
)
def aggregate_messages(messages: Sequence[AnyMessage]) -> AIMessage:
# Get all the AI messages and apply json patches
resolved_tool_calls: Dict[Union[str, None], ToolCall] = {}
content: Union[str, List[Union[str, dict]]] = ""
for m in messages:
if m.type != "ai":
continue
if not content:
content = m.content
for tc in m.tool_calls:
if tc["name"] == JsonPatch.__name__:
if tc["args"]["tool_call_id"] not in resolved_tool_calls:
raise ValueError(
f"JsonPatch tool call ID {tc['args']['tool_call_id']} not found."
f"Valid tool call IDs: {list(resolved_tool_calls.keys())}"
)
current_args = resolved_tool_calls[tc["args"]["tool_call_id"]][
"args"
]
patches = tc["args"]["patches"]
resolved_tool_calls[tc["args"]["tool_call_id"]][
"args"
] = jsonpatch.apply_patch(
current_args,
patches,
)
else:
resolved_tool_calls[tc["id"]] = tc.copy()
return AIMessage(
content=content,
tool_calls=list(resolved_tool_calls.values()),
)
def format_exception(error: BaseException, call: ToolCall, schema: Type[BaseModel]):
return (
f"Error:\n\n```\n{repr(error)}\n```\n"
"Expected Parameter Schema:\n\n" + f"```json\n{schema.schema_json()}\n```\n"
f"Please respond with a JSONPatch to correct the error for tool_call_id=[{call['id']}]."
)
validator = ValidationNode(
tools,
format_error=format_exception,
)
retry_strategy = RetryStrategy(
max_attempts=max_attempts,
fallback=fallback_llm,
aggregate_messages=aggregate_messages,
)
return _bind_validator_with_retries(
bound_llm,
validator=validator,
retry_strategy=retry_strategy,
tool_choice=tool_choice,
).with_config(metadata={"retry_strategy": "jsonpatch"})In [71]:
bound_llm = bind_validator_with_jsonpatch_retries(
llm, tools=tools, tool_choice=tools[0].__name__
)In [72]:
from IPython.display import Image, display
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except:
passIn [75]:
chain = prompt | bound_llm
results = chain.invoke(
{
"messages": [
(
"user",
f"Extract the summary from the following conversation:\n\n<convo>\n{formatted}\n</convo>",
)
]
},
)
results.pretty_print()==================================[1m Ai Message [0m================================== Tool Calls: TranscriptSummary (call_RrbvdJMt4T2xbOyqr74oF7Dd) Call ID: call_RrbvdJMt4T2xbOyqr74oF7Dd Args: metadata: {'title': 'Discussion on Drake and Kendrick Beef', 'location': {'sources': "<convo>Pete: Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.</convo>", 'content': 'Video Call'}, 'duration': 'Not specified'} participants: [{'name': {'sources': '<convo>Pete: Hey Xu, Laura, thanks for hopping on this call.</convo>', 'content': 'Pete'}, 'background_details': [{'factoid': {'sources': "<convo>Pete: Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.</convo>", 'content': 'Interested in discussing artist rivalries'}, 'professions': [], 'why': 'Sets the topic of discussion'}]}, {'name': {'sources': "<convo>Xu: No problem. As its my job, I've got some thoughts on this beef.</convo>", 'content': 'Xu'}, 'background_details': [{'factoid': {'sources': "<convo>Xu: No problem. As its my job, I've got some thoughts on this beef.</convo>", 'content': 'Professional insight into artist rivalries'}, 'professions': [], 'why': 'Provides expert opinion'}]}, {'name': {'sources': "<convo>Laura: Yeah, I've got some insider info so this should be interesting.</convo>", 'content': 'Laura'}, 'background_details': [{'factoid': {'sources': "<convo>Laura: Yeah, I've got some insider info so this should be interesting.</convo>", 'content': 'Has insider information'}, 'professions': [], 'why': 'Adds depth to the discussion'}]}] key_moments: [{'topic': 'Origin and Dynamics of the Beef', 'happy_moments': [{'quote': "Pete: Definitely was Kendrick's 'Control' verse that kicked it off.", 'description': 'Identifying the start of the beef between Drake and Kendrick.', 'expressed_preference': {'sources': "<convo>Pete: Definitely was Kendrick's 'Control' verse that kicked it off.</convo>", 'content': "Kendrick's 'Control' verse started it"}}, {'quote': 'Laura: Truth, but Drake never went after him directly. Just some subtle jabs here and there.', 'description': 'Discussing how Drake approached the beef.', 'expressed_preference': {'sources': '<convo>Laura: Truth, but Drake never went after him directly. Just some subtle jabs here and there.</convo>', 'content': "Drake's subtle approach"}}, {'quote': "Xu: That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.", 'description': 'Analyzing the impact of beefs on artists.', 'expressed_preference': {'sources': "<convo>Xu: That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.</convo>", 'content': 'Beefs push artists'}}, {'quote': "Pete: For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.", 'description': 'Highlighting fan reactions and preferences.', 'expressed_preference': {'sources': "<convo>Pete: For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.</convo>", 'content': 'Fan preferences'}}, {'quote': "Pete: I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.", 'description': "Pete expressing his preference for Kendrick's skills.", 'expressed_preference': {'sources': "<convo>Pete: I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.</convo>", 'content': 'Preference for Kendrick'}}, {'quote': "Laura: I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.", 'description': "Laura mentioning Drake's ability to create popular hits.", 'expressed_preference': {'sources': "<convo>Laura: I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.</convo>", 'content': "Drake's hit-making ability"}}], 'tense_moments': [], 'sad_moments': [], 'background_info': [], 'moments_summary': "The conversation focused on the origins and dynamics of the beef between Drake and Kendrick, discussing how it started with Kendrick's 'Control' verse and evolved with subtle jabs from Drake. The participants analyzed the impact of the beef on the artists and their fanbases, highlighting how it pushes artists to excel and divides fans based on their preferences for mainstream appeal versus lyrical skill."}] insightful_quotes: [{'quote': {'sources': "<convo>Xu: Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.</convo>", 'content': "When it's done right, a beef can push the genre forward and make artists level up."}, 'speaker': 'Xu', 'analysis': 'Xu highlights the potential positive impact of artist rivalries on the evolution of the music genre, suggesting that when managed correctly, these beefs can lead to significant artistic growth and innovation.'}, {'quote': {'sources': "<convo>Laura: Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.</convo>", 'content': "Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate."}, 'speaker': 'Laura', 'analysis': 'Laura speculates that the beef will remain a topic of interest among fans but believes it will not escalate further without more direct confrontations in the form of diss tracks.'}] overall_summary: The conversation between Pete, Xu, and Laura centered on the ongoing beef between Drake and Kendrick Lamar, exploring its origins, dynamics, and impact on both the artists and their fanbases. The discussion highlighted the role of artist rivalries in pushing musical boundaries and enhancing fan engagement, while also acknowledging the potential for escalation if more direct actions are taken. The participants shared their insights and preferences, contributing to a multifaceted understanding of the situation. next_steps: ['Continue monitoring the situation for any new developments in the beef between Drake and Kendrick.', 'Discuss further with other industry experts to gather more perspectives on the impact of such rivalries.'] other_stuff: []
In [ ]: