mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
388 KiB
388 KiB
In [1]:
%%capture --no-stderr
%pip install -U langgraphIn [2]:
from typing import TypedDict, Optional, Annotated
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
# The structure of the logs
class Logs(TypedDict):
id: str
question: str
answer: str
grade: Optional[int]
feedback: Optional[str]
# Define custom reducer (see more on this in the "Custom reducer" section below)
def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:
if not left:
left = []
if not right:
right = []
logs = left.copy()
left_id_to_idx = {log["id"]: idx for idx, log in enumerate(logs)}
# update if the new logs are already in the state, otherwise append
for log in right:
idx = left_id_to_idx.get(log["id"])
if idx is not None:
logs[idx] = log
else:
logs.append(log)
return logs
# Failure Analysis Subgraph
class FailureAnalysisState(TypedDict):
# keys shared with the parent graph (EntryGraphState)
logs: Annotated[list[Logs], add_logs]
failure_report: str
# subgraph key
failures: list[Logs]
def get_failures(state: FailureAnalysisState):
failures = [log for log in state["logs"] if log["grade"] == 0]
return {"failures": failures}
def generate_summary(state: FailureAnalysisState):
failures = state["failures"]
# NOTE: you can implement custom summarization logic here
failure_ids = [log["id"] for log in failures]
fa_summary = f"Poor quality of retrieval for document IDs: {', '.join(failure_ids)}"
return {"failure_report": fa_summary}
fa_builder = StateGraph(FailureAnalysisState)
fa_builder.add_node("get_failures", get_failures)
fa_builder.add_node("generate_summary", generate_summary)
fa_builder.add_edge(START, "get_failures")
fa_builder.add_edge("get_failures", "generate_summary")
fa_builder.add_edge("generate_summary", END)
# Summarization subgraph
class QuestionSummarizationState(TypedDict):
# keys that are shared with the parent graph (EntryGraphState)
summary_report: str
logs: Annotated[list[Logs], add_logs]
# subgraph keys
summary: str
def generate_summary(state: QuestionSummarizationState):
docs = state["logs"]
# NOTE: you can implement custom summarization logic here
summary = "Questions focused on usage of ChatOllama and Chroma vector store."
return {"summary": summary}
def send_to_slack(state: QuestionSummarizationState):
summary = state["summary"]
# NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack
return {"summary_report": summary}
qs_builder = StateGraph(QuestionSummarizationState)
qs_builder.add_node("generate_summary", generate_summary)
qs_builder.add_node("send_to_slack", send_to_slack)
qs_builder.add_edge(START, "generate_summary")
qs_builder.add_edge("generate_summary", "send_to_slack")
qs_builder.add_edge("send_to_slack", END)In [3]:
# Dummy logs
dummy_logs = [
Logs(
id="1",
question="How can I import ChatOllama?",
grade=1,
answer="To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'",
),
Logs(
id="2",
question="How can I use Chroma vector store?",
answer="To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).",
grade=0,
feedback="The retrieved documents discuss vector stores in general, but not Chroma specifically",
),
Logs(
id="3",
question="How do I create react agent in langgraph?",
answer="from langgraph.prebuilt import create_react_agent",
)
]
# Entry Graph
class EntryGraphState(TypedDict):
raw_logs: Annotated[list[Logs], add_logs]
logs: Annotated[list[Logs], add_logs] # This will be used in subgraphs
failure_report: str # This will be generated in the FA subgraph
summary_report: str # This will be generated in the QS subgraph
def select_logs(state):
return {"logs": [log for log in state["raw_logs"] if "grade" in log]}
entry_builder = StateGraph(EntryGraphState)
entry_builder.add_node("select_logs", select_logs)
entry_builder.add_node("question_summarization", qs_builder.compile())
entry_builder.add_node("failure_analysis", fa_builder.compile())
entry_builder.add_edge(START, "select_logs")
entry_builder.add_edge("select_logs", "failure_analysis")
entry_builder.add_edge("select_logs", "question_summarization")
entry_builder.add_edge("failure_analysis", END)
entry_builder.add_edge("question_summarization", END)
graph = entry_builder.compile()
from IPython.display import Image, display
# Setting xray to 1 will show the internal structure of the nested graph
display(Image(graph.get_graph(xray=1).draw_mermaid_png()))In [4]:
graph.invoke({"raw_logs": dummy_logs}, debug=False)Out [4]:
{'raw_logs': [{'id': '1',
'question': 'How can I import ChatOllama?',
'grade': 1,
'answer': "To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'"},
{'id': '2',
'question': 'How can I use Chroma vector store?',
'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',
'grade': 0,
'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},
{'id': '3',
'question': 'How do I create react agent in langgraph?',
'answer': 'from langgraph.prebuilt import create_react_agent'}],
'logs': [{'id': '1',
'question': 'How can I import ChatOllama?',
'grade': 1,
'answer': "To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'"},
{'id': '2',
'question': 'How can I use Chroma vector store?',
'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',
'grade': 0,
'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],
'failure_report': 'Poor quality of retrieval for document IDs: 2',
'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}In [5]:
from typing import Annotated
from typing_extensions import TypedDict
# define a simple reducer
def reduce_list(left: list, right: list) -> list:
if not left:
left = []
if not right:
right = []
return left + right
# define parent and child state
class ChildState(TypedDict):
name: str
path: Annotated[list[str], reduce_list]
class ParentState(TypedDict):
name: str
path: Annotated[list[str], reduce_list]
# define a helper to build the graph
def make_graph(parent_schema, child_schema):
child_builder = StateGraph(child_schema)
child_builder.add_node("child_start", lambda state: {"path": ["child_start"]})
child_builder.add_edge(START, "child_start")
child_builder.add_node("child_middle", lambda state: {"path": ["child_middle"]})
child_builder.add_node("child_end", lambda state: {"path": ["child_end"]})
child_builder.add_edge("child_start", "child_middle")
child_builder.add_edge("child_middle", "child_end")
child_builder.add_edge("child_end", END)
builder = StateGraph(parent_schema)
builder.add_node("grandparent", lambda state: {"path": ["grandparent"]})
builder.add_edge(START, "grandparent")
builder.add_node("parent", lambda state: {"path": ["parent"]})
builder.add_node("child", child_builder.compile())
builder.add_node("sibling", lambda state: {"path": ["sibling"]})
builder.add_node("fin", lambda state: {"path": ["fin"]})
# Add connections
builder.add_edge("grandparent", "parent")
builder.add_edge("parent", "child")
builder.add_edge("parent", "sibling")
builder.add_edge("child", "fin")
builder.add_edge("sibling", "fin")
builder.add_edge("fin", END)
graph = builder.compile()
return graph
graph = make_graph(ParentState, ChildState)In [6]:
from IPython.display import Image, display
# Setting xray to 1 will show the internal structure of the nested graph
display(Image(graph.get_graph(xray=1).draw_mermaid_png()))In [7]:
graph.invoke({"name": "test"}, debug=True)Out [7]:
[36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'path': []} [36;1m[1;3m[0:tasks][0m [1mStarting step 0 with 1 task: [0m- [32;1m[1;3m__start__[0m -> {'name': 'test'} [36;1m[1;3m[0:writes][0m [1mFinished step 0 with writes to 1 channel: [0m- [33;1m[1;3mname[0m -> 'test' [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': []} [36;1m[1;3m[1:tasks][0m [1mStarting step 1 with 1 task: [0m- [32;1m[1;3mgrandparent[0m -> {'name': 'test', 'path': []} [36;1m[1;3m[1:writes][0m [1mFinished step 1 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['grandparent'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': ['grandparent']} [36;1m[1;3m[2:tasks][0m [1mStarting step 2 with 1 task: [0m- [32;1m[1;3mparent[0m -> {'name': 'test', 'path': ['grandparent']} [36;1m[1;3m[2:writes][0m [1mFinished step 2 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['parent'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': ['grandparent', 'parent']} [36;1m[1;3m[3:tasks][0m [1mStarting step 3 with 2 tasks: [0m- [32;1m[1;3mchild[0m -> {'name': 'test', 'path': ['grandparent', 'parent']} - [32;1m[1;3msibling[0m -> {'name': 'test', 'path': ['grandparent', 'parent']} [36;1m[1;3m[3:writes][0m [1mFinished step 3 with writes to 2 channels: [0m- [33;1m[1;3mname[0m -> 'test' - [33;1m[1;3mpath[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': ['grandparent', 'parent', 'grandparent', 'parent', 'child_start', 'child_middle', 'child_end', 'sibling']} [36;1m[1;3m[4:tasks][0m [1mStarting step 4 with 1 task: [0m- [32;1m[1;3mfin[0m -> {'name': 'test', 'path': ['grandparent', 'parent', 'grandparent', 'parent', 'child_start', 'child_middle', 'child_end', 'sibling']} [36;1m[1;3m[4:writes][0m [1mFinished step 4 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['fin']
{'name': 'test',
'path': ['grandparent',
'parent',
'grandparent',
'parent',
'child_start',
'child_middle',
'child_end',
'sibling',
'fin']}In [8]:
import uuid
def reduce_list(left: list | None, right: list | None) -> list:
"""Append the right-hand list, replacing any elements with the same id in the left-hand list."""
if not left:
left = []
if not right:
right = []
left_, right_ = [], []
for orig, new in [(left, left_), (right, right_)]:
for val in orig:
if not isinstance(val, dict):
val = {"val": val}
if "id" not in val:
val["id"] = str(uuid.uuid4())
new.append(val)
# Merge the two lists
left_idx_by_id = {val["id"]: i for i, val in enumerate(left_)}
merged = left_.copy()
for val in right_:
if (existing_idx := left_idx_by_id.get(val["id"])) is not None:
merged[existing_idx] = val
else:
merged.append(val)
return merged
class ChildState(TypedDict):
name: str
# note the updated reducer here
path: Annotated[list[str], reduce_list]
class ParentState(TypedDict):
name: str
# note the updated reducer here
path: Annotated[list[str], reduce_list]In [9]:
graph = make_graph(ParentState, ChildState)
graph.invoke({"name": "test"}, debug=True)Out [9]:
[36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'path': []} [36;1m[1;3m[0:tasks][0m [1mStarting step 0 with 1 task: [0m- [32;1m[1;3m__start__[0m -> {'name': 'test'} [36;1m[1;3m[0:writes][0m [1mFinished step 0 with writes to 1 channel: [0m- [33;1m[1;3mname[0m -> 'test' [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': []} [36;1m[1;3m[1:tasks][0m [1mStarting step 1 with 1 task: [0m- [32;1m[1;3mgrandparent[0m -> {'name': 'test', 'path': []} [36;1m[1;3m[1:writes][0m [1mFinished step 1 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['grandparent'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]} [36;1m[1;3m[2:tasks][0m [1mStarting step 2 with 1 task: [0m- [32;1m[1;3mparent[0m -> {'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]} [36;1m[1;3m[2:writes][0m [1mFinished step 2 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['parent'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]} [36;1m[1;3m[3:tasks][0m [1mStarting step 3 with 2 tasks: [0m- [32;1m[1;3mchild[0m -> {'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]} - [32;1m[1;3msibling[0m -> {'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]} [36;1m[1;3m[3:writes][0m [1mFinished step 3 with writes to 2 channels: [0m- [33;1m[1;3mname[0m -> 'test' - [33;1m[1;3mpath[0m -> [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}, {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'}, {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'}, {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'}], ['sibling'] [36;1m[1;3m[-2:checkpoint][0m [1mState at the end of step -2: [0m{'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}, {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'}, {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'}, {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'}, {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]} [36;1m[1;3m[4:tasks][0m [1mStarting step 4 with 1 task: [0m- [32;1m[1;3mfin[0m -> {'name': 'test', 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}, {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}, {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'}, {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'}, {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'}, {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]} [36;1m[1;3m[4:writes][0m [1mFinished step 4 with writes to 1 channel: [0m- [33;1m[1;3mpath[0m -> ['fin']
{'name': 'test',
'path': [{'val': 'grandparent', 'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7'},
{'val': 'parent', 'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e'},
{'val': 'child_start', 'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf'},
{'val': 'child_middle', 'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3'},
{'val': 'child_end', 'id': '052b5578-6939-4dc0-8e24-0a13548a937e'},
{'val': 'sibling', 'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29'},
{'val': 'fin', 'id': '82dc42d5-799b-4fad-8fbd-b12f32c179d2'}]}
