- agent_runner: local directory + cloud agent orchestration via Redis - 5 domain agents: filesystem, task, note, project, timeline - integrations: Gmail, MS Graph (Outlook + Teams) - journey: guided chatbot conversation to build prompt_template - routes: REST endpoints (catalog, can-create, trigger) - redis_consumer: subscribes to batch:request:* pattern - ws_context: Redis-based execute_on_client for tool round-trip - Dockerfile with 300s timeout for long-running batch jobs
109 lines
3.1 KiB
Python
109 lines
3.1 KiB
Python
"""Cloud provider integration utilities.
|
|
|
|
Adapted for Batch Agent Service: import from shared.config instead of app.config.
|
|
|
|
Provides:
|
|
* Shared message dataclasses (EmailMessage, ChatMessage)
|
|
* get_provider() — factory for Gmail/MS Graph clients
|
|
* encrypt_token() / decrypt_token() — Fernet-based OAuth token encryption
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING
|
|
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
|
|
from shared.config import settings
|
|
|
|
if TYPE_CHECKING:
|
|
from app.integrations.gmail import GmailClient
|
|
from app.integrations.ms_graph import MSGraphClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class EmailMessage:
|
|
id: str
|
|
subject: str
|
|
sender: str
|
|
body_text: str
|
|
date: datetime
|
|
labels: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def as_text(self) -> str:
|
|
date_str = self.date.strftime("%Y-%m-%d %H:%M")
|
|
labels_str = f" [{', '.join(self.labels)}]" if self.labels else ""
|
|
return (
|
|
f"From: {self.sender}\n"
|
|
f"Date: {date_str}{labels_str}\n"
|
|
f"Subject: {self.subject}\n\n"
|
|
f"{self.body_text}"
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ChatMessage:
|
|
id: str
|
|
content: str
|
|
sender: str
|
|
channel: str | None
|
|
date: datetime
|
|
|
|
@property
|
|
def as_text(self) -> str:
|
|
date_str = self.date.strftime("%Y-%m-%d %H:%M")
|
|
channel_str = f" [channel: {self.channel}]" if self.channel else ""
|
|
return (
|
|
f"From: {self.sender}\n"
|
|
f"Date: {date_str}{channel_str}\n\n"
|
|
f"{self.content}"
|
|
)
|
|
|
|
|
|
def _get_fernet() -> Fernet:
|
|
key = settings.OAUTH_ENCRYPTION_KEY
|
|
if not key:
|
|
raise RuntimeError(
|
|
"OAUTH_ENCRYPTION_KEY is not set. "
|
|
"Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
|
)
|
|
return Fernet(key.encode() if isinstance(key, str) else key)
|
|
|
|
|
|
def encrypt_token(token_info: dict) -> str:
|
|
if not isinstance(token_info, dict) or not token_info:
|
|
raise ValueError("token_info must be a non-empty dict")
|
|
plaintext = json.dumps(token_info).encode("utf-8")
|
|
return _get_fernet().encrypt(plaintext).decode("utf-8")
|
|
|
|
|
|
def decrypt_token(encrypted: str) -> dict:
|
|
try:
|
|
plaintext = _get_fernet().decrypt(encrypted.encode("utf-8"))
|
|
return json.loads(plaintext)
|
|
except (InvalidToken, json.JSONDecodeError) as exc:
|
|
raise ValueError(f"Failed to decrypt OAuth token: {exc}") from exc
|
|
|
|
|
|
def get_provider(
|
|
provider: str,
|
|
credentials_info: dict,
|
|
) -> "GmailClient | MSGraphClient":
|
|
if provider == "gmail":
|
|
from app.integrations.gmail import GmailClient
|
|
return GmailClient(credentials_info)
|
|
if provider in {"outlook", "teams"}:
|
|
from app.integrations.ms_graph import MSGraphClient
|
|
return MSGraphClient(credentials_info)
|
|
raise ValueError(
|
|
f"Unknown cloud provider {provider!r}. "
|
|
"Supported: 'gmail', 'outlook', 'teams'."
|
|
)
|