langgraph: remove FewShotExamples managed value (#1195)

This commit is contained in:
Vadym Barda
2024-08-02 11:05:17 -04:00
committed by GitHub
parent 487157eafa
commit 4d7a42a65e
4 changed files with 0 additions and 993 deletions
File diff suppressed because one or more lines are too long
@@ -1,105 +0,0 @@
from contextlib import asynccontextmanager, contextmanager
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
AsyncIterator,
Callable,
Dict,
Generator,
Generic,
Iterator,
Optional,
Sequence,
Union,
)
from langchain_core.runnables import RunnableConfig
from typing_extensions import Self
from langgraph.channels.manager import AsyncChannelsManager, ChannelsManager
from langgraph.managed.base import ConfiguredManagedValue, ManagedValue, V
from langgraph.pregel import Pregel
from langgraph.pregel.io import read_channels
from langgraph.pregel.types import PregelTaskDescription
if TYPE_CHECKING:
from langgraph.pregel import Pregel
# Metadata filter can be a dict (static) or a function (dynamic) that takes a
# RunnableConfig and returns a dict. Functions are used for filtering on
# metadata values that are only available at runtime.
MetadataFilter = Union[Dict[str, Any], Callable[[RunnableConfig], Dict[str, Any]]]
class FewShotExamples(ManagedValue[Sequence[V]], Generic[V]):
examples: list[V]
def __init__(
self,
config: RunnableConfig,
graph: Pregel,
k: int = 5,
metadata_filter: Optional[MetadataFilter] = None,
) -> None:
super().__init__(config, graph)
self.k = k
self.metadata_filter = metadata_filter or {}
@classmethod
def configure(
cls, k: int = 5, metadata_filter: Optional[MetadataFilter] = None
) -> ConfiguredManagedValue:
return ConfiguredManagedValue(
cls,
{
"k": k,
"metadata_filter": metadata_filter,
},
)
@property
def metadata_filter_dict(self) -> Dict[str, Any]:
if isinstance(self.metadata_filter, Callable):
return self.metadata_filter(self.config)
else:
return self.metadata_filter
def iter(self, score: int = 1) -> Iterator[V]:
for example in self.graph.checkpointer.list(
None, filter={"score": score, **self.metadata_filter_dict}, limit=self.k
):
with ChannelsManager(
self.graph.channels, example.checkpoint, self.config
) as channels:
yield read_channels(channels, self.graph.output_channels)
async def aiter(self, score: int = 1) -> AsyncIterator[V]:
async for example in self.graph.checkpointer.alist(
None, filter={"score": score, **self.metadata_filter_dict}, limit=self.k
):
async with AsyncChannelsManager(
self.graph.channels, example.checkpoint, self.config
) as channels:
yield read_channels(channels, self.graph.output_channels)
@classmethod
@contextmanager
def enter(
cls, config: RunnableConfig, graph: "Pregel", **kwargs: Any
) -> Generator[Self, None, None]:
with super().enter(config, graph, **kwargs) as value:
value.examples = list(value.iter())
yield value
@classmethod
@asynccontextmanager
async def aenter(
cls, config: RunnableConfig, graph: "Pregel", **kwargs: Any
) -> AsyncGenerator[Self, None]:
async with super().aenter(config, graph, **kwargs) as value:
value.examples = [e async for e in value.aiter()]
yield value
def __call__(self, step: int, task: PregelTaskDescription) -> Sequence[V]:
return self.examples
-228
View File
@@ -54,7 +54,6 @@ from langgraph.graph import END, Graph
from langgraph.graph.graph import START
from langgraph.graph.message import MessageGraph, add_messages
from langgraph.graph.state import StateGraph
from langgraph.managed.few_shot import FewShotExamples
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
@@ -3394,233 +3393,6 @@ def test_state_graph_w_config(snapshot: SnapshotAssertion) -> None:
assert app.config_schema().schema_json() == snapshot
def test_state_graph_few_shot() -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import AIMessage, AnyMessage, ToolMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
def filter_by_source(config: RunnableConfig) -> Dict[str, Any]:
"""This function is a trivial example that demonstrates that passing
a Callable to metadata_filter works as expected.
"""
return {"source": "loop"}
class BaseState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
class AgentState(BaseState):
examples: Annotated[
Sequence[BaseState],
FewShotExamples[BaseState].configure(k=1, metadata_filter=filter_by_source),
]
# Assemble the tools
@tool()
def search_api(query: str) -> str:
"""Searches the API for the query."""
return f"result for {query}"
tools = [search_api]
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are a nice assistant.
Some examples of past conversations:
{examples}""",
),
("placeholder", "{messages}"),
]
)
model = FakeMessagesListChatModel(
responses=[
AIMessage(
content="",
tool_calls=[
{
"id": "tool_call123",
"name": "search_api",
"args": {"query": "query"},
},
],
),
AIMessage(content="answer"),
]
)
def agent(state: AgentState, config: RunnableConfig) -> AgentState:
# begin: testing code
assert state["examples"] == config["configurable"]["expected_examples"]
# end: testing code
formatted = prompt.invoke(state)
response = model.invoke(formatted)
return {"messages": response}
# Define decision-making logic
def should_continue(data: AgentState) -> str:
# Logic to decide whether to continue in the loop or exit
if not data["messages"][-1].tool_calls:
return "exit"
else:
return "continue"
# Define a new graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", agent)
workflow.add_node("tools", ToolNode(tools))
workflow.set_entry_point("agent")
workflow.add_conditional_edges(
"agent", should_continue, {"continue": "tools", "exit": END}
)
workflow.add_edge("tools", "agent")
with SqliteSaver.from_conn_string(":memory:") as saver:
app = workflow.compile(checkpointer=saver)
first_messages = [
_AnyIdHumanMessage(content="what is weather in sf"),
AIMessage(
content="",
id=AnyStr(),
tool_calls=[
{
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),
ToolMessage(
content="result for query",
name="search_api",
id=AnyStr(),
tool_call_id="tool_call123",
),
_AnyIdAIMessage(content="answer"),
]
actual = app.invoke(
{"messages": "what is weather in sf"},
{
"configurable": {
"thread_id": "1",
"expected_examples": [],
},
},
)
expected = {"messages": first_messages}
assert actual == expected
# get first checkpoint
chkpnt_tuple_1 = saver.get_tuple({"configurable": {"thread_id": "1"}})
config = chkpnt_tuple_1.config
checkpoint = chkpnt_tuple_1.checkpoint
metadata = chkpnt_tuple_1.metadata
# not needed in application code, only for testing
hiscored = list(saver.list(None, filter={"score": 1}))
assert hiscored == []
# mark as "good"
metadata["score"] = 1
saver.put(config, checkpoint, metadata)
# not needed in application code, only for testing
hiscored = list(saver.list(None, filter={"score": 1}))
assert len(hiscored) == 1
assert hiscored[0].checkpoint["channel_values"]["messages"] == first_messages
second_messages = [
_AnyIdHumanMessage(content="what is weather in la"),
AIMessage(
content="",
id=AnyStr(),
tool_calls=[
{
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),
ToolMessage(
content="result for query",
name="search_api",
id=AnyStr(),
tool_call_id="tool_call123",
),
_AnyIdAIMessage(content="answer"),
]
assert app.invoke(
{"messages": "what is weather in la"},
{
"configurable": {
"thread_id": "2",
# below is only for testing purposes, not part of few shot api
"expected_examples": [{"messages": first_messages}],
}
},
) == {"messages": second_messages}
# get first checkpoint
chkpnt_tuple_2 = saver.get_tuple({"configurable": {"thread_id": "2"}})
config = chkpnt_tuple_2.config
checkpoint = chkpnt_tuple_2.checkpoint
metadata = chkpnt_tuple_2.metadata
# not needed in application code, only for testing
hiscored = list(saver.list(None, filter={"score": 1}))
assert len(hiscored) == 1
# mark as "good"
metadata["score"] = 1
saver.put(config, checkpoint, metadata)
hiscored = list(saver.list(None, filter={"score": 1}))
assert len(hiscored) == 2
assert app.invoke(
{"messages": "what is weather in ny"},
{
"configurable": {
"thread_id": "3",
# below is only for testing purposes, not part of few shot api
"expected_examples": [{"messages": second_messages}],
}
},
) == {
"messages": [
_AnyIdHumanMessage(content="what is weather in ny"),
AIMessage(
content="",
id=AnyStr(),
tool_calls=[
{
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),
ToolMessage(
content="result for query",
name="search_api",
id=AnyStr(),
tool_call_id="tool_call123",
),
_AnyIdAIMessage(content="answer"),
]
}
def test_conditional_entrypoint_graph_state(snapshot: SnapshotAssertion) -> None:
class AgentState(TypedDict, total=False):
input: str
-183
View File
@@ -14,7 +14,6 @@ from typing import (
List,
Literal,
Optional,
Sequence,
Tuple,
TypedDict,
Union,
@@ -52,7 +51,6 @@ from langgraph.errors import InvalidUpdateError
from langgraph.graph import END, Graph, StateGraph
from langgraph.graph.graph import START
from langgraph.graph.message import MessageGraph, add_messages
from langgraph.managed.few_shot import FewShotExamples
from langgraph.prebuilt.chat_agent_executor import (
create_function_calling_executor,
create_tool_calling_executor,
@@ -3242,187 +3240,6 @@ async def test_conditional_graph_state() -> None:
)
async def test_state_graph_few_shot() -> None:
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
AnyMessage,
ToolCall,
ToolMessage,
)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
def filter_by_source(config: RunnableConfig) -> Dict[str, Any]:
"""This function is a trivial example that demonstrates that passing
a Callable to metadata_filter works as expected.
"""
return {"source": "loop"}
class BaseState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
class AgentState(BaseState):
examples: Annotated[
Sequence[BaseState],
FewShotExamples[BaseState].configure(k=1, metadata_filter=filter_by_source),
]
# Assemble the tools
@tool()
def search_api(query: str) -> str:
"""Searches the API for the query."""
return f"result for {query}"
tools = [search_api]
tools_by_name = {t.name: t for t in tools}
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are a nice assistant.
Some examples of past conversations:
{examples}""",
),
("placeholder", "{messages}"),
]
)
model = FakeMessagesListChatModel(
responses=[
AIMessage(
content="",
tool_calls=[
{
"id": "tool_call123",
"name": "search_api",
"args": {"query": "query"},
},
],
),
AIMessage(content="answer"),
]
)
async def agent(state: AgentState, config: RunnableConfig) -> AgentState:
# begin: testing code
assert state["examples"] == config["configurable"]["expected_examples"]
# end: testing code
formatted = await prompt.ainvoke(state)
response = await model.ainvoke(formatted)
return {"messages": response}
# Define decision-making logic
def should_continue(data: AgentState) -> str:
# Logic to decide whether to continue in the loop or exit
if tool_calls := data["messages"][-1].tool_calls:
return [Send("tools", tool_call) for tool_call in tool_calls]
else:
return "exit"
def tools_node(tool_call: ToolCall, config: RunnableConfig) -> AgentState:
output = tools_by_name[tool_call["name"]].invoke(tool_call["args"], config)
return {
"messages": ToolMessage(
content=output, name=tool_call["name"], tool_call_id=tool_call["id"]
)
}
# Define a new graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", agent)
workflow.add_node("tools", tools_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges(
"agent", should_continue, {"continue": "tools", "exit": END}
)
workflow.add_edge("tools", "agent")
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
app = workflow.compile(checkpointer=saver)
first_messages = [
_AnyIdHumanMessage(content="what is weather in sf"),
AIMessage(
content="",
id=AnyStr(),
tool_calls=[
{
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
}
],
),
ToolMessage(
content="result for query",
name="search_api",
id=AnyStr(),
tool_call_id="tool_call123",
),
_AnyIdAIMessage(content="answer"),
]
assert await app.ainvoke(
{"messages": "what is weather in sf"},
{"configurable": {"thread_id": "1", "expected_examples": []}},
) == {"messages": first_messages}
# get first checkpoint
chkpnt_tuple_1 = await saver.aget_tuple({"configurable": {"thread_id": "1"}})
config = chkpnt_tuple_1.config
checkpoint = chkpnt_tuple_1.checkpoint
metadata = chkpnt_tuple_1.metadata
# not needed in application code, only for testing
assert [c async for c in saver.alist(None, filter={"score": 1})] == []
# mark as "good"
metadata["score"] = 1
await saver.aput(config, checkpoint, metadata)
# not needed in application code, only for testing
hiscored = [c async for c in saver.alist(None, filter={"score": 1})]
assert len(hiscored) == 1
assert hiscored[0].checkpoint["channel_values"]["messages"] == first_messages
assert await app.ainvoke(
{"messages": "what is weather in la"},
{
"configurable": {
"thread_id": "2",
# below is only for testing purposes, not part of few shot api
"expected_examples": [{"messages": first_messages}],
}
},
) == {
"messages": [
_AnyIdHumanMessage(content="what is weather in la"),
AIMessage(
content="",
id=AnyStr(),
tool_calls=[
{
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
}
],
),
ToolMessage(
content="result for query",
name="search_api",
id=AnyStr(),
tool_call_id="tool_call123",
),
_AnyIdAIMessage(content="answer"),
]
}
async def test_conditional_entrypoint_graph() -> None:
async def left(data: str) -> str:
return data + "->left"