Spaces:
Runtime error
Runtime error
changes
Browse files- README.md +68 -1
- app/core/auth.py +105 -0
- app/core/config.py +5 -0
- app/gmail_auth.py +20 -0
- app/main.py +44 -15
- app/tools/email_writing_agent_tools.py +7 -5
- authenticate_gmail.py +29 -0
- requirements.txt +3 -0
README.md
CHANGED
|
@@ -9,4 +9,71 @@ license: apache-2.0
|
|
| 9 |
short_description: Email ai agent project with memory.
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
short_description: Email ai agent project with memory.
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 📧 AI-Driven Email Agent 🧠
|
| 13 |
+
|
| 14 |
+
A production-grade, multi-agent system built with LangGraph and FastAPI that automates email triage, context retrieval, and drafting. This project demonstrates advanced implementation of **Long-term Memory**, **State Persistence**, and **Human-in-the-Loop Interrupts** using LangGraph's Functional API Command pattern.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 🚀 Key Features
|
| 19 |
+
|
| 20 |
+
- **Advanced Learning Implementation**
|
| 21 |
+
Implemented **Semantic Memory**, **Checkpointer Persistence**, and **Functional Interrupts**, enabling the agent to maintain state and handle user feedback reliably.
|
| 22 |
+
|
| 23 |
+
- **Multi-Agent Workflow**
|
| 24 |
+
Specialized agents for:
|
| 25 |
+
- Triage
|
| 26 |
+
- Context Synthesis
|
| 27 |
+
- Email Drafting
|
| 28 |
+
|
| 29 |
+
- **Intelligent Triage**
|
| 30 |
+
Automatically classifies emails, assigns priority, and determines if a reply is required.
|
| 31 |
+
|
| 32 |
+
- **Semantic Memory**
|
| 33 |
+
Uses `langmem` and `PostgresStore` to retrieve past interactions, allowing the agent to remember previous project details.
|
| 34 |
+
|
| 35 |
+
- **Resource Management**
|
| 36 |
+
A dedicated **Token Count Node** ensures large emails (e.g., deployment logs) are summarized before processing to optimize costs.
|
| 37 |
+
|
| 38 |
+
- **Human-in-the-Loop**
|
| 39 |
+
The graph pauses using `interrupt()` to allow users to:
|
| 40 |
+
- Review drafts
|
| 41 |
+
- Approve responses
|
| 42 |
+
- Provide feedback via `Command(resume=...)`
|
| 43 |
+
|
| 44 |
+
- **Scalable Architecture**
|
| 45 |
+
Built with **FastAPI**, **Docker**, and a modular structure for enterprise-grade deployment.
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## 🛠️ Tech Stack
|
| 50 |
+
|
| 51 |
+
- **Orchestration:** `langgraph` (Functional API), `langchain`
|
| 52 |
+
- **LLM Interface:** `langchain-groq`
|
| 53 |
+
- **Memory & Persistence:**
|
| 54 |
+
- `langmem`
|
| 55 |
+
- `PostgresCheckpoint`
|
| 56 |
+
- `PostgresStore` (Neon/PostgreSQL)
|
| 57 |
+
- **Database ORM:** SQLAlchemy 2.0
|
| 58 |
+
- **Embeddings:** `langchain_huggingface` (DistilBERT)
|
| 59 |
+
- **Backend:** FastAPI + Uvicorn
|
| 60 |
+
- **Configuration:** `pydantic-settings` (.env management)
|
| 61 |
+
- **Authentication:** `google-auth` (Gmail API Integration)
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 📂 Project Structure
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
app/
|
| 69 |
+
├── agents/ # Brains: Specialized LLM logic (Triage, Writer, Context)
|
| 70 |
+
├── database/ # Data: SQLAlchemy models and Connection Pooling
|
| 71 |
+
├── nodes/ # Workflow: Functional steps of the graph (Safety, Tokens)
|
| 72 |
+
├── persistance/ # Persistence: Postgres Checkpointer & Memory Store config
|
| 73 |
+
├── state/ # Schema: Pydantic & TypedDict state definitions
|
| 74 |
+
├── utils/ # Toolbox: Token counters, Embeddings, and Auth helpers
|
| 75 |
+
├── graph.py # Logic: StateGraph construction and compilation
|
| 76 |
+
└── main.py # Entry: FastAPI app and Controller logic
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
|
app/core/auth.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta, timezone
|
| 2 |
+
from jose import JWTError, jwt
|
| 3 |
+
from passlib.context import CryptContext
|
| 4 |
+
from fastapi import HTTPException, Depends, status
|
| 5 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
import hashlib
|
| 8 |
+
import base64
|
| 9 |
+
from app.core.config import settings
|
| 10 |
+
from app.database.connection import get_session
|
| 11 |
+
from app.database.models import User
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# CONFIG
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
SECRET_KEY = settings.SECRET_KEY
|
| 18 |
+
ALGORITHM = settings.ALGORITHM
|
| 19 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
| 20 |
+
|
| 21 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 22 |
+
|
| 23 |
+
# auto_error=False → allows us to return 401 instead of FastAPI's 403
|
| 24 |
+
security = HTTPBearer(auto_error=False)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# TOKEN LOGIC
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def create_access_token(data: dict) -> str:
|
| 31 |
+
"""
|
| 32 |
+
Create a JWT access token with UTC-aware expiration
|
| 33 |
+
"""
|
| 34 |
+
to_encode = data.copy()
|
| 35 |
+
|
| 36 |
+
expire = datetime.now(timezone.utc) + timedelta(
|
| 37 |
+
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
to_encode.update({
|
| 41 |
+
"exp": expire,
|
| 42 |
+
"sub": str(data.get("id")) # JWT best practice
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def verify_token(
|
| 49 |
+
credentials: HTTPAuthorizationCredentials = Depends(security)
|
| 50 |
+
) -> dict:
|
| 51 |
+
"""
|
| 52 |
+
Verify JWT token and return payload
|
| 53 |
+
"""
|
| 54 |
+
if credentials is None:
|
| 55 |
+
raise HTTPException(
|
| 56 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 57 |
+
detail="Not authenticated"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
token = credentials.credentials
|
| 62 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 63 |
+
|
| 64 |
+
user_id = payload.get("id")
|
| 65 |
+
if user_id is None:
|
| 66 |
+
raise HTTPException(
|
| 67 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 68 |
+
detail="Invalid token payload"
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return payload
|
| 72 |
+
|
| 73 |
+
except JWTError:
|
| 74 |
+
raise HTTPException(
|
| 75 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 76 |
+
detail="Invalid or expired token"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# CURRENT USER DEPENDENCY
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
async def get_current_user(
|
| 83 |
+
token_data: dict = Depends(verify_token),
|
| 84 |
+
db: Session = Depends(get_session)
|
| 85 |
+
) -> User:
|
| 86 |
+
"""
|
| 87 |
+
Fetch the logged-in user from DB using JWT payload
|
| 88 |
+
"""
|
| 89 |
+
user_id = token_data.get("id")
|
| 90 |
+
|
| 91 |
+
user = (
|
| 92 |
+
db.query(User)
|
| 93 |
+
.filter(User.id == user_id)
|
| 94 |
+
.first()
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
if not user:
|
| 98 |
+
raise HTTPException(
|
| 99 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 100 |
+
detail="User not found"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
return user
|
| 104 |
+
|
| 105 |
+
# PASSWORD LOGIC (bcrypt-safe, unlimited length)
|
app/core/config.py
CHANGED
|
@@ -11,6 +11,11 @@ class Settings(BaseSettings):
|
|
| 11 |
GMAIL_CREDENTIALS_PATH: str = "credentials.json"
|
| 12 |
GMAIL_TOKEN_PATH: str = "token.json"
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
DB_URL_FOR_SQL_AL:str
|
| 15 |
|
| 16 |
model_config = SettingsConfigDict(
|
|
|
|
| 11 |
GMAIL_CREDENTIALS_PATH: str = "credentials.json"
|
| 12 |
GMAIL_TOKEN_PATH: str = "token.json"
|
| 13 |
|
| 14 |
+
SECRET_KEY: str
|
| 15 |
+
ALGORITHM: str
|
| 16 |
+
|
| 17 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
|
| 18 |
+
|
| 19 |
DB_URL_FOR_SQL_AL:str
|
| 20 |
|
| 21 |
model_config = SettingsConfigDict(
|
app/gmail_auth.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.core.config import settings
|
| 2 |
+
from langchain_google_community.gmail.utils import (
|
| 3 |
+
build_resource_service,
|
| 4 |
+
get_gmail_credentials,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
from langchain_google_community import GmailToolkit
|
| 8 |
+
credentials = get_gmail_credentials(
|
| 9 |
+
token_file=settings.GMAIL_TOKEN_PATH,
|
| 10 |
+
scopes=["https://mail.google.com/"],
|
| 11 |
+
client_sercret_file=settings.GMAIL_CREDENTIALS_PATH,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
api_resource = build_resource_service(
|
| 15 |
+
credentials=credentials
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
gmail_toolkit = GmailToolkit(
|
| 19 |
+
api_resource=api_resource
|
| 20 |
+
)
|
app/main.py
CHANGED
|
@@ -4,7 +4,6 @@ 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
|
| 8 |
import logging
|
| 9 |
from app.graph import graph
|
| 10 |
from app.state.state import EmailAgentState
|
|
@@ -12,6 +11,24 @@ 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__)
|
|
@@ -29,7 +46,6 @@ app = FastAPI(title="AI Email Agent API")
|
|
| 29 |
|
| 30 |
class EmailProcessRequest(BaseModel):
|
| 31 |
thread_id: str
|
| 32 |
-
user_email: EmailStr
|
| 33 |
sender_email_id: EmailStr
|
| 34 |
sender_subject: str
|
| 35 |
sender_email_body: str
|
|
@@ -37,13 +53,12 @@ class EmailProcessRequest(BaseModel):
|
|
| 37 |
|
| 38 |
class ReviewActionRequest(BaseModel):
|
| 39 |
thread_id: str
|
| 40 |
-
|
| 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 |
|
|
@@ -67,24 +82,38 @@ def parse_interrupt(final_state: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
| 67 |
|
| 68 |
# --- Endpoints ---
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
@app.post("/process-email")
|
| 71 |
-
def process_email(request: EmailProcessRequest, db: Session = Depends(get_session)) -> Dict[str, Any]:
|
| 72 |
"""Process email through the graph pipeline."""
|
| 73 |
|
| 74 |
try:
|
| 75 |
-
|
| 76 |
|
| 77 |
thread_id = request.thread_id
|
| 78 |
config = {
|
| 79 |
"configurable": {
|
| 80 |
"thread_id": thread_id,
|
| 81 |
-
"user_id": str(
|
| 82 |
}
|
| 83 |
}
|
| 84 |
|
| 85 |
input_data = {
|
| 86 |
-
"user_email_id":
|
| 87 |
-
"user_id":
|
| 88 |
"user_name": "Atharva",
|
| 89 |
"sender_email_id": request.sender_email_id,
|
| 90 |
"sender_subject": request.sender_subject,
|
|
@@ -123,14 +152,14 @@ def process_email(request: EmailProcessRequest, db: Session = Depends(get_sessio
|
|
| 123 |
|
| 124 |
|
| 125 |
@app.post("/review-action")
|
| 126 |
-
def review_action(request: ReviewActionRequest) -> Dict[str, Any]:
|
| 127 |
"""Resume graph execution based on user review."""
|
| 128 |
|
| 129 |
try:
|
| 130 |
config = {
|
| 131 |
"configurable": {
|
| 132 |
"thread_id": request.thread_id,
|
| 133 |
-
"user_id":
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
|
@@ -182,12 +211,12 @@ def review_action(request: ReviewActionRequest) -> Dict[str, Any]:
|
|
| 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":
|
| 191 |
}
|
| 192 |
}
|
| 193 |
|
|
@@ -195,7 +224,7 @@ def send_email(request: SendEmailRequest) -> Dict[str, Any]:
|
|
| 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 {
|
|
@@ -210,4 +239,4 @@ def send_email(request: SendEmailRequest) -> Dict[str, Any]:
|
|
| 210 |
|
| 211 |
if __name__ == "__main__":
|
| 212 |
import uvicorn
|
| 213 |
-
uvicorn.run(app, host="127.0.0.1", port=
|
|
|
|
| 4 |
from langchain_core.messages import BaseMessage, HumanMessage
|
| 5 |
from langgraph.graph import add_messages
|
| 6 |
from langgraph.types import Command
|
|
|
|
| 7 |
import logging
|
| 8 |
from app.graph import graph
|
| 9 |
from app.state.state import EmailAgentState
|
|
|
|
| 11 |
from app.database.utils import get_or_create_user
|
| 12 |
from sqlalchemy.orm import Session
|
| 13 |
from app.database.connection import SessionLocal
|
| 14 |
+
from app.core.config import settings
|
| 15 |
+
from fastapi import Request
|
| 16 |
+
import os
|
| 17 |
+
from app.database.models import User
|
| 18 |
+
from app.core.auth import create_access_token,get_current_user
|
| 19 |
+
|
| 20 |
+
# CREATE GMAIL AUTH FILES FROM HF SECRETS
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
if not os.path.exists(settings.GMAIL_CREDENTIALS_PATH):
|
| 24 |
+
|
| 25 |
+
with open(settings.GMAIL_CREDENTIALS_PATH, "w") as f:
|
| 26 |
+
f.write(os.environ["GOOGLE_CREDENTIALS"])
|
| 27 |
+
|
| 28 |
+
if not os.path.exists(settings.GMAIL_TOKEN_PATH):
|
| 29 |
+
|
| 30 |
+
with open(settings.GMAIL_TOKEN_PATH, "w") as f:
|
| 31 |
+
f.write(os.environ["GOOGLE_TOKEN"])
|
| 32 |
|
| 33 |
|
| 34 |
logger = logging.getLogger(__name__)
|
|
|
|
| 46 |
|
| 47 |
class EmailProcessRequest(BaseModel):
|
| 48 |
thread_id: str
|
|
|
|
| 49 |
sender_email_id: EmailStr
|
| 50 |
sender_subject: str
|
| 51 |
sender_email_body: str
|
|
|
|
| 53 |
|
| 54 |
class ReviewActionRequest(BaseModel):
|
| 55 |
thread_id: str
|
| 56 |
+
|
| 57 |
status: str # "approved" or "rejected"
|
| 58 |
feedback: Optional[str] = None
|
| 59 |
|
| 60 |
class SendEmailRequest(BaseModel):
|
| 61 |
thread_id: str
|
|
|
|
| 62 |
human_message: str
|
| 63 |
# --- Helper Functions ---
|
| 64 |
|
|
|
|
| 82 |
|
| 83 |
# --- Endpoints ---
|
| 84 |
|
| 85 |
+
|
| 86 |
+
@app.post("/get-user-data")
|
| 87 |
+
def get_user_data(user_email: EmailStr, db: Session = Depends(get_session)):
|
| 88 |
+
"""Get user data by email."""
|
| 89 |
+
user = get_or_create_user(db, user_email)
|
| 90 |
+
|
| 91 |
+
token = create_access_token({
|
| 92 |
+
"id": user.id,
|
| 93 |
+
"email": user_email
|
| 94 |
+
})
|
| 95 |
+
return {"user_id": str(user.id), "email": user.email, "token": token}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
|
| 99 |
@app.post("/process-email")
|
| 100 |
+
def process_email(request: EmailProcessRequest, db: Session = Depends(get_session), current_user: User = Depends(get_current_user)) -> Dict[str, Any]:
|
| 101 |
"""Process email through the graph pipeline."""
|
| 102 |
|
| 103 |
try:
|
| 104 |
+
|
| 105 |
|
| 106 |
thread_id = request.thread_id
|
| 107 |
config = {
|
| 108 |
"configurable": {
|
| 109 |
"thread_id": thread_id,
|
| 110 |
+
"user_id": str(current_user.id)
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
input_data = {
|
| 115 |
+
"user_email_id": current_user.email,
|
| 116 |
+
"user_id": current_user.id,
|
| 117 |
"user_name": "Atharva",
|
| 118 |
"sender_email_id": request.sender_email_id,
|
| 119 |
"sender_subject": request.sender_subject,
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
@app.post("/review-action")
|
| 155 |
+
def review_action(request: ReviewActionRequest,db: Session = Depends(get_session), current_user: User = Depends(get_current_user)) -> Dict[str, Any]:
|
| 156 |
"""Resume graph execution based on user review."""
|
| 157 |
|
| 158 |
try:
|
| 159 |
config = {
|
| 160 |
"configurable": {
|
| 161 |
"thread_id": request.thread_id,
|
| 162 |
+
"user_id": current_user.id
|
| 163 |
}
|
| 164 |
}
|
| 165 |
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
@app.post("/send_email")
|
| 214 |
+
def send_email(request: SendEmailRequest,db: Session = Depends(get_session),current_user: User = Depends(get_current_user)) -> Dict[str, Any]:
|
| 215 |
|
| 216 |
config = {
|
| 217 |
"configurable": {
|
| 218 |
"thread_id": request.thread_id,
|
| 219 |
+
"user_id": current_user.id
|
| 220 |
}
|
| 221 |
}
|
| 222 |
|
|
|
|
| 224 |
config,
|
| 225 |
{"messages": [HumanMessage(content=request.human_message)]},
|
| 226 |
as_node="prepare_context_node"
|
| 227 |
+
)
|
| 228 |
final_state = graph.invoke(None, config=config)
|
| 229 |
|
| 230 |
return {
|
|
|
|
| 239 |
|
| 240 |
if __name__ == "__main__":
|
| 241 |
import uvicorn
|
| 242 |
+
uvicorn.run(app, host="127.0.0.1", port=8080)
|
app/tools/email_writing_agent_tools.py
CHANGED
|
@@ -7,9 +7,10 @@ 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(
|
| 15 |
to: Union[str, list],
|
|
@@ -31,8 +32,8 @@ def create_gmail_draft(
|
|
| 31 |
"data": {"to": to, "subject": subject, "body": body}
|
| 32 |
})
|
| 33 |
|
| 34 |
-
|
| 35 |
-
draft_tool = [t for t in
|
| 36 |
|
| 37 |
# 2. Handle Logic
|
| 38 |
if response.get("status") == "approved":
|
|
@@ -61,14 +62,15 @@ def create_gmail_draft(
|
|
| 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 |
-
|
| 71 |
-
result =
|
| 72 |
userId="me", body={"id": runtime.state["draft_id"]}
|
| 73 |
).execute()
|
| 74 |
|
|
|
|
| 7 |
from langchain_core.tools import InjectedToolCallId, tool
|
| 8 |
from langchain.tools import ToolRuntime
|
| 9 |
from langgraph.types import Command
|
|
|
|
| 10 |
from langgraph.graph import END
|
| 11 |
+
from langchain_core.messages import ToolMessage,SystemMessage, HumanMessage
|
| 12 |
|
| 13 |
+
from app.gmail_auth import gmail_toolkit ,api_resource
|
| 14 |
@tool(args_schema=CreateDraftSchema)
|
| 15 |
def create_gmail_draft(
|
| 16 |
to: Union[str, list],
|
|
|
|
| 32 |
"data": {"to": to, "subject": subject, "body": body}
|
| 33 |
})
|
| 34 |
|
| 35 |
+
|
| 36 |
+
draft_tool = [t for t in gmail_toolkit.get_tools() if t.name == "create_gmail_draft"][0]
|
| 37 |
|
| 38 |
# 2. Handle Logic
|
| 39 |
if response.get("status") == "approved":
|
|
|
|
| 62 |
#---------------------------------------------------------------------------
|
| 63 |
|
| 64 |
|
| 65 |
+
@tool
|
| 66 |
def send_draft(
|
| 67 |
tool_call_id: Annotated[str, InjectedToolCallId],runtime: ToolRuntime # Injected ID
|
| 68 |
):
|
| 69 |
"""Sends a finalized Gmail draft by its ID."""
|
| 70 |
|
| 71 |
try:
|
| 72 |
+
|
| 73 |
+
result = gmail_toolkit.api_resource.users().drafts().send(
|
| 74 |
userId="me", body={"id": runtime.state["draft_id"]}
|
| 75 |
).execute()
|
| 76 |
|
authenticate_gmail.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
| 3 |
+
|
| 4 |
+
# Gmail API scope
|
| 5 |
+
SCOPES = [
|
| 6 |
+
"https://mail.google.com/"
|
| 7 |
+
]
|
| 8 |
+
|
| 9 |
+
def run_authentication():
|
| 10 |
+
print("Starting Google OAuth Authentication Flow...")
|
| 11 |
+
|
| 12 |
+
if not os.path.exists("credentials.json"):
|
| 13 |
+
print("ERROR: Please place your 'credentials.json' file in this folder first!")
|
| 14 |
+
return
|
| 15 |
+
|
| 16 |
+
flow = InstalledAppFlow.from_client_secrets_file(
|
| 17 |
+
"credentials.json",
|
| 18 |
+
SCOPES
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
creds = flow.run_local_server(port=8080)
|
| 22 |
+
|
| 23 |
+
with open("token.json", "w") as token_file:
|
| 24 |
+
token_file.write(creds.to_json())
|
| 25 |
+
|
| 26 |
+
print("\nSUCCESS: 'token.json' has been created successfully!")
|
| 27 |
+
|
| 28 |
+
if __name__ == "__main__":
|
| 29 |
+
run_authentication()
|
requirements.txt
CHANGED
|
@@ -14,3 +14,6 @@ psycopg-pool
|
|
| 14 |
transformers
|
| 15 |
pydantic-settings
|
| 16 |
email-validator
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
transformers
|
| 15 |
pydantic-settings
|
| 16 |
email-validator
|
| 17 |
+
itsdangerous
|
| 18 |
+
python-jose[cryptography]
|
| 19 |
+
passlib[bcrypt]
|