✨Building Building a Next-Generation Gmail AI Assistant with Agent Authentication powered by Streamlit, LangGraph, and Arcade AI✨
🎯 Introduction
Tired of drowning in your inbox? What if you could simply ask for emails like you’d ask a personal assistant?
Introducing our AI-Powered Email Assistant — a revolutionary way to manage your Gmail with natural language commands. This isn’t just another email client; it’s your AI copilot for email management.
This experiment implements a sophisticated AI-powered email assistant that can read, search, and manage Gmail emails using modern AI technologies. The application combines the power of LangGraph for workflow orchestration, Streamlit for the user interface, and Arcade AI for Gmail integration.
🏗️ Project Overview
The **Gmail AI Assistant** allows users to:
- Search and read emails using natural language queries
- Draft and send emails through AI assistance
- Manage email labels and organization
- Maintain conversation memory across sessions
- Provide a clean, professional user interface
Key Capabilities
- ✅ **Natural Language Email Search** — “Show me emails from LinkedIn”
- ✅ **Email Drafting** — “Draft a reply to the latest email”
- ✅ **Email Analysis** — “Summarize my emails from this week”
- ✅ **Conversation Memory** — Maintains context across sessions
- ✅ **Professional UI** — Clean, modern interface with light mode
- ✅ **Duplicate Prevention** — Smart content deduplication
- ✅ **Real-time Streaming** — Live content updates during processing
🛠️ Technology Stack
Core Technologies
• Python 3.12+ — Primary programming language
• Streamlit — Web application framework for the UI
• LangGraph — Workflow orchestration and state management
• LangChain — LLM framework and tool integration
• Arcade AI — Gmail API integration and tool management(Arcade enables AI agents to securely take real-world actions through user-specific permissions, pre-built integrations with Gmail, Slack, GitHub, and more. You can also build your own agentic tools and MCP servers with our authoring and testing suite. Arcade is your tool engine, registry, and runtime.)
AI & ML
• OpenAI GPT-4 — Large language model for natural language processing
• LangChain Core — Message handling and tool execution
• LangGraph — Multi-actor application framework
Database & Storage
• PostgreSQL — Persistent conversation storage and checkpoints
• Supabase — Authentication and user management
• LangGraph Checkpoints — State persistence and conversation memory
Authentication & Security
• Supabase Auth — User authentication and session management
• OAuth 2.0 — Gmail API authorization
• Environment Variables — Secure API key management
1. Install Dependencies
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install requirements
pip install -r requirements.txt
2. Environment Configuration
Create a .env file in the project root:
# OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here
MODEL_CHOICE=gpt-4o-mini
# Arcade AI Configuration
ARCADE_API_KEY=your_arcade_api_key_here
# Supabase Configuration
SUPABASE_URL=your_supabase_url_here
SUPABASE_KEY=your_supabase_anon_key_here
# Database Configuration (Optional)
DATABASE_URL=your_postgresql_url_here
# Application Configuration
EMAIL=your_email@example.com4. Database Setup (Optional)
— Create PostgreSQL database
CREATE DATABASE arcade_ai_assistant;
— Tables will be created automatically by LangGraph
🏗️ Architecture & Implementation
1. Core Architecture
The application follows a multi-layered architecture:
Here we will create a LangGraph workflow that requires user authorization before running certain Arcade tools. When a tool needs authorization, the graph displays an authorization URL and waits for the user’s approval. This ensures that only the tools you explicitly authorize are available to the language model
🛠️ What Are Tools?
Large Language Models (LLMs) are brilliant at generating text 💡, but they often stumble when it comes to doing things like calculations 🔢, fetching live data 🌐, or interacting with apps 📲.
Think of it this way — LLMs have incredible brains 🧠 for reasoning, but no hands ✋ to touch the digital world.
That’s where tool calling (sometimes called “function calling”) comes in 🚀.
🎯 Why Use Tool Calling?
Imagine this:
A colleague shares a Google Drive doc 📄 with you, and you want your LLM to analyze it.
Without tool calling:
- You’d open Google Drive
- Find the doc
- Copy its content
- Paste it into your chat 😩
With tool calling:
- The LLM simply calls the Google Docs SearchAndRetrieveDocuments tool 🕵️♂️
- Finds and reads the doc instantly ⚡ — no manual steps needed!
From there:
- Need a meeting? 📅 The LLM can use Google Calendar to schedule it.
- Want to send the summary to your colleague? 📧 The Gmail tool has it covered.
⚙️ How Tool Calling Works
When an AI model supports tool calling, it can decide when and how to use tools you’ve made available — whether pre-built 🧩 or custom-made 🔨.
Example: You say, “Help me analyze the ‘Project XYZ’ Google Doc shared by John.”
The LLM can:
- Recognize it needs external data 🔍
- Decide GoogleDocs.SearchAndRetrieveDocuments is the best tool 🏆
- Call it with the right parameters 📤
- Read the doc 📖 and give you the perfect answer 💬
🔐 The Authorization Challenge
Of course, the LLM needs secure access to your data.
Arcade solves this by:
- Offering a standardized authorization interface
- Providing pre-built integrations with Google, Dropbox, GitHub, Notion, and more 🌟
- Supporting any OAuth 2.0 API 🔄
With this, our AI agent can think, act, and interact — all while keeping your data safe. 🛡️
💡 Endless Possibilities for Developers & AI Agents
Tool calling supercharges LLMs by combining their reasoning power 🧠 with almost any action available via APIs or code execution 💻.
With Arcade SDKs, developers can:
- Build AI-powered apps 🤖
- Create smart AI Agents 🕵️♀️
- Keep everything secure 🔒 and privacy-first 🛡️
🔧 Tool Augmented Generation (TAG)
Think of TAG as the cool cousin of RAG (Retrieval Augmented Generation) — but with extra superpowers! 🦸♂️✨
Just like RAG, it lets an AI use external data 📚 to answer questions.
But unlike RAG, it’s far more flexible — allowing the AI to use a wide variety of tools 🛠️, not just text or vector search.
🛠️ Tool Calling High-Level Flow
1️⃣ Step One — The Request Arrives 📩
A language model receives a user’s request.
It then decides if it needs a tool to complete the task.
2️⃣ Step Two — Picking the Right Tool 🔍
If a tool is needed, the model selects the most suitable one from the list of available tools.
Next, it predicts the correct parameters for the tool (like search terms, file IDs, or user details) and sends these back to the client application.
3️⃣ Step Three — Running the Tool ⚡
The client executes the tool, gets the output, and sends it to the model.
4️⃣ Step Four — Generating the Response 💡
With the tool’s output in hand, the model can now craft a precise, well-informed answer for the user.
💡 In short: TAG is like giving your AI both a brain 🧠 and hands 🤲 — letting it think deeply and take action!
🛠️Code Implementation
install required dependencies
langchain-arcade==1.3.1
langchain-core==0.3.72
langchain-openai==0.3.28
langgraph==0.6.0
langgraph-api==0.2.109
langgraph-checkpoint==2.1.1
langgraph-checkpoint-postgres==2.0.23
langgraph-cli==0.3.6
langgraph-prebuilt==0.6.0
langgraph-runtime-inmem==0.6.4
langgraph-sdk==0.2.0memeory_agent.py
from langchain_arcade import ToolManager
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode
from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
from langchain_core.runnables import RunnableConfig
from dotenv import load_dotenv
import sys
import os
import uuid
# Load in the environment variables
load_dotenv()
arcade_api_key = os.environ.get("ARCADE_API_KEY")
openai_api_key = os.environ.get("OPENAIAPIKEY")
model_choice = os.environ.get("MODEL_CHOICE")
email = os.environ.get("EMAIL")
database_url = os.environ.get("DATABASE_URL")
# Add debug print for API key
print(f"🔧 Agent: OpenAI API key loaded: {openai_api_key[:20]}..." if openai_api_key else "❌ Agent: No OpenAI API key found")
# Initialize the tool manager and fetch tools compatible with langgraph
manager = ToolManager(api_key=arcade_api_key)
print(f"🔧 Agent: ToolManager initialized with Arcade API key: {arcade_api_key[:20]}..." if arcade_api_key else "❌ Agent: No Arcade API key")
manager.init_tools(toolkits=["Gmail"])
tools = manager.to_langchain(use_interrupts=True)
print(f"🔧 Agent: Converted {len(tools)} tools to LangChain format")
# Print available tools with more details
print("🔧 Agent: Available tools:")
for i, tool in enumerate(tools):
tool_name = tool.name if hasattr(tool, 'name') else str(tool)
requires_auth = manager.requires_auth(tool_name) if hasattr(manager, 'requires_auth') else 'Unknown'
print(f" 🔧 Tool {i+1}: {tool_name} - Requires auth: {requires_auth}")
if hasattr(tool, 'description'):
print(f" Description: {tool.description[:100]}...")
if hasattr(tool, 'args_schema'):
print(f" Args schema: {tool.args_schema}")
if hasattr(tool, 'name'):
print(f" Tool name: {tool.name}")
if hasattr(tool, 'function'):
print(f" Function: {tool.function.name if hasattr(tool.function, 'name') else 'Unknown'}")
# Initialize the prebuilt tool node
tool_node = ToolNode(tools)
print("✅ Agent: ToolNode initialized successfully")
# Create a language model instance and bind it with the tools
print(f"🔧 Agent: Creating ChatOpenAI with model: {model_choice}")
print(f"🔧 Agent: Using API key: {openai_api_key[:20]}..." if openai_api_key else "❌ Agent: No API key provided")
model = ChatOpenAI(model=model_choice, api_key=openai_api_key, temperature=0)
model_with_tools = model.bind_tools(tools)
print("✅ Agent: ChatOpenAI model created successfully")
print(f"🔧 Agent: Model bound with {len(tools)} tools")
# Print tool names for debugging
print("🔧 Agent: Tool names available to model:")
for tool in tools:
if hasattr(tool, 'name'):
print(f" - {tool.name}")
else:
print(f" - {str(tool)}")
# Print tool descriptions for debugging
print("🔧 Agent: Tool descriptions:")
for tool in tools:
if hasattr(tool, 'description'):
print(f" - {tool.name}: {tool.description[:100]}...")
else:
print(f" - {tool.name}: No description available")
# Function to invoke the model and get a response
def call_agent(state: MessagesState):
messages = state["messages"]
# Add system message to guide the model on how to use Gmail tools
system_message = """You are a helpful AI assistant with access to Gmail tools. When users ask about emails, you MUST use the Gmail tools to retrieve actual email data.
CRITICAL: Do not just repeat the user's query - you must use the Gmail tools to actually retrieve emails.
For email queries like "show me top 5 emails from today":
1. Use Gmail_ListEmails tool with appropriate search criteria
2. For "top 5 emails from today" use: {"query": "after:today", "max_results": 5}
3. For "emails from today" use: {"query": "after:today"}
4. For "emails from specific sender" use: {"query": "from:example@gmail.com"}
For email drafting like "draft a reply":
1. Use Gmail_WriteDraftEmail tool to create a draft email
2. Use Gmail_WriteDraftReplyEmail tool to create a draft reply to an existing email
3. Provide a helpful response explaining what you've done
4. Include details about the draft email (subject, recipient, etc.)
Available Gmail tools:
- Gmail_ListEmails: List emails from inbox with search criteria
- Gmail_GetEmail: Get specific email by ID
- Gmail_SearchEmails: Search emails with advanced criteria
- Gmail_WriteDraftEmail: Create a draft email (saves to Gmail drafts folder)
- Gmail_ListDraftEmails: List all draft emails in the user's drafts folder
- Gmail_DeleteDraftEmail: Delete a specific draft email
- Gmail_SendDraftEmail: Send a draft email
- Gmail_UpdateDraftEmail: Update an existing draft email
- Gmail_WriteDraftReplyEmail: Create a draft reply to an existing email
Example tool usage:
- For "top 5 emails from today": Use Gmail_ListEmails with {"query": "after:today", "max_results": 5}
- For "emails from yesterday": Use Gmail_ListEmails with {"query": "after:yesterday before:today"}
- For "unread emails": Use Gmail_ListEmails with {"query": "is:unread"}
- For "draft a reply": Use Gmail_WriteDraftReplyEmail with appropriate subject, body, and recipient
- For "create a new draft": Use Gmail_WriteDraftEmail with appropriate subject, body, and recipient
- For "show me my drafts": Use Gmail_ListDraftEmails to list all draft emails
- For "delete a draft": Use Gmail_DeleteDraftEmail with the draft ID
- For "send a draft": Use Gmail_SendDraftEmail with the draft ID
- For "update a draft": Use Gmail_UpdateDraftEmail with the draft ID and new content
When using Gmail_ListEmails, you can pass these arguments:
- query: Search query (e.g., "after:today", "from:example@gmail.com", "subject:meeting")
- max_results: Maximum number of results to return (default is 10)
When using Gmail_WriteDraftEmail, you can pass these arguments:
- subject: Email subject line (REQUIRED)
- body: Email body content (REQUIRED - must not be empty)
- recipient: Email recipient address (REQUIRED)
CRITICAL FOR DRAFT EMAILS:
- ALWAYS provide a meaningful body content when creating draft emails
- If the user doesn't specify body content, create a professional default body
- For empty body requests, use a professional template like:
"Dear [Recipient],
[Your message here]
Best regards,
[Your name]"
- NEVER create draft emails with empty body content
- If the user only provides a subject, create a professional body based on the subject
When using Gmail_WriteDraftReplyEmail, you can pass these arguments:
- subject: Email subject line (usually starts with "Re:")
- body: Email body content (REQUIRED - must not be empty)
- recipient: Email recipient address
- thread_id: ID of the email thread to reply to (if available)
IMPORTANT: Always use the tools to retrieve actual email data and provide informative responses about what you find. Do not just repeat the user's query. After using tools, provide a helpful summary of what you accomplished.
For draft emails specifically:
- Draft emails created with Gmail_WriteDraftEmail are automatically saved to the user's Gmail drafts folder
- Draft replies created with Gmail_WriteDraftReplyEmail are automatically saved to the user's Gmail drafts folder
- Users can access their drafts at: https://mail.google.com/mail/u/0/#drafts
- Draft emails will appear in the Gmail web interface under the "Drafts" folder
- Users can edit, send, or delete drafts directly from Gmail
- ALWAYS provide meaningful body content for draft emails
Tool Selection Guidelines:
- Use Gmail_WriteDraftReplyEmail when the user wants to reply to an existing email
- Use Gmail_WriteDraftEmail when the user wants to create a new email from scratch
- Use Gmail_ListDraftEmails when the user wants to see all their saved drafts
- Use Gmail_DeleteDraftEmail when the user wants to delete a specific draft
- Use Gmail_SendDraftEmail when the user wants to send an existing draft
- Use Gmail_UpdateDraftEmail when the user wants to modify an existing draft
DRAFT EMAIL BODY REQUIREMENTS:
- NEVER create draft emails with empty body content
- Always provide professional, meaningful content
- If user doesn't specify body, create a professional template
- Use appropriate greetings and closings
- Make the content relevant to the subject and recipient"""
# Add system message if not already present
messages_with_system = messages[:]
if not messages or not isinstance(messages[0], SystemMessage):
messages_with_system = [SystemMessage(content=system_message)] + messages
print(f"🔧 Agent: Processing {len(messages_with_system)} messages")
print(f"🔧 Agent: Last message content: {messages_with_system[-1].content[:100]}...")
# Use invoke for synchronous processing
response = model_with_tools.invoke(messages_with_system)
print(f"🔧 Agent: Response generated with {len(response.tool_calls) if hasattr(response, 'tool_calls') and response.tool_calls else 0} tool calls")
if hasattr(response, 'tool_calls') and response.tool_calls:
for i, tool_call in enumerate(response.tool_calls):
print(f"🔧 Agent: Tool call {i+1}: {tool_call.get('name', 'Unknown')} with args: {tool_call.get('args', {})}")
else:
print(f"🔧 Agent: No tool calls generated - response content: {response.content}...")
print(f"🔧 Agent: This might indicate the model is not using tools properly")
# Return the updated message history
return {"messages": [response]}
# Function to determine the next step in the workflow based on the last message
def should_continue(state: MessagesState):
last_message = state["messages"][-1]
print(f"🔧 Agent: should_continue called with message type: {type(last_message)}")
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
print(f"🔧 Agent: Found {len(last_message.tool_calls)} tool calls")
for tool_call in last_message.tool_calls:
tool_name = tool_call.get("name", "Unknown")
print(f"🔧 Agent: Tool call: {tool_name}")
if manager.requires_auth(tool_name):
print(f"🔧 Agent: Tool {tool_name} requires authorization")
return "authorization"
print(f"🔧 Agent: No authorization required, proceeding to tools")
return "tools" # Proceed to tool execution if no authorization is needed
else:
print(f"🔧 Agent: No tool calls found, ending workflow")
return END # End the workflow if no tool calls are present
# Function to handle authorization for tools that require it
def authorize(state: MessagesState, *args, **kwargs):
# Extract config from args or kwargs
config = None
if args and hasattr(args[0], 'get'):
config = args[0]
elif 'config' in kwargs:
config = kwargs['config']
# Handle the case where config might not be passed
if config is None:
# Try to get user_id from the state or use a default
user_id = "default_user"
else:
user_id = config["configurable"].get("user_id", "default_user")
for tool_call in state["messages"][-1].tool_calls:
tool_name = tool_call["name"]
if not manager.requires_auth(tool_name):
continue
auth_response = manager.authorize(tool_name, user_id)
if auth_response.status != "completed":
# Prompt the user to visit the authorization URL
print(f"Visit the following URL to authorize: {auth_response.url}")
# Wait for the user to complete the authorization
# and then check the authorization status again
manager.wait_for_auth(auth_response.id)
if not manager.is_authorized(auth_response.id):
# This stops execution if authorization fails
raise ValueError("Authorization failed")
return {"messages": []}
# Builds the LangGraph workflow with memory
def build_graph():
# Build the workflow graph using StateGraph
workflow = StateGraph(MessagesState)
# Add nodes (steps) to the graph
workflow.add_node("agent", call_agent)
workflow.add_node("tools", tool_node)
workflow.add_node("authorization", authorize)
# Define the edges and control flow between nodes
workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", should_continue, ["authorization", "tools", END])
workflow.add_edge("authorization", "tools")
workflow.add_edge("tools", "agent")
# Set up memory for checkpointing the state
memory = MemorySaver()
# Compile the graph with the checkpointer
graph = workflow.compile(checkpointer=memory)
return graph
workflow = build_graph()
async def main():
async with (
# AsyncPostgresStore.from_conn_string(database_url) as store, # This line was removed as per the new_code
# AsyncPostgresSaver.from_conn_string(database_url) as checkpointer, # This line was removed as per the new_code
):
# Run these lines the first time to set up everything in Postgres
# await checkpointer.setup()
# await store.setup()
graph = build_graph()
# User query
user_query = "What emails do I have in my inbox from today? Remember my question too"
# Build messages list with user query
messages = [
HumanMessage(content=user_query)
]
# Define the input with messages
inputs = {
"messages": messages
}
# Configuration with thread and user IDs for authorization purposes
config = {"configurable": {"thread_id": "4", "user_id": email}}
# Run the graph and stream the outputs
async for chunk in graph.astream(inputs, config=config, stream_mode="values"):
# Pretty-print the last message in the chunk
chunk["messages"][-1].pretty_print()
if __name__ == "__main__":
import asyncio
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())LangGraph Workflow
app.py
"""
Streamlit UI for Arcade AI Agent with LangGraph and Supabase Authentication.
"""
import os
import streamlit as st
from dotenv import load_dotenv
import supabase
from supabase.client import Client
import uuid
import asyncio
import sys
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from langgraph.store.postgres import AsyncPostgresStore
# Import our agent
from memory_agent import build_graph
# Load environment variables
load_dotenv()
# Initialize Supabase client
supabase_url = os.environ.get("SUPABASE_URL", "")
supabase_key = os.environ.get("SUPABASE_KEY", "")
supabase_client = supabase.create_client(supabase_url, supabase_key)
# Get other environment variables
arcade_api_key = os.environ.get("ARCADE_API_KEY")
openai_api_key = os.environ.get("OPENAI_API_KEY")
model_choice = os.environ.get("MODEL_CHOICE")
database_url = os.environ.get("DATABASE_URL")
# Streamlit page configuration
st.set_page_config(
page_title="Arcade AI Agent",
page_icon="🎮",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS for better formatting
st.markdown("""
<style>
.stMarkdown h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
color: #1f77b4;
}
.auth-message {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 0.75rem;
border-radius: 0.25rem;
margin: 1rem 0;
}
.main-header {
text-align: center;
margin-bottom: 2rem;
}
.chat-container {
max-width: 900px;
margin: 0 auto;
}
</style>
""", unsafe_allow_html=True)
# Authentication functions
def sign_up(email, password, full_name):
try:
response = supabase_client.auth.sign_up({
"email": email,
"password": password,
"options": {
"data": {
"full_name": full_name
}
}
})
if response and response.user:
st.session_state.authenticated = True
st.session_state.user = response.user
st.rerun()
return response
except Exception as e:
st.error(f"Error signing up: {str(e)}")
return None
def sign_in(email, password):
try:
response = supabase_client.auth.sign_in_with_password({
"email": email,
"password": password
})
if response and response.user:
# Store user info directly in session state
st.session_state.authenticated = True
st.session_state.user = response.user
st.rerun()
return response
except Exception as e:
st.error(f"Error signing in: {str(e)}")
return None
def sign_out():
try:
supabase_client.auth.sign_out()
# Clear authentication-related session state
st.session_state.authenticated = False
st.session_state.user = None
# Clear conversation history
st.session_state.messages = []
# Generate new thread ID for next session
st.session_state.thread_id = str(uuid.uuid4())
# Set a flag to trigger rerun on next render
st.session_state.logout_requested = True
except Exception as e:
st.error(f"Error signing out: {str(e)}")
# Initialize session state
if "messages" not in st.session_state:
st.session_state.messages = []
if "authenticated" not in st.session_state:
st.session_state.authenticated = False
if "user" not in st.session_state:
st.session_state.user = None
if "thread_id" not in st.session_state:
st.session_state.thread_id = str(uuid.uuid4())
# Check for logout flag and clear it after processing
if st.session_state.get("logout_requested", False):
st.session_state.logout_requested = False
st.rerun()
# Custom writer function for streaming
class StreamlitWriter:
def __init__(self, placeholder):
self.placeholder = placeholder
self.content = ""
def __call__(self, text):
# Add proper formatting for authorization messages
if "🔐 Authorization required" in text:
# Add newlines around authorization messages
if not self.content.endswith("\n"):
self.content += "\n"
self.content += text
if not text.endswith("\n"):
self.content += "\n"
elif "Visit the following URL to authorize:" in text:
# Add newline before and after URL instruction
if not self.content.endswith("\n"):
self.content += "\n"
self.content += text + "\n"
elif "Waiting for authorization..." in text:
# Add newlines around waiting message
if not self.content.endswith("\n"):
self.content += "\n"
self.content += text + "\n\n"
elif text.startswith("You have the following emails") or text.startswith("From "):
# Add newline before email content
if not self.content.endswith("\n"):
self.content += "\n"
self.content += text
else:
self.content += text
self.placeholder.markdown(self.content)
async def stream_agent_response(user_input: str, graph, config: dict, placeholder):
"""Stream agent response with proper handling."""
# Build conversation history
messages = []
# Add conversation history
for msg in st.session_state.messages:
if msg["role"] == "user":
messages.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
messages.append(AIMessage(content=msg["content"]))
# Add current user message
messages.append(HumanMessage(content=user_input))
# Prepare input
inputs = {
"messages": messages
}
writer = StreamlitWriter(placeholder)
full_response = ""
processed_content = set()
email_content_processed = False
try:
# Stream the response
async for chunk in graph.astream(inputs, config=config, stream_mode="values"):
if isinstance(chunk, dict) and "messages" in chunk:
last_message = chunk["messages"][-1]
# Check for content in the message
if hasattr(last_message, 'content') and last_message.content:
content = str(last_message.content)
if content and content.strip() and content.lower() != 'null':
# Check if this content has already been processed
content_hash = hash(content.strip())
if content_hash not in processed_content:
processed_content.add(content_hash)
# Check if this is email content
if _is_email_like_content(content):
# Only process email content once per response
if not email_content_processed:
print(f"🔧 Processing email content: {content[:100]}...")
formatted_content = _format_email_response(content)
if formatted_content: # Only add if not empty (not a duplicate)
print(f"🔧 Adding formatted email content: {formatted_content[:100]}...")
writer(formatted_content)
full_response += formatted_content
email_content_processed = True
print(f"📧 Processed email content for this response")
else:
print(f"⚠️ Skipped duplicate email content")
else:
print(f"⚠️ Skipping duplicate email content in same response")
else:
# For non-email content, check if it's just repeating the user's query
if content.strip().lower() == user_input.strip().lower():
print(f"⚠️ Skipping user query repetition: {content[:50]}...")
else:
print(f"🔧 Adding non-email content: {content[:100]}...")
writer(content)
full_response += content
else:
print(f"⚠️ Skipping duplicate content: {content[:50]}{'...' if len(content) > 50 else ''}")
# Check for tool calls - if tools were called, provide feedback
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
print(f"🔧 Tool calls found: {len(last_message.tool_calls)}")
for tool_call in last_message.tool_calls:
tool_name = tool_call.get('name', '')
tool_args = tool_call.get('args', {})
print(f"🔧 Tool call: {tool_name} with args: {tool_args}")
# Provide user feedback for draft email tools
if tool_name == 'Gmail_WriteDraftEmail':
subject = tool_args.get('subject', 'No subject')
recipient = tool_args.get('recipient', 'No recipient')
body = tool_args.get('body', 'No body')
draft_summary = f"""✅ **Draft Email Created Successfully!**
**📧 Draft Details:**
- **To**: {recipient}
- **Subject**: {subject}
**📝 Complete Draft Content:**
```
{body}
```
**✅ What happened:**
- Your draft email has been created and saved to your Gmail drafts folder
- You can access it at: https://mail.google.com/mail/u/0/#drafts
- You can edit, customize, and send it directly from Gmail
**📝 Next steps:**
1. Go to Gmail Drafts to review your email
2. Make any edits if needed
3. Send when ready!"""
print(f"🔧 Adding draft email summary")
writer(draft_summary)
full_response += draft_summary
break
elif tool_name == 'Gmail_WriteDraftReplyEmail':
subject = tool_args.get('subject', 'No subject')
recipient = tool_args.get('recipient', 'No recipient')
body = tool_args.get('body', 'No body')
reply_to_id = tool_args.get('reply_to_message_id', 'Unknown')
draft_reply_summary = f"""✅ **Draft Reply Created Successfully!**
**📧 Draft Reply Details:**
- **To**: {recipient}
- **Subject**: {subject}
- **Replying to Message ID**: {reply_to_id}
**📝 Complete Draft Reply Content:**
```
{body}
```
**✅ What happened:**
- Your draft reply has been created and saved to your Gmail drafts folder
- You can access it at: https://mail.google.com/mail/u/0/#drafts
- You can edit, customize, and send it directly from Gmail
**📝 Next steps:**
1. Go to Gmail Drafts to review your reply
2. Make any edits if needed
3. Send when ready!"""
print(f"🔧 Adding draft reply summary")
writer(draft_reply_summary)
full_response += draft_reply_summary
break
# Check for tool results - this is the key addition
if hasattr(last_message, 'tool_results') and last_message.tool_results:
print(f"🔧 Tool results found: {len(last_message.tool_results)}")
for tool_result in last_message.tool_results:
if isinstance(tool_result, dict):
# Check for content in tool result
for key, value in tool_result.items():
if key in ['content', 'output', 'result', 'data'] and value:
content = str(value)
if content and content.strip() and content.lower() != 'null':
print(f"🔧 Tool result {key} found: {content[:100]}...")
# Check if this is email content
if _is_email_like_content(content):
if not email_content_processed:
print(f"🔧 Processing tool result email content: {content[:100]}...")
formatted_content = _format_email_response(content)
if formatted_content:
print(f"🔧 Adding formatted tool result email content: {formatted_content[:100]}...")
writer(formatted_content)
full_response += formatted_content
email_content_processed = True
print(f"📧 Processed tool result email content")
else:
print(f"⚠️ Skipped duplicate tool result email content")
else:
print(f"⚠️ Skipping duplicate tool result email content")
else:
# Add non-email tool result content
if content.strip().lower() != user_input.strip().lower():
print(f"🔧 Adding tool result content: {content[:100]}...")
writer(content)
full_response += content
break
# Check for additional kwargs that might contain tool results
if hasattr(last_message, 'additional_kwargs') and last_message.additional_kwargs:
if 'tool_results' in last_message.additional_kwargs:
tool_results = last_message.additional_kwargs['tool_results']
print(f"🔧 Tool results from kwargs: {len(tool_results)}")
for tool_result in tool_results:
if isinstance(tool_result, dict) and 'content' in tool_result:
content = tool_result['content']
if content and content.strip() and content.lower() != 'null':
print(f"🔧 Tool result content from kwargs: {content[:100]}...")
# Check if this is email content
if _is_email_like_content(content):
if not email_content_processed:
print(f"🔧 Processing kwargs email content: {content[:100]}...")
formatted_content = _format_email_response(content)
if formatted_content:
print(f"🔧 Adding formatted kwargs email content: {formatted_content[:100]}...")
writer(formatted_content)
full_response += formatted_content
email_content_processed = True
print(f"📧 Processed kwargs email content")
else:
print(f"⚠️ Skipped duplicate kwargs email content")
else:
print(f"⚠️ Skipping duplicate kwargs email content")
else:
# Add non-email kwargs content
if content.strip().lower() != user_input.strip().lower():
print(f"🔧 Adding kwargs content: {content[:100]}...")
writer(content)
full_response += content
return full_response
except Exception as e:
error_msg = f"Error processing request: {str(e)}"
writer(error_msg)
return error_msg
def _is_email_like_content(content):
"""Check if content looks like email data."""
if not content or not isinstance(content, str):
return False
# First, check if content is already formatted (contains HTML)
if '<div' in content or '<span' in content:
return False
# Check if this is just a user query (not actual email data)
normalized_content = content.strip().lower()
user_query_indicators = [
'show me', 'what emails', 'get emails', 'find emails', 'search for emails',
'list emails', 'display emails', 'retrieve emails', 'draft a reply'
]
# If it's a user query, don't treat it as email content
if any(indicator in normalized_content for indicator in user_query_indicators):
return False
# Check for actual email data indicators - be more comprehensive
email_indicators = [
'emails":', 'subject:', 'from:', 'date:', 'to:',
'snippet:', 'full email:', 'thread_id', 'body', '{"emails"',
'email_id', 'message_id', 'gmail_id', 'thread_id',
'sender', 'recipient', 'cc:', 'bcc:', 'reply-to:',
'content-type:', 'mime-version:', 'x-mailer:',
'received:', 'return-path:', 'delivered-to:',
'email', 'emails', 'message', 'messages'
]
# Check for JSON structure that might contain email data
if content.strip().startswith('{') and content.strip().endswith('}'):
try:
import json
data = json.loads(content)
# Check if it contains email-related keys
if isinstance(data, dict):
email_keys = ['emails', 'email', 'messages', 'message', 'threads', 'thread']
if any(key in data for key in email_keys):
return True
# Check if any value contains email-like data
for value in data.values():
if isinstance(value, str) and any(indicator in value.lower() for indicator in email_indicators):
return True
except (json.JSONDecodeError, Exception):
pass
# Check for email-like patterns in the content
has_email_indicators = any(indicator in content.lower() for indicator in email_indicators)
# Additional check: if it's a summary or description, don't treat as email content
if 'here are' in normalized_content and 'emails' in normalized_content and len(content) < 200:
return False
# Check for email-like structure (subject, from, date patterns)
email_structure_patterns = [
r'subject\s*:', r'from\s*:', r'date\s*:', r'to\s*:',
r'email\s*:', r'message\s*:', r'thread\s*:'
]
import re
has_email_structure = any(re.search(pattern, content, re.IGNORECASE) for pattern in email_structure_patterns)
return has_email_indicators or has_email_structure
def _format_email_response(content):
"""Format email response to match Arcade playground style."""
print(f"🔧 Formatting email response: {content[:200]}...")
# Create a normalized version of the content for duplicate detection
normalized_content = content.strip().lower()
# Check if this is just a repetition of a user query
if any(query in normalized_content for query in ['show me', 'what emails', 'get emails', 'find emails']):
print(f"⚠️ Skipping user query repetition in email content: {content[:50]}...")
return ""
# Check if this is a summary or description (not actual email data)
if 'here are' in normalized_content and 'emails' in normalized_content and len(content) < 200:
print(f"⚠️ Skipping email summary/description: {content[:50]}...")
return ""
try:
# Try to parse as JSON first
import json
if content.strip().startswith('{') and content.strip().endswith('}'):
print(f"🔧 Parsing as JSON: {content[:100]}...")
data = json.loads(content)
formatted = _format_json_email_data(data)
print(f"🔧 JSON formatting result: {formatted[:100]}...")
return formatted
# If not JSON, try to format as plain text email
print(f"🔧 Parsing as plain text email")
formatted = _format_plain_text_email(content)
print(f"🔧 Plain text formatting result: {formatted[:100]}...")
return formatted
except (json.JSONDecodeError, Exception) as e:
print(f"⚠️ Could not parse as JSON, formatting as plain text: {e}")
return _format_plain_text_email(content)
def _format_json_email_data(data):
"""Format JSON email data in simple list format."""
formatted_content = ""
print(f"🔧 Formatting JSON email data: {str(data)[:200]}...")
if 'emails' in data and isinstance(data['emails'], list):
print(f"🔧 Found {len(data['emails'])} emails in data")
formatted_content += f"**Here are the emails from {data.get('from', 'your inbox')}:**\n\n"
# Track which emails we've already processed in this session
processed_in_session = set()
email_count = 0 # Track actual emails added (not skipped)
for i, email in enumerate(data['emails'], 1):
print(f"🔧 Processing email {i}: {email.get('subject', 'No subject')[:50]}...")
# Create a unique identifier for this email
email_id = f"{email.get('subject', '')}_{email.get('date', '')}_{email.get('snippet', '')[:50]}"
email_subject = email.get('subject', '').strip()
# Check for duplicates using multiple criteria
is_duplicate = False
# Check session-level email ID tracking
if email_id in processed_in_session:
print(f"⚠️ Skipping duplicate email (session ID): {email.get('subject', 'No subject')}")
is_duplicate = True
if is_duplicate:
continue
# Add to tracking sets
processed_in_session.add(email_id)
email_count += 1
# Format email in simple format with content
subject = email.get('subject', 'No subject')
from_name = email.get('from_name', email.get('from', 'Unknown'))
from_email = email.get('from_email', email.get('from', 'unknown@example.com'))
# Clean up the from_email if it's in angle brackets
if '<' in from_email and '>' in from_email:
from_email = from_email.strip('<>')
# Format as simple text with subject and sender
formatted_content += f"{email_count}. Subject: \"{subject}\" from {from_name} ({from_email})\n"
# Add email content/body if available
if 'body' in email and email['body']:
# Truncate very long emails for readability
body = email['body']
if len(body) > 500:
body = body[:500] + "...\n[Content truncated - full email content available]"
formatted_content += f" Content: {body}\n"
elif 'snippet' in email and email['snippet']:
formatted_content += f" Preview: {email['snippet']}\n"
formatted_content += "\n" # Add extra line between emails
print(f"🔧 Added {email_count} unique emails out of {len(data['emails'])} total emails")
else:
print(f"⚠️ No 'emails' key found in data or emails is not a list: {list(data.keys()) if isinstance(data, dict) else type(data)}")
print(f"🔧 Final formatted content length: {len(formatted_content)}")
return formatted_content
def _format_plain_text_email(content):
"""Format plain text email content."""
lines = content.split('\n')
formatted_lines = []
for line in lines:
line = line.strip()
if line and line.lower() != 'null':
formatted_lines.append(line)
# Rejoin with proper spacing
formatted_content = '\n\n'.join(formatted_lines)
return formatted_content
async def run_agent_interaction(user_input: str, user_email: str, thread_id: str):
"""Run the agent interaction with proper setup."""
# Set up configuration
config = {
"configurable": {
"thread_id": thread_id,
"user_id": user_email
},
"recursion_limit": 100
}
# Create placeholder for streaming
response_placeholder = st.empty()
# Use context managers for store and checkpointer
async with (
AsyncPostgresStore.from_conn_string(database_url) as store,
AsyncPostgresSaver.from_conn_string(database_url) as checkpointer,
):
# Build the graph
graph = build_graph()
# Stream the response
response = await stream_agent_response(
user_input,
graph,
config,
response_placeholder
)
return response
# Sidebar for authentication
with st.sidebar:
st.title("🎮 Arcade AI Agent")
if not st.session_state.authenticated:
tab1, tab2 = st.tabs(["Login", "Sign Up"])
with tab1:
st.subheader("Login")
login_email = st.text_input("Email", key="login_email")
login_password = st.text_input("Password", type="password", key="login_password")
login_button = st.button("Login")
if login_button:
if login_email and login_password:
sign_in(login_email, login_password)
else:
st.warning("Please enter both email and password.")
with tab2:
st.subheader("Sign Up")
signup_email = st.text_input("Email", key="signup_email")
signup_password = st.text_input("Password", type="password", key="signup_password")
signup_name = st.text_input("Full Name", key="signup_name")
signup_button = st.button("Sign Up")
if signup_button:
if signup_email and signup_password and signup_name:
response = sign_up(signup_email, signup_password, signup_name)
if response and response.user:
st.success("Sign up successful! Please check your email to confirm your account.")
else:
st.error("Sign up failed. Please try again.")
else:
st.warning("Please fill in all fields.")
else:
user = st.session_state.user
if user:
st.success(f"Logged in as: {user.email}")
st.button("Logout", on_click=sign_out)
# Display session information
st.divider()
st.subheader("Session Info")
st.text(f"Thread ID: {st.session_state.thread_id[:8]}...")
st.text(f"Messages: {len(st.session_state.messages)}")
# Clear conversation button
if st.button("🔄 New Conversation"):
st.session_state.messages = []
st.session_state.thread_id = str(uuid.uuid4())
st.rerun()
# Instructions
st.divider()
st.markdown("""
### 💡 How to Use
**Email Commands:**
- "What emails do I have?"
- "Show me emails from today"
- "Search for emails about meetings"
**Memory Features:**
- Include "remember" in your message to store information
- The agent will recall relevant memories automatically
**Web Scraping:**
- "Scrape this URL: [website]"
- "Get information from [website]"
The agent has access to Gmail and web scraping tools.
""")
# Main chat interface
if st.session_state.authenticated and st.session_state.user:
# Header
st.markdown('<div class="main-header">', unsafe_allow_html=True)
st.title("🎮 Arcade AI Agent Chat")
st.markdown("AI assistant with email access, web scraping, and memory capabilities")
st.markdown('</div>', unsafe_allow_html=True)
# Display all messages from conversation history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Chat input (naturally appears at bottom)
if prompt := st.chat_input("Ask me anything..."):
# Display user message immediately
with st.chat_message("user"):
st.markdown(prompt)
# Display assistant response
with st.chat_message("assistant"):
try:
# Handle Windows event loop
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# Run the async agent interaction
response = asyncio.run(
run_agent_interaction(
prompt,
st.session_state.user.email,
st.session_state.thread_id
)
)
# Add both messages to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
st.session_state.messages.append({"role": "assistant", "content": response})
except Exception as e:
error_msg = f"Error: {str(e)}"
st.error(error_msg)
# Still add the user message and error to history
st.session_state.messages.append({"role": "user", "content": prompt})
st.session_state.messages.append({"role": "assistant", "content": error_msg})
else:
# Welcome screen for non-authenticated users
st.title("Welcome to Arcade AI Agent")
st.write("Please login or sign up to start chatting with the AI assistant.")
st.write("This AI agent can:")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("### 📧 Email Access")
st.write("Read and search through your Gmail inbox")
with col2:
st.markdown("### 🌐 Web Scraping")
st.write("Extract information from websites")
with col3:
st.markdown("### 🧠 Memory")
st.write("Remember important information across conversations")
st.markdown("""
<div class="auth-message">
<strong>Note:</strong> You'll need to authorize Gmail access when first using email features.
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
# This won't run in Streamlit, but kept for consistency
pass🚀 Running the Application
1. Start the Application
# Activate virtual environment
source venv/bin/activate
# Run Streamlit app
streamlit run arcade_3_streamlit_app.py2. Access the Application
• Local URL: http://localhost:8501
• Network URL: http://your-ip:8501
3. First-Time Setup
1. Sign up/Sign in using your email
2. Authorize Gmail access when prompted
3. Start chatting with the AI assistant
Streamlit UI
Streamlit App response logs
(Arcade) PS C:\Users\nayak\Documents\Arcade> streamlit run .\app.py
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8501
Network URL: http://192.168.31.156:8501
🔧 Agent: OpenAI API key loaded: sk-proj-ivggDp-VD5po...
🔧 Agent: ToolManager initialized with Arcade API key: arc_o1DRyPPuJRxJKVAp...
🔧 Agent: Converted 17 tools to LangChain format
🔧 Agent: Available tools:
🔧 Tool 1: Gmail_ChangeEmailLabels - Requires auth: True
Description: Add and remove labels from an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ChangeEmailLabelsArgs'>
Tool name: Gmail_ChangeEmailLabels
🔧 Tool 2: Gmail_CreateLabel - Requires auth: True
Description: Create a new label in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.CreateLabelArgs'>
Tool name: Gmail_CreateLabel
🔧 Tool 3: Gmail_DeleteDraftEmail - Requires auth: True
Description: Delete a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.DeleteDraftEmailArgs'>
Tool name: Gmail_DeleteDraftEmail
🔧 Tool 4: Gmail_GetThread - Requires auth: True
Description: Get the specified thread by ID....
Args schema: <class 'langchain_arcade._utilities.GetThreadArgs'>
Tool name: Gmail_GetThread
🔧 Tool 5: Gmail_ListDraftEmails - Requires auth: True
Description: Lists draft emails in the user's draft mailbox using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ListDraftEmailsArgs'>
Tool name: Gmail_ListDraftEmails
🔧 Tool 6: Gmail_ListEmails - Requires auth: True
Description: Read emails from a Gmail account and extract plain text content....
Args schema: <class 'langchain_arcade._utilities.ListEmailsArgs'>
Tool name: Gmail_ListEmails
🔧 Tool 7: Gmail_ListEmailsByHeader - Requires auth: True
Description: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
Args schema: <class 'langchain_arcade._utilities.ListEmailsByHeaderArgs'>
Tool name: Gmail_ListEmailsByHeader
🔧 Tool 8: Gmail_ListLabels - Requires auth: True
Description: List all the labels in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListLabelsArgs'>
Tool name: Gmail_ListLabels
🔧 Tool 9: Gmail_ListThreads - Requires auth: True
Description: List threads in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListThreadsArgs'>
Tool name: Gmail_ListThreads
🔧 Tool 10: Gmail_ReplyToEmail - Requires auth: True
Description: Send a reply to an email message....
Args schema: <class 'langchain_arcade._utilities.ReplyToEmailArgs'>
Tool name: Gmail_ReplyToEmail
🔧 Tool 11: Gmail_SearchThreads - Requires auth: True
Description: Search for threads in the user's mailbox...
Args schema: <class 'langchain_arcade._utilities.SearchThreadsArgs'>
Tool name: Gmail_SearchThreads
🔧 Tool 12: Gmail_SendDraftEmail - Requires auth: True
Description: Send a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendDraftEmailArgs'>
Tool name: Gmail_SendDraftEmail
🔧 Tool 13: Gmail_SendEmail - Requires auth: True
Description: Send an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendEmailArgs'>
Tool name: Gmail_SendEmail
🔧 Tool 14: Gmail_TrashEmail - Requires auth: True
Description: Move an email to the trash folder using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.TrashEmailArgs'>
Tool name: Gmail_TrashEmail
🔧 Tool 15: Gmail_UpdateDraftEmail - Requires auth: True
Description: Update an existing email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.UpdateDraftEmailArgs'>
Tool name: Gmail_UpdateDraftEmail
🔧 Tool 16: Gmail_WriteDraftEmail - Requires auth: True
Description: Compose a new email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.WriteDraftEmailArgs'>
Tool name: Gmail_WriteDraftEmail
🔧 Tool 17: Gmail_WriteDraftReplyEmail - Requires auth: True
Description: Compose a draft reply to an email message....
Args schema: <class 'langchain_arcade._utilities.WriteDraftReplyEmailArgs'>
Tool name: Gmail_WriteDraftReplyEmail
✅ Agent: ToolNode initialized successfully
🔧 Agent: Creating ChatOpenAI with model: gpt-4.1-mini
🔧 Agent: Using API key: sk-proj-ivggDp-VD5po...
✅ Agent: ChatOpenAI model created successfully
🔧 Agent: Model bound with 17 tools
🔧 Agent: Tool names available to model:
- Gmail_ChangeEmailLabels
- Gmail_CreateLabel
- Gmail_DeleteDraftEmail
- Gmail_GetThread
- Gmail_ListDraftEmails
- Gmail_ListEmails
- Gmail_ListEmailsByHeader
- Gmail_ListLabels
- Gmail_ListThreads
- Gmail_ReplyToEmail
- Gmail_SearchThreads
- Gmail_SendDraftEmail
- Gmail_SendEmail
- Gmail_TrashEmail
- Gmail_UpdateDraftEmail
- Gmail_WriteDraftEmail
- Gmail_WriteDraftReplyEmail
🔧 Agent: Tool descriptions:
- Gmail_ChangeEmailLabels: Add and remove labels from an email using the Gmail API....
- Gmail_CreateLabel: Create a new label in the user's mailbox....
- Gmail_DeleteDraftEmail: Delete a draft email using the Gmail API....
- Gmail_GetThread: Get the specified thread by ID....
- Gmail_ListDraftEmails: Lists draft emails in the user's draft mailbox using the Gmail API....
- Gmail_ListEmails: Read emails from a Gmail account and extract plain text content....
- Gmail_ListEmailsByHeader: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
- Gmail_ListLabels: List all the labels in the user's mailbox....
- Gmail_ListThreads: List threads in the user's mailbox....
- Gmail_ReplyToEmail: Send a reply to an email message....
- Gmail_SearchThreads: Search for threads in the user's mailbox...
- Gmail_SendDraftEmail: Send a draft email using the Gmail API....
- Gmail_SendEmail: Send an email using the Gmail API....
- Gmail_TrashEmail: Move an email to the trash folder using the Gmail API....
- Gmail_UpdateDraftEmail: Update an existing email draft using the Gmail API....
- Gmail_WriteDraftEmail: Compose a new email draft using the Gmail API....
- Gmail_WriteDraftReplyEmail: Compose a draft reply to an email message....
⚠️ Skipping user query repetition: show me mails with subject line Agentic AI Pioneer...
🔧 Agent: Processing 2 messages
🔧 Agent: Last message content: show me mails with subject line Agentic AI Pioneer Program #8468...
🔧 Agent: Response generated with 1 tool calls
🔧 Agent: Tool call 1: Gmail_ListEmailsByHeader with args: {'subject': 'Agentic AI Pioneer Program #8468', 'max_results': 5}
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: Found 1 tool calls
🔧 Agent: Tool call: Gmail_ListEmailsByHeader
🔧 Agent: Tool Gmail_ListEmailsByHeader requires authorization
🔧 Processing email content: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Formatting email response: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will have to pay the discounted price i.e. 30% of the fee for 6 months extension (Rs.20,999) and 50% of the ...
🔧 Parsing as JSON: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Formatting JSON email data: {'emails': [{'body': "Hi Plaban Plaban, Hope you are doing well, For further extension, you will have to pay the discounted price i.e. 30% of the fee for 6 months extension (Rs.20,999) and 50% of the ...
🔧 Found 4 emails in data
🔧 Processing email 1: RE: [#8468] Re: Exciting New Courses Just Launched...
🔧 Processing email 2: Re: [#8468] Re: Exciting New Courses Just Launched...
🔧 Processing email 3: RE: [#8468] Re: Exciting New Courses Just Launched...
🔧 Processing email 4: Ticket received - Re: Exciting New Courses Just La...
🔧 Added 4 unique emails out of 4 total emails
🔧 Final formatted content length: 2953
🔧 JSON formatting result: **Here are the emails from your inbox:**
1. Subject: "RE: [#8468] Re: Exciting New Courses Just Lau...
🔧 Adding formatted email content: **Here are the emails from your inbox:**
1. Subject: "RE: [#8468] Re: Exciting New Courses Just Lau...
📧 Processed email content for this response
🔧 Agent: Processing 4 messages
🔧 Agent: Last message content: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Agent: Response generated with 0 tool calls
🔧 Agent: No tool calls generated - response content: I found several emails with the subject line related to "Agentic AI Pioneer Program #8468." Here is a summary of the emails:
1. An email from AV Customer Support (customersupport@analyticsvidhya.com) dated Mon, 16 Jun 2025, about course extension fees for the Agentic AI program and Gen AI Pinnacle program.
2. A reply from you (nayakpplaban@gmail.com) dated Tue, 17 Jun 2025, requesting information about course fees for extensions and a consideration for a discount.
3. Another email from AV Customer Support dated Mon, 16 Jun 2025, confirming the extension of your access to the Gen AI Pinnacle program and details about the Agentic AI program access.
4. An acknowledgment email from LeadSquared Service CRM (customersupport@analyticsvidhya.com) dated Mon, 16 Jun 2025, confirming receipt of your request and creation of a ticket with ID #8468.
If you want, I can provide more details or the full content of any specific email. Let me know how you would like to proceed....
🔧 Agent: This might indicate the model is not using tools properly
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: No tool calls found, ending workflow
⚠️ Skipping duplicate email content in same response
🔧 Processing email content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Formatting email response: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subject "Course Extension Inquiry" and body asking about program access...
🔧 Parsing as plain text email
🔧 Plain text formatting result: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Adding formatted email content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
📧 Processed email content for this response
🔧 Agent: Processing 4 messages
🔧 Agent: Last message content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Agent: Response generated with 1 tool calls
🔧 Agent: Tool call 1: Gmail_WriteDraftEmail with args: {'subject': 'Course Extension Inquiry', 'body': 'Dear Customer Support,\n\nI hope this message finds you well. I would like to inquire about the current access duration for the Agentic AI Pioneer Program and the options available for extending the program access.\n\nCould you please provide details on the extension process, fees, and any other relevant information?\n\nThank you for your assistance.\n\nBest regards,\n[Your Name]', 'recipient': 'customersupport@analyticsvidhya.com'}
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: Found 1 tool calls
🔧 Agent: Tool call: Gmail_WriteDraftEmail
🔧 Agent: Tool Gmail_WriteDraftEmail requires authorization
🔧 Agent: OpenAI API key loaded: sk-proj-ivggDp-VD5po...
🔧 Agent: ToolManager initialized with Arcade API key: arc_o1DRyPPuJRxJKVAp...
🔧 Agent: Converted 17 tools to LangChain format
🔧 Agent: Available tools:
🔧 Tool 1: Gmail_ChangeEmailLabels - Requires auth: True
Description: Add and remove labels from an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ChangeEmailLabelsArgs'>
Tool name: Gmail_ChangeEmailLabels
🔧 Tool 2: Gmail_CreateLabel - Requires auth: True
Description: Create a new label in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.CreateLabelArgs'>
Tool name: Gmail_CreateLabel
🔧 Tool 3: Gmail_DeleteDraftEmail - Requires auth: True
Description: Delete a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.DeleteDraftEmailArgs'>
Tool name: Gmail_DeleteDraftEmail
🔧 Tool 4: Gmail_GetThread - Requires auth: True
Description: Get the specified thread by ID....
Args schema: <class 'langchain_arcade._utilities.GetThreadArgs'>
Tool name: Gmail_GetThread
🔧 Tool 5: Gmail_ListDraftEmails - Requires auth: True
Description: Lists draft emails in the user's draft mailbox using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ListDraftEmailsArgs'>
Tool name: Gmail_ListDraftEmails
🔧 Tool 6: Gmail_ListEmails - Requires auth: True
Description: Read emails from a Gmail account and extract plain text content....
Args schema: <class 'langchain_arcade._utilities.ListEmailsArgs'>
Tool name: Gmail_ListEmails
🔧 Tool 7: Gmail_ListEmailsByHeader - Requires auth: True
Description: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
Args schema: <class 'langchain_arcade._utilities.ListEmailsByHeaderArgs'>
Tool name: Gmail_ListEmailsByHeader
🔧 Tool 8: Gmail_ListLabels - Requires auth: True
Description: List all the labels in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListLabelsArgs'>
Tool name: Gmail_ListLabels
🔧 Tool 9: Gmail_ListThreads - Requires auth: True
Description: List threads in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListThreadsArgs'>
Tool name: Gmail_ListThreads
🔧 Tool 10: Gmail_ReplyToEmail - Requires auth: True
Description: Send a reply to an email message....
Args schema: <class 'langchain_arcade._utilities.ReplyToEmailArgs'>
Tool name: Gmail_ReplyToEmail
🔧 Tool 11: Gmail_SearchThreads - Requires auth: True
Description: Search for threads in the user's mailbox...
Args schema: <class 'langchain_arcade._utilities.SearchThreadsArgs'>
Tool name: Gmail_SearchThreads
🔧 Tool 12: Gmail_SendDraftEmail - Requires auth: True
Description: Send a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendDraftEmailArgs'>
Tool name: Gmail_SendDraftEmail
🔧 Tool 13: Gmail_SendEmail - Requires auth: True
Description: Send an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendEmailArgs'>
Tool name: Gmail_SendEmail
🔧 Tool 14: Gmail_TrashEmail - Requires auth: True
Description: Move an email to the trash folder using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.TrashEmailArgs'>
Tool name: Gmail_TrashEmail
🔧 Tool 15: Gmail_UpdateDraftEmail - Requires auth: True
Description: Update an existing email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.UpdateDraftEmailArgs'>
Tool name: Gmail_UpdateDraftEmail
🔧 Tool 16: Gmail_WriteDraftEmail - Requires auth: True
Description: Compose a new email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.WriteDraftEmailArgs'>
Tool name: Gmail_WriteDraftEmail
🔧 Tool 17: Gmail_WriteDraftReplyEmail - Requires auth: True
Description: Compose a draft reply to an email message....
Args schema: <class 'langchain_arcade._utilities.WriteDraftReplyEmailArgs'>
Tool name: Gmail_WriteDraftReplyEmail
✅ Agent: ToolNode initialized successfully
🔧 Agent: Creating ChatOpenAI with model: gpt-4.1-mini
🔧 Agent: Using API key: sk-proj-ivggDp-VD5po...
✅ Agent: ChatOpenAI model created successfully
🔧 Agent: Model bound with 17 tools
🔧 Agent: Tool names available to model:
- Gmail_ChangeEmailLabels
- Gmail_CreateLabel
- Gmail_DeleteDraftEmail
- Gmail_GetThread
- Gmail_ListDraftEmails
- Gmail_ListEmails
- Gmail_ListEmailsByHeader
- Gmail_ListLabels
- Gmail_ListThreads
- Gmail_ReplyToEmail
- Gmail_SearchThreads
- Gmail_SendDraftEmail
- Gmail_SendEmail
- Gmail_TrashEmail
- Gmail_UpdateDraftEmail
- Gmail_WriteDraftEmail
- Gmail_WriteDraftReplyEmail
🔧 Agent: Tool descriptions:
- Gmail_ChangeEmailLabels: Add and remove labels from an email using the Gmail API....
- Gmail_CreateLabel: Create a new label in the user's mailbox....
- Gmail_DeleteDraftEmail: Delete a draft email using the Gmail API....
- Gmail_GetThread: Get the specified thread by ID....
- Gmail_ListDraftEmails: Lists draft emails in the user's draft mailbox using the Gmail API....
- Gmail_ListEmails: Read emails from a Gmail account and extract plain text content....
- Gmail_ListEmailsByHeader: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
- Gmail_ListLabels: List all the labels in the user's mailbox....
- Gmail_ListThreads: List threads in the user's mailbox....
- Gmail_ReplyToEmail: Send a reply to an email message....
- Gmail_SearchThreads: Search for threads in the user's mailbox...
- Gmail_SendDraftEmail: Send a draft email using the Gmail API....
- Gmail_SendEmail: Send an email using the Gmail API....
- Gmail_TrashEmail: Move an email to the trash folder using the Gmail API....
- Gmail_UpdateDraftEmail: Update an existing email draft using the Gmail API....
- Gmail_WriteDraftEmail: Compose a new email draft using the Gmail API....
- Gmail_WriteDraftReplyEmail: Compose a draft reply to an email message....
🔧 Processing email content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Formatting email response: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subject "Course Extension Inquiry" and body asking about program access...
🔧 Parsing as plain text email
🔧 Plain text formatting result: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Adding formatted email content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
📧 Processed email content for this response
🔧 Agent: Processing 6 messages
🔧 Agent: Last message content: Use Gmail_WriteDraftEmail to create a draft email to customersupport@analyticsvidhya.com with subjec...
🔧 Agent: Response generated with 1 tool calls
🔧 Agent: Tool call 1: Gmail_WriteDraftEmail with args: {'subject': 'Course Extension Inquiry', 'body': 'Dear Customer Support,\n\nI hope this message finds you well. I would like to inquire about the access duration and extension options for the Agentic AI Pioneer Program. Could you please provide details on how I can extend my program access and any associated fees?\n\nThank you for your assistance.\n\nBest regards,\n[Your Name]', 'recipient': 'customersupport@analyticsvidhya.com'}
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: Found 1 tool calls
🔧 Agent: Tool call: Gmail_WriteDraftEmail
🔧 Agent: Tool Gmail_WriteDraftEmail requires authorization
🔧 Tool calls found: 1
🔧 Tool call: Gmail_WriteDraftEmail with args: {'subject': 'Course Extension Inquiry', 'body': 'Dear Customer Support,\n\nI hope this message finds you well. I would like to inquire about the access duration and extension options for the Agentic AI Pioneer Program. Could you please provide details on how I can extend my program access and any associated fees?\n\nThank you for your assistance.\n\nBest regards,\n[Your Name]', 'recipient': 'customersupport@analyticsvidhya.com'}
🔧 Adding draft email summary
🔧 Tool calls found: 1
🔧 Tool call: Gmail_WriteDraftEmail with args: {'subject': 'Course Extension Inquiry', 'body': 'Dear Customer Support,\n\nI hope this message finds you well. I would like to inquire about the access duration and extension options for the Agentic AI Pioneer Program. Could you please provide details on how I can extend my program access and any associated fees?\n\nThank you for your assistance.\n\nBest regards,\n[Your Name]', 'recipient': 'customersupport@analyticsvidhya.com'}
🔧 Adding draft email summaryDraft Email
🔧 Adding draft reply summary
🔧 Agent: OpenAI API key loaded: sk-proj-ivggDp-VD5po...
🔧 Agent: ToolManager initialized with Arcade API key: arc_o1DRyPPuJRxJKVAp...
🔧 Agent: Converted 17 tools to LangChain format
🔧 Agent: Available tools:
🔧 Tool 1: Gmail_ChangeEmailLabels - Requires auth: True
Description: Add and remove labels from an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ChangeEmailLabelsArgs'>
Tool name: Gmail_ChangeEmailLabels
🔧 Tool 2: Gmail_CreateLabel - Requires auth: True
Description: Create a new label in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.CreateLabelArgs'>
Tool name: Gmail_CreateLabel
🔧 Tool 3: Gmail_DeleteDraftEmail - Requires auth: True
Description: Delete a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.DeleteDraftEmailArgs'>
Tool name: Gmail_DeleteDraftEmail
🔧 Tool 4: Gmail_GetThread - Requires auth: True
Description: Get the specified thread by ID....
Args schema: <class 'langchain_arcade._utilities.GetThreadArgs'>
Tool name: Gmail_GetThread
🔧 Tool 5: Gmail_ListDraftEmails - Requires auth: True
Description: Lists draft emails in the user's draft mailbox using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.ListDraftEmailsArgs'>
Tool name: Gmail_ListDraftEmails
🔧 Tool 6: Gmail_ListEmails - Requires auth: True
Description: Read emails from a Gmail account and extract plain text content....
Args schema: <class 'langchain_arcade._utilities.ListEmailsArgs'>
Tool name: Gmail_ListEmails
🔧 Tool 7: Gmail_ListEmailsByHeader - Requires auth: True
Description: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
Args schema: <class 'langchain_arcade._utilities.ListEmailsByHeaderArgs'>
Tool name: Gmail_ListEmailsByHeader
🔧 Tool 8: Gmail_ListLabels - Requires auth: True
Description: List all the labels in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListLabelsArgs'>
Tool name: Gmail_ListLabels
🔧 Tool 9: Gmail_ListThreads - Requires auth: True
Description: List threads in the user's mailbox....
Args schema: <class 'langchain_arcade._utilities.ListThreadsArgs'>
Tool name: Gmail_ListThreads
🔧 Tool 10: Gmail_ReplyToEmail - Requires auth: True
Description: Send a reply to an email message....
Args schema: <class 'langchain_arcade._utilities.ReplyToEmailArgs'>
Tool name: Gmail_ReplyToEmail
🔧 Tool 11: Gmail_SearchThreads - Requires auth: True
Description: Search for threads in the user's mailbox...
Args schema: <class 'langchain_arcade._utilities.SearchThreadsArgs'>
Tool name: Gmail_SearchThreads
🔧 Tool 12: Gmail_SendDraftEmail - Requires auth: True
Description: Send a draft email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendDraftEmailArgs'>
Tool name: Gmail_SendDraftEmail
🔧 Tool 13: Gmail_SendEmail - Requires auth: True
Description: Send an email using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.SendEmailArgs'>
Tool name: Gmail_SendEmail
🔧 Tool 14: Gmail_TrashEmail - Requires auth: True
Description: Move an email to the trash folder using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.TrashEmailArgs'>
Tool name: Gmail_TrashEmail
🔧 Tool 15: Gmail_UpdateDraftEmail - Requires auth: True
Description: Update an existing email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.UpdateDraftEmailArgs'>
Tool name: Gmail_UpdateDraftEmail
🔧 Tool 16: Gmail_WriteDraftEmail - Requires auth: True
Description: Compose a new email draft using the Gmail API....
Args schema: <class 'langchain_arcade._utilities.WriteDraftEmailArgs'>
Tool name: Gmail_WriteDraftEmail
🔧 Tool 17: Gmail_WriteDraftReplyEmail - Requires auth: True
Description: Compose a draft reply to an email message....
Args schema: <class 'langchain_arcade._utilities.WriteDraftReplyEmailArgs'>
Tool name: Gmail_WriteDraftReplyEmail
✅ Agent: ToolNode initialized successfully
🔧 Agent: Creating ChatOpenAI with model: gpt-4.1-mini
🔧 Agent: Using API key: sk-proj-ivggDp-VD5po...
✅ Agent: ChatOpenAI model created successfully
🔧 Agent: Model bound with 17 tools
🔧 Agent: Tool names available to model:
- Gmail_ChangeEmailLabels
- Gmail_CreateLabel
- Gmail_DeleteDraftEmail
- Gmail_GetThread
- Gmail_ListDraftEmails
- Gmail_ListEmails
- Gmail_ListEmailsByHeader
- Gmail_ListLabels
- Gmail_ListThreads
- Gmail_ReplyToEmail
- Gmail_SearchThreads
- Gmail_SendDraftEmail
- Gmail_SendEmail
- Gmail_TrashEmail
- Gmail_UpdateDraftEmail
- Gmail_WriteDraftEmail
- Gmail_WriteDraftReplyEmail
🔧 Agent: Tool descriptions:
- Gmail_ChangeEmailLabels: Add and remove labels from an email using the Gmail API....
- Gmail_CreateLabel: Create a new label in the user's mailbox....
- Gmail_DeleteDraftEmail: Delete a draft email using the Gmail API....
- Gmail_GetThread: Get the specified thread by ID....
- Gmail_ListDraftEmails: Lists draft emails in the user's draft mailbox using the Gmail API....
- Gmail_ListEmails: Read emails from a Gmail account and extract plain text content....
- Gmail_ListEmailsByHeader: Search for emails by header using the Gmail API.
At least one of the following parameters MUST be p...
- Gmail_ListLabels: List all the labels in the user's mailbox....
- Gmail_ListThreads: List threads in the user's mailbox....
- Gmail_ReplyToEmail: Send a reply to an email message....
- Gmail_SearchThreads: Search for threads in the user's mailbox...
- Gmail_SendDraftEmail: Send a draft email using the Gmail API....
- Gmail_SendEmail: Send an email using the Gmail API....
- Gmail_TrashEmail: Move an email to the trash folder using the Gmail API....
- Gmail_UpdateDraftEmail: Update an existing email draft using the Gmail API....
- Gmail_WriteDraftEmail: Compose a new email draft using the Gmail API....
- Gmail_WriteDraftReplyEmail: Compose a draft reply to an email message....
⚠️ Skipping user query repetition: can you draft a reply to customersupport@analytics...
🔧 Agent: Processing 6 messages
🔧 Agent: Last message content: can you draft a reply to customersupport@analyticsvidhya.com asking to send you the payment link for...
🔧 Agent: Response generated with 1 tool calls
🔧 Agent: Tool call 1: Gmail_ListEmailsByHeader with args: {'sender': 'customersupport@analyticsvidhya.com', 'subject': 'Agentic AI Pioneer Program #8468', 'max_results': 1}
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: Found 1 tool calls
🔧 Agent: Tool call: Gmail_ListEmailsByHeader
🔧 Agent: Tool Gmail_ListEmailsByHeader requires authorization
🔧 Tool calls found: 1
🔧 Tool call: Gmail_ListEmailsByHeader with args: {'sender': 'customersupport@analyticsvidhya.com', 'subject': 'Agentic AI Pioneer Program #8468', 'max_results': 1}
🔧 Tool calls found: 1
🔧 Tool call: Gmail_ListEmailsByHeader with args: {'sender': 'customersupport@analyticsvidhya.com', 'subject': 'Agentic AI Pioneer Program #8468', 'max_results': 1}
🔧 Processing email content: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Formatting email response: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will have to pay the discounted price i.e. 30% of the fee for 6 months extension (Rs.20,999) and 50% of the ...
🔧 Parsing as JSON: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Formatting JSON email data: {'emails': [{'body': "Hi Plaban Plaban, Hope you are doing well, For further extension, you will have to pay the discounted price i.e. 30% of the fee for 6 months extension (Rs.20,999) and 50% of the ...
🔧 Found 1 emails in data
🔧 Processing email 1: RE: [#8468] Re: Exciting New Courses Just Launched...
🔧 Added 1 unique emails out of 1 total emails
🔧 Final formatted content length: 827
🔧 JSON formatting result: **Here are the emails from your inbox:**
1. Subject: "RE: [#8468] Re: Exciting New Courses Just Lau...
🔧 Adding formatted email content: **Here are the emails from your inbox:**
1. Subject: "RE: [#8468] Re: Exciting New Courses Just Lau...
📧 Processed email content for this response
🔧 Agent: Processing 8 messages
🔧 Agent: Last message content: {"emails": [{"body": "Hi Plaban Plaban, Hope you are doing well, For further extension, you will hav...
🔧 Agent: Response generated with 1 tool calls
🔧 Agent: Tool call 1: Gmail_WriteDraftReplyEmail with args: {'body': 'Dear Customer Support Team,\n\nI hope this message finds you well. Kindly send me the payment link to proceed with the payment for the discounted price for the program extension. I would like to pay the discounted price of Rs. 20,999 for the 6 months extension and Rs. 34,999 for the 12 months extension.\n\nThank you for your assistance.\n\nBest regards,\nPlaban Nayak', 'reply_to_message_id': '1977c0e42eb9761a', 'reply_to_whom': 'ONLY_THE_SENDER'}
🔧 Agent: should_continue called with message type: <class 'langchain_core.messages.ai.AIMessage'>
🔧 Agent: Found 1 tool calls
🔧 Agent: Tool call: Gmail_WriteDraftReplyEmail
🔧 Agent: Tool Gmail_WriteDraftReplyEmail requires authorization
🔧 Tool calls found: 1
🔧 Tool call: Gmail_WriteDraftReplyEmail with args: {'body': 'Dear Customer Support Team,\n\nI hope this message finds you well. Kindly send me the payment link to proceed with the payment for the discounted price for the program extension. I would like to pay the discounted price of Rs. 20,999 for the 6 months extension and Rs. 34,999 for the 12 months extension.\n\nThank you for your assistance.\n\nBest regards,\nPlaban Nayak', 'reply_to_message_id': '1977c0e42eb9761a', 'reply_to_whom': 'ONLY_THE_SENDER'}
🔧 Adding draft reply summary
🔧 Tool calls found: 1
🔧 Tool call: Gmail_WriteDraftReplyEmail with args: {'body': 'Dear Customer Support Team,\n\nI hope this message finds you well. Kindly send me the payment link to proceed with the payment for the discounted price for the program extension. I would like to pay the discounted price of Rs. 20,999 for the 6 months extension and Rs. 34,999 for the 12 months extension.\n\nThank you for your assistance.\n\nBest regards,\nPlaban Nayak', 'reply_to_message_id': '1977c0e42eb9761a', 'reply_to_whom': 'ONLY_THE_SENDER'}
🔧 Adding draft reply summary🎯 Use Cases & Examples
1. Email Search
User: “Show me emails from LinkedIn”
AI: [Displays formatted LinkedIn emails with subjects, dates, and snippets]
2. Email Drafting
User: “Draft a reply to the latest email”
AI: [Creates a draft email with appropriate subject, body, and recipient]
3. Email Management
User: “Find emails about meetings from this week”
AI: [Searches and displays relevant emails with meeting information]
🔍 Troubleshooting
Common Issues
1. Authentication Errors
– Ensure Supabase credentials are correct
– Check Gmail authorization status
– Verify API keys in .env file
2. Duplicate Content
– Clear browser cache
– Reset conversation history
– Check for multiple Streamlit instances
3. Performance Issues
– Use in-memory storage for development
– Optimize database queries
– Monitor API rate limits
🎯 Main Objectives Behind Using LangGraph with Arcade AI
1. Multi-Actor Workflow Orchestration
- : LangGraph provides built-in state management for maintaining conversation context and user sessions — Multi-Actor Architecture: Supports multiple AI agents working together (email search, drafting, analysis)
- Enables conditional routing and decision-making based on user requests
# Example: Multi-actor workflow
def build_graph():
workflow = StateGraph(MessagesState)
# Multiple specialized nodes
workflow.add_node("agent", call_agent) # Main AI agent
workflow.add_node("tools", tool_node) # Tool execution
workflow.add_node("authorization", authorize) # Auth handling
# Conditional routing
workflow.add_conditional_edges("agent", should_continue,
["authorization", "tools", END])2. Seamless Tool Integration with Arcade AI
- Tool Abstraction: Arcade AI provides pre-built Gmail tools that LangGraph can execute
- Automatic Tool Selection: LangGraph can automatically choose the right Gmail tool based on user intent
- Error Handling: Built-in error handling and retry mechanisms for tool execution
# Arcade AI tools automatically integrated
tools = manager.to_langchain(use_interrupts=True)
tool_node = ToolNode(tools)
# LangGraph automatically routes to appropriate tools
def should_continue(state: MessagesState):
if state["messages"][-1].tool_calls:
for tool_call in state["messages"][-1].tool_calls:
if manager.requires_auth(tool_call["name"]):
return "authorization"
return "tools"
return END3. Conversation Memory and State Persistence
- Checkpointing: Automatic state persistence using PostgreSQL or in-memory storage
- Conversation History: Maintains full conversation context for better AI responses
- Session Management: Handles user sessions and thread management
# State persistence with checkpoints
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
# Conversation context maintained
config = {
"configurable": {
"thread_id": thread_id,
"user_id": user_id
}
}4. Authorization and Security Management
- - Interrupt Handling: LangGraph can pause execution for user authorization
- OAuth Integration: Arcade AI handles OAuth 2.0 flow for Gmail access
- Secure Tool Execution: Tools are executed with proper authentication
def authorize(state: MessagesState, config: RunnableConfig):
"""Handle authorization interrupts."""
user_id = config["configurable"].get("user_id")
for tool_call in state["messages"][-1].tool_calls:
tool_name = tool_call["name"]
if manager.requires_auth(tool_name):
auth_response = manager.authorize(tool_name, user_id)
if auth_response.status != "completed":
# LangGraph handles the interrupt
return {"messages": [AIMessage(content=f"Authorization required for {tool_name}")]}🎯 Key Advantages
1. Developer Experience
• Declarative Workflows: Define workflows using simple Python functions
• Built-in Debugging: Comprehensive logging and error tracking
• Hot Reloading: Changes to workflow are reflected immediately
2. User Experience
• Natural Language Interface: Users can interact using natural language
• Context Awareness: System remembers previous interactions
• Seamless Authorization: OAuth flow is handled automatically
3. Production Readiness
• Scalability: Handles multiple users and complex workflows
• Reliability: Robust error handling and recovery mechanisms
- Monitoring: Built-in logging and performance tracking
🚀 Future Extensibility
1. Additional Tools
- Calendar Integration: Schedule meetings and events
- Document Processing: Analyze email attachments
- Analytics: Email usage patterns and insights
- Work on saving Drafts into Drafts Folder og Gmail which at present is not functioning properly
- Authorize to send relies via email
2. Advanced Features
• Multi-Modal Support: Voice and image input
• Personalization: User-specific preferences and behaviors
- Integration: Connect with other productivity tools
🎉 Conclusion
This AI-powered Gmail assistant demonstrates the power of modern AI technologies working together. By combining Streamlit’s user-friendly interface, LangGraph’s workflow orchestration, and Arcade AI’s Gmail integration, we’ve created an intelligent application that can:
• Process natural language queries about emails
• Maintain conversation context across sessions
• Provide professional email formatting and display
• Handle authentication and authorization seamlessly
- Prevent duplicate content and ensure clean UI

