Gaykar commited on
Commit
57d78b2
·
1 Parent(s): 6545626

made changes in tools and graph

Browse files
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, send_draft_by_id
3
  base_llm = ChatGroq(
4
  model="qwen/qwen3-32b",
5
  temperature=0.1,
6
  )
7
- tools = [create_gmail_draft, send_draft_by_id]
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, send_draft_by_id
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
- final_state = graph.invoke(payload, config=config)
137
 
138
  # Still in review phase
139
- if "__interrupt__" in final_state and not final_state.get("draft_id"):
140
- parsed_interrupt = parse_interrupt(final_state)
141
  if parsed_interrupt:
142
  data = parsed_interrupt["data"]
143
  return {
144
  "status": "needs_review",
145
  "thread_id": request.thread_id,
146
- "triage_label": final_state.get("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 final_state.get("draft_id"):
157
  return {
158
  "thread_id": request.thread_id,
159
- "draft_id": final_state["draft_id"],
160
- "reply_subject": final_state.get("reply_subject"),
161
- "reply_email_body": final_state.get("reply_email_body"),
 
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="0.0.0.0", port=8000)
 
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 "parse_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
 
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: send_draft_by_id(draft_id="draft_999")
20
- Output: "SUCCESS: Sent! Message ID: msg_123"
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_by_id` when user explicitly orders to send.
28
- 4. ARCHIVING: After `send_draft_by_id` 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**
 
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
- content = f"Successfully created draft: <id>{draft_id}</id> <subject>{subject}</subject> <body>{body}</body>"
 
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
- @tool(args_schema=SendDraftSchema)
61
- def send_draft_by_id(
62
- draft_id: str,
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 with ID: <id>{sent_id}</id>"
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
- send_draft_by_id
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
  ]