In [41]:
from dotenv import load_dotenv
load_dotenv()
Out[41]:
True
In [42]:
import os, asyncio, json
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent, AgentState
from langchain.messages import HumanMessage, AIMessage, ToolMessage
from langchain.tools import tool, ToolRuntime
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_community.utilities import SQLDatabase
from langchain_community.document_loaders import PyPDFLoader
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from mcp.shared.exceptions import McpError
from mcp.types import CallToolResult, TextContent
from typing import Dict, Any
from tavily import TavilyClient
from pprint import pprint
from dataclasses import dataclass
from datetime import date
DEEPSEEK_MODEL = "deepseek-chat"
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
def deepseek_model(model: str = DEEPSEEK_MODEL, max_tokens=1000, **kwargs):
return init_chat_model(
model=model,
# DeepSeek uses LangChain's OpenAI-compatible transport.
model_provider="openai",
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=DEEPSEEK_BASE_URL,
max_tokens=max_tokens,
**kwargs,
)
In [43]:
@tool
def read_email(runtime: ToolRuntime) -> str:
"""Read an email from the given address."""
# take email from state
return runtime.state["email"]
@tool
def send_email(body: str) -> str:
"""Send an email to the given address with the given subject and body."""
# fake email sending
return f"Email sent"
In [44]:
from langchain.agents.middleware import HumanInTheLoopMiddleware
class EmailState(AgentState):
email: str
humanInTheLoopMiddleware = HumanInTheLoopMiddleware(
interrupt_on={
"read_email": False,
"send_email": True,
},
description_prefix="Tool execution requires approval",
)
agent = create_agent(
model=deepseek_model(),
tools=[read_email, send_email],
state_schema=EmailState,
checkpointer=InMemorySaver(),
middleware=[humanInTheLoopMiddleware],
)
In [45]:
config = {"configurable": {"thread_id": "1"}}
message = [HumanMessage(content="Please read my email and send a response immediately. Send the reply now in the same thread.")]
response = agent.invoke(
{
"messages": message,
"email": "Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John."
},
config=config
)
In [39]:
from pprint import pprint
print(response)
{'messages': [HumanMessage(content='Please read my email and send a response immediately. Send the reply now in the same thread.', additional_kwargs={}, response_metadata={}, id='a52f1dfe-b1fb-447e-a823-8925886c2f1f'), AIMessage(content='Let me start by reading your email.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 331, 'total_tokens': 366, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 75}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'a03191ed-faac-4389-b2cc-9ebcc4e2275c', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2957-d940-7663-af57-c9d8def66403-0', tool_calls=[{'name': 'read_email', 'args': {}, 'id': 'call_00_gm8VQDrLDGZ1M4efhuXo6820', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 331, 'output_tokens': 35, 'total_tokens': 366, 'input_token_details': {'cache_read': 256}, 'output_token_details': {}}), ToolMessage(content="Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.", name='read_email', id='0962b0ac-5f64-49b8-a1b8-0211724b5c88', tool_call_id='call_00_gm8VQDrLDGZ1M4efhuXo6820'), AIMessage(content='Now I have the email. It looks like John is asking to reschedule a meeting. Let me send a reply to John.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 400, 'total_tokens': 499, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 16}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '0aa5e5db-6044-4ea0-9ccf-d5cd6b6c02d6', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2957-df7b-7f03-bfc2-f540f09f3c1f-0', tool_calls=[{'name': 'send_email', 'args': {'body': "Hi John,\n\nThanks for letting me know. No problem at all — let's reschedule. What time works best for you tomorrow instead?\n\nBest,\nSeán"}, 'id': 'call_00_vDKIlrbPxQVEgvh71EWa3651', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 400, 'output_tokens': 99, 'total_tokens': 499, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}})], 'email': "Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.", '__interrupt__': [Interrupt(value={'action_requests': [{'name': 'send_email', 'args': {'body': "Hi John,\n\nThanks for letting me know. No problem at all — let's reschedule. What time works best for you tomorrow instead?\n\nBest,\nSeán"}, 'description': 'Tool execution requires approval\n\nTool: send_email\nArgs: {\'body\': "Hi John,\\n\\nThanks for letting me know. No problem at all — let\'s reschedule. What time works best for you tomorrow instead?\\n\\nBest,\\nSeán"}'}], 'review_configs': [{'action_name': 'send_email', 'allowed_decisions': ['approve', 'edit', 'reject']}]}, id='9e5c98f3e34ad8c79c1e5ff8da0cc3de')]}
Approve¶
In [32]:
from langgraph.types import Command
response = agent.invoke(
Command(
resume={"decisions": [{"type": "approve"}]}
),
config=config # Same thread ID to resume the paused conversation
)
pprint(response)
{'email': "Hi Seán, I'm going to be late for our meeting tomorrow. Can we "
'reschedule? Best, John.',
'messages': [HumanMessage(content='Please read my email and send a response immediately. Send the reply now in the same thread.', additional_kwargs={}, response_metadata={}, id='fdd63659-cdc6-4168-bddf-d9ee1bab463f'),
AIMessage(content='Let me read your email first.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 331, 'total_tokens': 365, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 75}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '06cd579c-f168-4084-959f-09cc1bf5823d', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2952-eff8-7042-afcd-cf63bdf26f5b-0', tool_calls=[{'name': 'read_email', 'args': {}, 'id': 'call_00_SkU93FDM5yZEDyPiviQ05006', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 331, 'output_tokens': 34, 'total_tokens': 365, 'input_token_details': {'cache_read': 256}, 'output_token_details': {}}),
ToolMessage(content="Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.", name='read_email', id='1784ee0c-fd2c-4e58-9574-62b122dd919c', tool_call_id='call_00_SkU93FDM5yZEDyPiviQ05006'),
AIMessage(content="Now I'll send a reply to John:", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 80, 'prompt_tokens': 399, 'total_tokens': 479, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 15}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '3ae67a7a-3f0b-460f-99cc-3a55aa21514d', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2952-f354-7fb1-ae9c-e8600da25004-0', tool_calls=[{'name': 'send_email', 'args': {'body': "Hi John,\n\nThanks for letting me know. No problem at all — let's reschedule. What time works best for you?\n\nBest,\nSeán"}, 'id': 'call_00_8nZg4bdRh23FNrcdQaGr5355', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 399, 'output_tokens': 80, 'total_tokens': 479, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}}),
ToolMessage(content='Email sent', name='send_email', id='c776919b-134a-4aa0-8bb9-dee7dd250dab', tool_call_id='call_00_8nZg4bdRh23FNrcdQaGr5355'),
AIMessage(content="Done! I read your email from John saying he'll be late for tomorrow's meeting and asking to reschedule. I've sent a reply acknowledging his message and asking what time would work better for him.", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 41, 'prompt_tokens': 493, 'total_tokens': 534, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 109}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'fe8b00f1-d653-4048-bc7d-37964221ea05', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e2953-14a8-7e11-9174-962e7ba3463e-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 493, 'output_tokens': 41, 'total_tokens': 534, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}})]}
In [33]:
for msg in response["messages"]:
if msg.type == "human":
role = "Human"
elif msg.type == "ai":
role = "AI"
elif msg.type == "tool":
role = f"Tool: {msg.name}"
else:
role = msg.type
print(f"[{role}]")
pprint(msg.content)
print()
[Human]
('Please read my email and send a response immediately. Send the reply now in '
'the same thread.')
[AI]
'Let me read your email first.'
[Tool: read_email]
("Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? "
'Best, John.')
[AI]
"Now I'll send a reply to John:"
[Tool: send_email]
'Email sent'
[AI]
("Done! I read your email from John saying he'll be late for tomorrow's "
"meeting and asking to reschedule. I've sent a reply acknowledging his "
'message and asking what time would work better for him.')
Reject¶
In [40]:
response = agent.invoke(
Command(
resume={
"decisions": [
{
"type": "reject",
"message": "No please sign off - Your merciful leader, Seán."
}
]
}
),
config=config # Same thread ID to resume the paused conversation
)
pprint(response)
{'__interrupt__': [Interrupt(value={'action_requests': [{'args': {'body': 'Hi '
'John,\n'
'\n'
'Thanks '
'for '
'letting '
'me '
'know. '
'No '
'problem '
'at '
'all '
'— '
"let's "
'reschedule. '
'What '
'time '
'works '
'best '
'for '
'you '
'tomorrow '
'instead?\n'
'\n'
'Your '
'merciful '
'leader,\n'
'Seán'},
'description': 'Tool '
'execution '
'requires '
'approval\n'
'\n'
'Tool: '
'send_email\n'
'Args: '
"{'body': "
'"Hi '
'John,\\n\\nThanks '
'for '
'letting '
'me '
'know. '
'No '
'problem '
'at '
'all — '
"let's "
'reschedule. '
'What '
'time '
'works '
'best '
'for '
'you '
'tomorrow '
'instead?\\n\\nYour '
'merciful '
'leader,\\nSeán"}',
'name': 'send_email'}],
'review_configs': [{'action_name': 'send_email',
'allowed_decisions': ['approve',
'edit',
'reject']}]},
id='35cf45479e3d9cbac4e94169bbe81ec5')],
'email': "Hi Seán, I'm going to be late for our meeting tomorrow. Can we "
'reschedule? Best, John.',
'messages': [HumanMessage(content='Please read my email and send a response immediately. Send the reply now in the same thread.', additional_kwargs={}, response_metadata={}, id='a52f1dfe-b1fb-447e-a823-8925886c2f1f'),
AIMessage(content='Let me start by reading your email.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 331, 'total_tokens': 366, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 75}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'a03191ed-faac-4389-b2cc-9ebcc4e2275c', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2957-d940-7663-af57-c9d8def66403-0', tool_calls=[{'name': 'read_email', 'args': {}, 'id': 'call_00_gm8VQDrLDGZ1M4efhuXo6820', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 331, 'output_tokens': 35, 'total_tokens': 366, 'input_token_details': {'cache_read': 256}, 'output_token_details': {}}),
ToolMessage(content="Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.", name='read_email', id='0962b0ac-5f64-49b8-a1b8-0211724b5c88', tool_call_id='call_00_gm8VQDrLDGZ1M4efhuXo6820'),
AIMessage(content='Now I have the email. It looks like John is asking to reschedule a meeting. Let me send a reply to John.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 400, 'total_tokens': 499, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 16}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '0aa5e5db-6044-4ea0-9ccf-d5cd6b6c02d6', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2957-df7b-7f03-bfc2-f540f09f3c1f-0', tool_calls=[{'name': 'send_email', 'args': {'body': "Hi John,\n\nThanks for letting me know. No problem at all — let's reschedule. What time works best for you tomorrow instead?\n\nBest,\nSeán"}, 'id': 'call_00_vDKIlrbPxQVEgvh71EWa3651', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 400, 'output_tokens': 99, 'total_tokens': 499, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}}),
ToolMessage(content='No please sign off - Your merciful leader, Seán.', name='send_email', id='0bc11216-be4b-4385-af54-5dd414820b79', tool_call_id='call_00_vDKIlrbPxQVEgvh71EWa3651', status='error'),
AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 76, 'prompt_tokens': 522, 'total_tokens': 598, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 138}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'b6221688-9a37-44eb-a67b-07a8a9d41b2a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2957-fc65-7161-afe0-c90858a80c11-0', tool_calls=[{'name': 'send_email', 'args': {'body': "Hi John,\n\nThanks for letting me know. No problem at all — let's reschedule. What time works best for you tomorrow instead?\n\nYour merciful leader,\nSeán"}, 'id': 'call_00_gLCXHeGAN9dA05B2AbPt2962', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 522, 'output_tokens': 76, 'total_tokens': 598, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}})]}
In [25]:
for msg in response["messages"]:
if msg.type == "human":
role = "Human"
elif msg.type == "ai":
role = "AI"
elif msg.type == "tool":
role = f"Tool: {msg.name}"
else:
role = msg.type
print(f"[{role}]")
pprint(msg.content)
print()
[Human]
('Please read my email and send a response immediately. Send the reply now in '
'the same thread.')
[AI]
'Let me read your email first.'
[Tool: read_email]
("Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? "
'Best, John.')
[AI]
'Now let me send a reply to John.'
[Tool: send_email]
'No please sign off - Your merciful leader, Seán.'
[AI]
'Let me fix that sign-off.'
In [23]:
print(response['__interrupt__'][0].value['action_requests'][0]['args']['body'])
Hi John, Thanks for letting me know. No problem at all — let's reschedule. How does the same time on Thursday work for you? Your merciful leader, Seán
Edit¶
In [46]:
response = agent.invoke(
Command(
resume={
"decisions": [
{
"type": "edit",
"edited_action": {
"name": "send_email",
"args": {"body": "This is the last straw, you're fired!"},
}
}
]
}
),
config=config # Same thread ID to resume the paused conversation
)
pprint(response)
{'email': "Hi Seán, I'm going to be late for our meeting tomorrow. Can we "
'reschedule? Best, John.',
'messages': [HumanMessage(content='Please read my email and send a response immediately. Send the reply now in the same thread.', additional_kwargs={}, response_metadata={}, id='4ea353a8-f124-4bc3-84d4-452be8fd2143'),
AIMessage(content="I'll start by reading your email to see what it contains.", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 40, 'prompt_tokens': 331, 'total_tokens': 371, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 75}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '61985fa2-2a92-4b8c-8aa5-93e114de222d', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2958-46d7-7052-a151-5a66a0de9aba-0', tool_calls=[{'name': 'read_email', 'args': {}, 'id': 'call_00_JAvGfAzTrjNW3QWpVcc45016', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 331, 'output_tokens': 40, 'total_tokens': 371, 'input_token_details': {'cache_read': 256}, 'output_token_details': {}}),
ToolMessage(content="Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? Best, John.", name='read_email', id='e6ba3b60-8bf6-4b09-b740-773b9c7f3ce8', tool_call_id='call_00_JAvGfAzTrjNW3QWpVcc45016'),
AIMessage(content='Now I have the email. Let me send a reply to John.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 90, 'prompt_tokens': 405, 'total_tokens': 495, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 149}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'ac746ade-6a57-442c-b987-54abdd005aa1', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e2958-4a8a-7a31-83ac-6606723e6b94-0', tool_calls=[{'type': 'tool_call', 'name': 'send_email', 'args': {'body': "This is the last straw, you're fired!"}, 'id': 'call_00_GB6EU1HymJSv2l2OpvvT7877'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 405, 'output_tokens': 90, 'total_tokens': 495, 'input_token_details': {'cache_read': 256}, 'output_token_details': {}}),
ToolMessage(content='Email sent', name='send_email', id='031cc9a4-5628-4f0a-b219-6c05852636af', tool_call_id='call_00_GB6EU1HymJSv2l2OpvvT7877'),
AIMessage(content="There you go. I read John's email where he mentioned he'd be late for the meeting and wanted to reschedule, and I've sent your reply letting him know how you feel about it.", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 40, 'prompt_tokens': 483, 'total_tokens': 523, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 99}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '7844d976-ebc6-4f7a-bf1a-ba226afedf2e', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e2958-6d22-7c43-a598-fcbd1a55a31a-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 483, 'output_tokens': 40, 'total_tokens': 523, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}})]}
In [47]:
for msg in response["messages"]:
if msg.type == "human":
role = "Human"
elif msg.type == "ai":
role = "AI"
elif msg.type == "tool":
role = f"Tool: {msg.name}"
else:
role = msg.type
print(f"[{role}]")
pprint(msg.content)
print()
[Human]
('Please read my email and send a response immediately. Send the reply now in '
'the same thread.')
[AI]
"I'll start by reading your email to see what it contains."
[Tool: read_email]
("Hi Seán, I'm going to be late for our meeting tomorrow. Can we reschedule? "
'Best, John.')
[AI]
'Now I have the email. Let me send a reply to John.'
[Tool: send_email]
'Email sent'
[AI]
("There you go. I read John's email where he mentioned he'd be late for the "
"meeting and wanted to reschedule, and I've sent your reply letting him know "
'how you feel about it.')
In [ ]: