Spaces:
Runtime error
Runtime error
made changes in tools and graph
Browse files- app/agents/email_writing_agent.py +2 -2
- app/database/connection.py +2 -0
- app/graph.py +1 -1
- app/main.py +50 -11
- app/nodes/email_writing_node.py +1 -1
- app/prompts/email_writing_agent_prompt.py +4 -4
- app/tools/email_writing_agent_tools.py +12 -9
app/agents/email_writing_agent.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
from langchain_groq import ChatGroq
|
| 2 |
-
from app.tools.email_writing_agent_tools import create_gmail_draft,
|
| 3 |
base_llm = ChatGroq(
|
| 4 |
model="qwen/qwen3-32b",
|
| 5 |
temperature=0.1,
|
| 6 |
)
|
| 7 |
-
tools = [create_gmail_draft,
|
| 8 |
email_agent = base_llm.bind_tools(tools)
|
|
|
|
| 1 |
from langchain_groq import ChatGroq
|
| 2 |
+
from app.tools.email_writing_agent_tools import create_gmail_draft, send_draft
|
| 3 |
base_llm = ChatGroq(
|
| 4 |
model="qwen/qwen3-32b",
|
| 5 |
temperature=0.1,
|
| 6 |
)
|
| 7 |
+
tools = [create_gmail_draft, send_draft]
|
| 8 |
email_agent = base_llm.bind_tools(tools)
|
app/database/connection.py
CHANGED
|
@@ -2,6 +2,7 @@ from sqlalchemy import create_engine
|
|
| 2 |
from sqlalchemy.orm import sessionmaker, Session
|
| 3 |
from app.core.config import settings
|
| 4 |
from psycopg_pool import ConnectionPool
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
|
|
@@ -26,6 +27,7 @@ engine = create_engine(
|
|
| 26 |
|
| 27 |
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
| 28 |
|
|
|
|
| 29 |
def get_session():
|
| 30 |
db = SessionLocal()
|
| 31 |
try:
|
|
|
|
| 2 |
from sqlalchemy.orm import sessionmaker, Session
|
| 3 |
from app.core.config import settings
|
| 4 |
from psycopg_pool import ConnectionPool
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
|
| 7 |
|
| 8 |
|
|
|
|
| 27 |
|
| 28 |
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
| 29 |
|
| 30 |
+
|
| 31 |
def get_session():
|
| 32 |
db = SessionLocal()
|
| 33 |
try:
|
app/graph.py
CHANGED
|
@@ -4,7 +4,7 @@ from langgraph.prebuilt import ToolNode ,tools_condition
|
|
| 4 |
from app.nodes.archive_node import archive_node
|
| 5 |
from app.nodes.email_writing_node import *
|
| 6 |
from langgraph.graph import StateGraph,END,START
|
| 7 |
-
from app.tools.email_writing_agent_tools import create_gmail_draft,
|
| 8 |
from app.nodes.safety_check_node import *
|
| 9 |
from app.nodes.parse_node import parse_response_node
|
| 10 |
from app.nodes.context_node import prepare_context_node
|
|
|
|
| 4 |
from app.nodes.archive_node import archive_node
|
| 5 |
from app.nodes.email_writing_node import *
|
| 6 |
from langgraph.graph import StateGraph,END,START
|
| 7 |
+
from app.tools.email_writing_agent_tools import create_gmail_draft, send_draft
|
| 8 |
from app.nodes.safety_check_node import *
|
| 9 |
from app.nodes.parse_node import parse_response_node
|
| 10 |
from app.nodes.context_node import prepare_context_node
|
app/main.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException, Depends
|
| 2 |
from pydantic import BaseModel, EmailStr
|
| 3 |
from typing import Optional, Dict, Any, TypedDict, Annotated, Sequence
|
| 4 |
-
from langchain_core.messages import BaseMessage
|
| 5 |
from langgraph.graph import add_messages
|
| 6 |
from langgraph.types import Command
|
| 7 |
import uuid
|
|
@@ -11,9 +11,18 @@ from app.state.state import EmailAgentState
|
|
| 11 |
from app.database.connection import get_session
|
| 12 |
from app.database.utils import get_or_create_user
|
| 13 |
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
app = FastAPI(title="AI Email Agent API")
|
| 18 |
|
| 19 |
# --- Schemas ---
|
|
@@ -32,7 +41,10 @@ class ReviewActionRequest(BaseModel):
|
|
| 32 |
status: str # "approved" or "rejected"
|
| 33 |
feedback: Optional[str] = None
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
# --- Helper Functions ---
|
| 37 |
|
| 38 |
def parse_interrupt(final_state: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
@@ -90,6 +102,7 @@ def process_email(request: EmailProcessRequest, db: Session = Depends(get_sessio
|
|
| 90 |
return {
|
| 91 |
"status": "needs_review",
|
| 92 |
"thread_id": thread_id,
|
|
|
|
| 93 |
"triage_label": final_state.get("triage_label"),
|
| 94 |
"action": parsed_interrupt["action"],
|
| 95 |
"email_draft": {
|
|
@@ -133,17 +146,17 @@ def review_action(request: ReviewActionRequest) -> Dict[str, Any]:
|
|
| 133 |
else:
|
| 134 |
raise HTTPException(status_code=400, detail="Invalid status")
|
| 135 |
|
| 136 |
-
|
| 137 |
|
| 138 |
# Still in review phase
|
| 139 |
-
if "__interrupt__" in
|
| 140 |
-
parsed_interrupt = parse_interrupt(
|
| 141 |
if parsed_interrupt:
|
| 142 |
data = parsed_interrupt["data"]
|
| 143 |
return {
|
| 144 |
"status": "needs_review",
|
| 145 |
"thread_id": request.thread_id,
|
| 146 |
-
"triage_label":
|
| 147 |
"action": parsed_interrupt["action"],
|
| 148 |
"email_draft": {
|
| 149 |
"to": data.get("to"),
|
|
@@ -153,12 +166,13 @@ def review_action(request: ReviewActionRequest) -> Dict[str, Any]:
|
|
| 153 |
}
|
| 154 |
|
| 155 |
# Draft created, review complete
|
| 156 |
-
if
|
| 157 |
return {
|
| 158 |
"thread_id": request.thread_id,
|
| 159 |
-
"draft_id":
|
| 160 |
-
"
|
| 161 |
-
"
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
except Exception as e:
|
|
@@ -167,8 +181,33 @@ def review_action(request: ReviewActionRequest) -> Dict[str, Any]:
|
|
| 167 |
|
| 168 |
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
|
| 172 |
if __name__ == "__main__":
|
| 173 |
import uvicorn
|
| 174 |
-
uvicorn.run(app, host="
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException, Depends
|
| 2 |
from pydantic import BaseModel, EmailStr
|
| 3 |
from typing import Optional, Dict, Any, TypedDict, Annotated, Sequence
|
| 4 |
+
from langchain_core.messages import BaseMessage, HumanMessage
|
| 5 |
from langgraph.graph import add_messages
|
| 6 |
from langgraph.types import Command
|
| 7 |
import uuid
|
|
|
|
| 11 |
from app.database.connection import get_session
|
| 12 |
from app.database.utils import get_or_create_user
|
| 13 |
from sqlalchemy.orm import Session
|
| 14 |
+
from app.database.connection import SessionLocal
|
| 15 |
+
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
| 19 |
+
def get_session():
|
| 20 |
+
db = SessionLocal()
|
| 21 |
+
try:
|
| 22 |
+
yield db
|
| 23 |
+
finally:
|
| 24 |
+
db.close()
|
| 25 |
+
|
| 26 |
app = FastAPI(title="AI Email Agent API")
|
| 27 |
|
| 28 |
# --- Schemas ---
|
|
|
|
| 41 |
status: str # "approved" or "rejected"
|
| 42 |
feedback: Optional[str] = None
|
| 43 |
|
| 44 |
+
class SendEmailRequest(BaseModel):
|
| 45 |
+
thread_id: str
|
| 46 |
+
user_id: str
|
| 47 |
+
human_message: str
|
| 48 |
# --- Helper Functions ---
|
| 49 |
|
| 50 |
def parse_interrupt(final_state: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
|
| 102 |
return {
|
| 103 |
"status": "needs_review",
|
| 104 |
"thread_id": thread_id,
|
| 105 |
+
"messages": final_state.get("messages", []),
|
| 106 |
"triage_label": final_state.get("triage_label"),
|
| 107 |
"action": parsed_interrupt["action"],
|
| 108 |
"email_draft": {
|
|
|
|
| 146 |
else:
|
| 147 |
raise HTTPException(status_code=400, detail="Invalid status")
|
| 148 |
|
| 149 |
+
intermediate_state = graph.invoke(payload, config=config)
|
| 150 |
|
| 151 |
# Still in review phase
|
| 152 |
+
if "__interrupt__" in intermediate_state and not intermediate_state.get("draft_id"):
|
| 153 |
+
parsed_interrupt = parse_interrupt(intermediate_state)
|
| 154 |
if parsed_interrupt:
|
| 155 |
data = parsed_interrupt["data"]
|
| 156 |
return {
|
| 157 |
"status": "needs_review",
|
| 158 |
"thread_id": request.thread_id,
|
| 159 |
+
"triage_label": intermediate_state.get("triage_label"),
|
| 160 |
"action": parsed_interrupt["action"],
|
| 161 |
"email_draft": {
|
| 162 |
"to": data.get("to"),
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
# Draft created, review complete
|
| 169 |
+
if intermediate_state.get("draft_id"):
|
| 170 |
return {
|
| 171 |
"thread_id": request.thread_id,
|
| 172 |
+
"draft_id": intermediate_state["draft_id"],
|
| 173 |
+
"messages": intermediate_state.get("messages", []),
|
| 174 |
+
"reply_subject": intermediate_state.get("reply_subject"),
|
| 175 |
+
"reply_email_body": intermediate_state.get("reply_email_body"),
|
| 176 |
}
|
| 177 |
|
| 178 |
except Exception as e:
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
|
| 184 |
+
@app.post("/send_email")
|
| 185 |
+
def send_email(request: SendEmailRequest) -> Dict[str, Any]:
|
| 186 |
+
|
| 187 |
+
config = {
|
| 188 |
+
"configurable": {
|
| 189 |
+
"thread_id": request.thread_id,
|
| 190 |
+
"user_id": request.user_id
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
graph.update_state(
|
| 195 |
+
config,
|
| 196 |
+
{"messages": [HumanMessage(content=request.human_message)]},
|
| 197 |
+
as_node="prepare_context_node"
|
| 198 |
+
)
|
| 199 |
+
final_state = graph.invoke(None, config=config)
|
| 200 |
+
|
| 201 |
+
return {
|
| 202 |
+
"thread_id": request.thread_id,
|
| 203 |
+
"messages": final_state.get("messages", []),
|
| 204 |
+
"sent_message_id": final_state.get("sent_message_id")
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
|
| 209 |
|
| 210 |
|
| 211 |
if __name__ == "__main__":
|
| 212 |
import uvicorn
|
| 213 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
app/nodes/email_writing_node.py
CHANGED
|
@@ -54,7 +54,7 @@ def route_after_tools(state: EmailAgentState):
|
|
| 54 |
# logic 1: If we just successfully SENT the email, go to Parser -> Memory
|
| 55 |
if last_tool_msg.name == "send_draft_by_id" and "SUCCESS" in content_upper:
|
| 56 |
print("--- ROUTER: Send successful. Moving to Parse/Memory. ---")
|
| 57 |
-
return "
|
| 58 |
|
| 59 |
# logic 2: If we just created a DRAFT (or the send failed)
|
| 60 |
# Go back to agent to talk to the user
|
|
|
|
| 54 |
# logic 1: If we just successfully SENT the email, go to Parser -> Memory
|
| 55 |
if last_tool_msg.name == "send_draft_by_id" and "SUCCESS" in content_upper:
|
| 56 |
print("--- ROUTER: Send successful. Moving to Parse/Memory. ---")
|
| 57 |
+
return "store_memory_and_data_node"
|
| 58 |
|
| 59 |
# logic 2: If we just created a DRAFT (or the send failed)
|
| 60 |
# Go back to agent to talk to the user
|
app/prompts/email_writing_agent_prompt.py
CHANGED
|
@@ -16,16 +16,16 @@ User: "Reply to client."
|
|
| 16 |
Agent Tool: create_gmail_draft(...)
|
| 17 |
Output: "Success! ID: draft_999"
|
| 18 |
User: "Send it."
|
| 19 |
-
Agent Tool:
|
| 20 |
-
Output: "SUCCESS: Sent! Message
|
| 21 |
Output: "Stored."
|
| 22 |
</one_shot_example>
|
| 23 |
|
| 24 |
<rules>
|
| 25 |
1. NEW DRAFT: Call `create_gmail_draft` first.
|
| 26 |
2. REJECTION: If "DRAFT REJECTED", rewrite and call `create_gmail_draft_tool` again.
|
| 27 |
-
3. SENDING: Only call `
|
| 28 |
-
4. ARCHIVING: After `
|
| 29 |
important:You are not allowed to send until the user explicitly orders to send.
|
| 30 |
5.Use **{sender_email_id}** as the recipient not the name.
|
| 31 |
**dont call send_draft_by_id until user allows you to send the draft**
|
|
|
|
| 16 |
Agent Tool: create_gmail_draft(...)
|
| 17 |
Output: "Success! ID: draft_999"
|
| 18 |
User: "Send it."
|
| 19 |
+
Agent Tool: send_draft()
|
| 20 |
+
Output: "SUCCESS: Sent! Message"
|
| 21 |
Output: "Stored."
|
| 22 |
</one_shot_example>
|
| 23 |
|
| 24 |
<rules>
|
| 25 |
1. NEW DRAFT: Call `create_gmail_draft` first.
|
| 26 |
2. REJECTION: If "DRAFT REJECTED", rewrite and call `create_gmail_draft_tool` again.
|
| 27 |
+
3. SENDING: Only call `send_draft` when user explicitly orders to send.
|
| 28 |
+
4. ARCHIVING: After `send_draft` returns a Message ID.
|
| 29 |
important:You are not allowed to send until the user explicitly orders to send.
|
| 30 |
5.Use **{sender_email_id}** as the recipient not the name.
|
| 31 |
**dont call send_draft_by_id until user allows you to send the draft**
|
app/tools/email_writing_agent_tools.py
CHANGED
|
@@ -5,8 +5,10 @@ from langchain.tools import tool
|
|
| 5 |
from langchain_google_community import GmailToolkit
|
| 6 |
from typing import Annotated, Union
|
| 7 |
from langchain_core.tools import InjectedToolCallId, tool
|
|
|
|
| 8 |
from langgraph.types import Command
|
| 9 |
from langchain_core.messages import SystemMessage, HumanMessage,ToolMessage,AIMessage,BaseMessage
|
|
|
|
| 10 |
|
| 11 |
@tool(args_schema=CreateDraftSchema)
|
| 12 |
def create_gmail_draft(
|
|
@@ -37,7 +39,8 @@ def create_gmail_draft(
|
|
| 37 |
reply = draft_tool.invoke({"message": body, "to": [to], "subject": subject})
|
| 38 |
try:
|
| 39 |
draft_id = reply.split(":")[1].strip()
|
| 40 |
-
|
|
|
|
| 41 |
|
| 42 |
# UPDATE STATE: Save draft_id directly
|
| 43 |
return Command(
|
|
@@ -46,7 +49,7 @@ def create_gmail_draft(
|
|
| 46 |
"reply_subject": subject,
|
| 47 |
"reply_email_body": body,
|
| 48 |
"messages": [ToolMessage(content, tool_call_id=tool_call_id)]
|
| 49 |
-
}
|
| 50 |
)
|
| 51 |
except IndexError:
|
| 52 |
return f"Draft created, but response parsing failed: {reply}"
|
|
@@ -57,20 +60,20 @@ def create_gmail_draft(
|
|
| 57 |
|
| 58 |
#---------------------------------------------------------------------------
|
| 59 |
|
| 60 |
-
|
| 61 |
-
def
|
| 62 |
-
|
| 63 |
-
tool_call_id: Annotated[str, InjectedToolCallId] # Injected ID
|
| 64 |
):
|
| 65 |
"""Sends a finalized Gmail draft by its ID."""
|
|
|
|
| 66 |
try:
|
| 67 |
toolkit = GmailToolkit()
|
| 68 |
result = toolkit.api_resource.users().drafts().send(
|
| 69 |
-
userId="me", body={"id": draft_id}
|
| 70 |
).execute()
|
| 71 |
|
| 72 |
sent_id = result['id']
|
| 73 |
-
content = f"SUCCESS: Sent! a Gmail
|
| 74 |
|
| 75 |
# UPDATE STATE: Save sent_message_id directly
|
| 76 |
return Command(
|
|
@@ -89,5 +92,5 @@ def send_draft_by_id(
|
|
| 89 |
|
| 90 |
email_writing_agent_tools = [
|
| 91 |
create_gmail_draft,
|
| 92 |
-
|
| 93 |
]
|
|
|
|
| 5 |
from langchain_google_community import GmailToolkit
|
| 6 |
from typing import Annotated, Union
|
| 7 |
from langchain_core.tools import InjectedToolCallId, tool
|
| 8 |
+
from langchain.tools import ToolRuntime
|
| 9 |
from langgraph.types import Command
|
| 10 |
from langchain_core.messages import SystemMessage, HumanMessage,ToolMessage,AIMessage,BaseMessage
|
| 11 |
+
from langgraph.graph import END
|
| 12 |
|
| 13 |
@tool(args_schema=CreateDraftSchema)
|
| 14 |
def create_gmail_draft(
|
|
|
|
| 39 |
reply = draft_tool.invoke({"message": body, "to": [to], "subject": subject})
|
| 40 |
try:
|
| 41 |
draft_id = reply.split(":")[1].strip()
|
| 42 |
+
|
| 43 |
+
content = f"Draft created. User You MUST confirm before calling send_draft()."
|
| 44 |
|
| 45 |
# UPDATE STATE: Save draft_id directly
|
| 46 |
return Command(
|
|
|
|
| 49 |
"reply_subject": subject,
|
| 50 |
"reply_email_body": body,
|
| 51 |
"messages": [ToolMessage(content, tool_call_id=tool_call_id)]
|
| 52 |
+
},goto=END
|
| 53 |
)
|
| 54 |
except IndexError:
|
| 55 |
return f"Draft created, but response parsing failed: {reply}"
|
|
|
|
| 60 |
|
| 61 |
#---------------------------------------------------------------------------
|
| 62 |
|
| 63 |
+
|
| 64 |
+
def send_draft(
|
| 65 |
+
tool_call_id: Annotated[str, InjectedToolCallId],runtime: ToolRuntime # Injected ID
|
|
|
|
| 66 |
):
|
| 67 |
"""Sends a finalized Gmail draft by its ID."""
|
| 68 |
+
|
| 69 |
try:
|
| 70 |
toolkit = GmailToolkit()
|
| 71 |
result = toolkit.api_resource.users().drafts().send(
|
| 72 |
+
userId="me", body={"id": runtime.state["draft_id"]}
|
| 73 |
).execute()
|
| 74 |
|
| 75 |
sent_id = result['id']
|
| 76 |
+
content = f"SUCCESS: Sent! a Gmail"
|
| 77 |
|
| 78 |
# UPDATE STATE: Save sent_message_id directly
|
| 79 |
return Command(
|
|
|
|
| 92 |
|
| 93 |
email_writing_agent_tools = [
|
| 94 |
create_gmail_draft,
|
| 95 |
+
send_draft
|
| 96 |
]
|