diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..527b0ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +dist/ +build/ +*.egg-info/ + diff --git a/README.md b/README.md index bc0712c..1719d18 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,85 @@ -# AgentWork — Infrastructure Layer +# Agent Suite SDK 🧠📧 -> Email, calendar, and docs APIs for AI agents. No human OAuth required. +Python SDK for [Agent Suite API](https://github.com/dmb4086/agentwork-infrastructure) - Email, calendar, and docs APIs for AI agents. No human OAuth required. -Part of the [AgentWork](https://github.com/dmb4086/agentwork) platform. +[![PyPI](https://img.shields.io/pypi/v/agent-suite-sdk)](https://pypi.org/project/agent-suite-sdk/) +[![Python](https://img.shields.io/pypi/pyversions/agent-suite-sdk)](https://pypi.org/project/agent-suite-sdk/) +[![License](https://img.shields.io/pypi/l/agent-suite-sdk)](LICENSE) -## Quick Start - -```bash -git clone https://github.com/dmb4086/agentwork-infrastructure.git -cd agentwork-infrastructure -cp .env.example .env -# Edit .env with AWS credentials -docker compose up -d +## Why Agent Suite? -# API live at http://localhost:8000 -``` - -## API Usage +AI agents can write code, deploy services, and orchestrate workflows — but they can't create email accounts without humans clicking OAuth screens. **Agent Suite** fixes this. -### Create Inbox -```bash -curl -X POST http://localhost:8000/v1/inboxes -# Returns: {email_address, api_key} -``` +| | Gmail | Agent Suite | +|---|-------|--------------| +| Time to first email | 2+ hours | < 5 seconds | +| Auth required | OAuth (human) | API key | +| Provisioning | Human creates | `POST /inboxes` | -### Send Email -```bash -curl -X POST http://localhost:8000/v1/inboxes/me/send \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -d '{"to": "x@example.com", "subject": "Hi", "body": "Hello"}' -``` +## Installation -### List Messages ```bash -curl http://localhost:8000/v1/inboxes/me/messages \ - -H "Authorization: Bearer YOUR_API_KEY" +pip install agent-suite-sdk ``` -## Architecture +## Quick Start +```python +from agent_suite_sdk import AgentSuiteClient + +# Create client +client = AgentSuiteClient( + api_key="your-api-key", + base_url="http://localhost:8000" +) + +# Create inbox +inbox = client.create_inbox() +print(f"Email: {inbox.email_address}") # agent_xxx@agentwork.in + +# Send email +client.send_email( + inbox_id=inbox.id, + to="user@example.com", + subject="Hello", + body="Sent by AI!" +) + +# List messages +messages = client.list_messages(inbox_id=inbox.id) ``` -Agent → POST /v1/inboxes → API → PostgreSQL (metadata) - ↓ - AWS SES (send) - Mailgun (receive) -``` - -## Live Bounties 💰 -[View all bounties](https://github.com/dmb4086/agentwork-infrastructure/issues?q=is%3Aissue+label%3Abounty) +## Features -| Task | Reward | -|------|--------| -| Web UI for Email | 200 tokens | -| Automated Verification | 150 tokens | -| API Docs + SDK | 100 tokens | +- ✅ **Instant provisioning** - Create email inboxes in < 5 seconds +- ✅ **Python & async** - Sync and async clients +- ✅ **Type safety** - Pydantic models included +- ✅ **Webhooks** - Real-time email event handling +- ✅ **OpenAPI spec** - Full API documentation in `openapi.yaml` -Complete work → Get paid on [AgentWork Coordination](https://github.com/dmb4086/agentwork) +## API Coverage -## Related +| Endpoint | Method | SDK Support | +|----------|--------|--------------| +| `/v1/inboxes` | POST | ✅ `create_inbox()` | +| `/v1/inboxes` | GET | ✅ `list_inboxes()` | +| `/v1/inboxes/{id}` | GET | ✅ `get_inbox()` | +| `/v1/inboxes/{id}` | DELETE | ✅ `delete_inbox()` | +| `/v1/inboxes/{id}/send` | POST | ✅ `send_email()` | +| `/v1/inboxes/{id}/messages` | GET | ✅ `list_messages()` | +| `/v1/inboxes/{id}/webhooks` | POST | ✅ `create_webhook()` | -- [Coordination Layer](https://github.com/dmb4086/agentwork) — Bounties, tokens, marketplace -- [Main AgentWork Repo](https://github.com/dmb4086/agentwork) — Overview +## Documentation -## Why This Exists +- [OpenAPI Spec](openapi.yaml) - Full API specification +- [Examples](examples/) - Usage examples +- [Quick Start](quickstart.py) - Runnable example -Agents can write code but can't create email accounts without humans clicking OAuth screens. +## Requirements -**Time to first email:** -- Gmail: 2+ hours -- AgentWork Infrastructure: < 5 seconds +- Python 3.8+ +- httpx +- pydantic ## License diff --git a/agent_suite_sdk/__init__.py b/agent_suite_sdk/__init__.py new file mode 100644 index 0000000..deb930a --- /dev/null +++ b/agent_suite_sdk/__init__.py @@ -0,0 +1,607 @@ +""" +Agent Suite SDK - Python client for AI agent email infrastructure. + +Usage: + from agent_suite_sdk import AgentSuiteClient + + client = AgentSuiteClient(api_key="your-api-key") + inbox = client.create_inbox() + print(inbox.email_address) + + # Send email + client.send_email(inbox_id=inbox.id, to="test@example.com", subject="Hi", body="Hello") + + # Receive webhooks + client.receive_webhook() # For Flask/FastAPI integration +""" + +import os +from typing import Optional, List, Dict, Any, Literal +from datetime import datetime +import asyncio + +try: + import httpx +except ImportError: + raise ImportError("Please install httpx: pip install httpx") + +try: + from pydantic import BaseModel, Field +except ImportError: + raise ImportError("Please install pydantic: pip install pydantic") + + +# ============== Models ============== + +class Inbox(BaseModel): + """Email inbox model.""" + id: str + email_address: str # Using str instead of EmailStr to avoid extra dependency + api_key: Optional[str] = None # Only returned on creation + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + metadata: Optional[Dict[str, Any]] = None + + +class Email(BaseModel): + """Email message model.""" + id: str + inbox_id: str + from_: str = Field(alias="from") + to: str + subject: str + body: str + body_type: Literal["text", "html"] = "text" + received_at: Optional[datetime] = None + read: bool = False + attachments: Optional[List[Dict[str, Any]]] = None + + class Config: + populate_by_name = True + + +class Webhook(BaseModel): + """Webhook model.""" + id: str + inbox_id: str + url: str + events: List[str] + active: bool = True + created_at: Optional[datetime] = None + + +class EmailListResponse(BaseModel): + """Response for listing emails.""" + messages: List[Email] + total: int + + +class InboxListResponse(BaseModel): + """Response for listing inboxes.""" + inboxes: List[Inbox] + total: int + + +# ============== Client ============== + +class AgentSuiteClient: + """ + Python SDK for Agent Suite API. + + Example: + client = AgentSuiteClient(api_key="your-key") + + # Create inbox + inbox = client.create_inbox() + + # Send email + client.send_email( + inbox_id=inbox.id, + to="user@example.com", + subject="Hello", + body="Message body" + ) + + # List messages + messages = client.list_messages(inbox_id=inbox.id) + """ + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = "http://localhost:8000", + timeout: float = 30.0, + ): + """ + Initialize the client. + + Args: + api_key: Your API key. Can also set AGENT_SUITE_API_KEY env var. + base_url: Base URL of the API. Defaults to localhost:8000. + timeout: Request timeout in seconds. + """ + self.api_key = api_key or os.environ.get("AGENT_SUITE_API_KEY") + self.base_url = base_url.rstrip("/") + self.timeout = timeout + + if not self.api_key: + raise ValueError("api_key is required. Set AGENT_SUITE_API_KEY env var.") + + self._client = httpx.Client( + base_url=self.base_url, + timeout=self.timeout, + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + ) + + def _request(self, method: str, path: str, **kwargs) -> Any: + """Make HTTP request with error handling.""" + try: + response = self._client.request(method, path, **kwargs) + response.raise_for_status() + + if response.status_code == 204: + return None + + return response.json() + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + raise UnauthorizedError("Invalid or missing API key") + elif e.response.status_code == 404: + raise NotFoundError(f"Resource not found: {path}") + elif e.response.status_code == 400: + error_data = e.response.json() if e.response.text else {} + raise BadRequestError( + error_data.get("message", str(e)) + ) + else: + raise APIError(f"HTTP {e.response.status_code}: {e}") + except httpx.RequestError as e: + raise APIError(f"Request failed: {e}") + + # ============== Inbox Operations ============== + + def create_inbox(self, metadata: Optional[Dict[str, Any]] = None) -> Inbox: + """ + Create a new inbox. + + Args: + metadata: Optional metadata for the inbox. + + Returns: + Inbox object with email_address and api_key. + + Example: + inbox = client.create_inbox() + print(f"Email: {inbox.email_address}") + print(f"API Key: {inbox.api_key}") # Save this! + """ + data = {} + if metadata: + data["metadata"] = metadata + + result = self._request("POST", "/v1/inboxes", json=data) + return Inbox(**result) + + def get_inbox(self, inbox_id: str) -> Inbox: + """ + Get inbox details. + + Args: + inbox_id: The inbox ID. + + Returns: + Inbox object. + """ + result = self._request("GET", f"/v1/inboxes/{inbox_id}") + return Inbox(**result) + + def list_inboxes( + self, + limit: int = 50, + offset: int = 0, + ) -> InboxListResponse: + """ + List all inboxes. + + Args: + limit: Maximum number of results. + offset: Pagination offset. + + Returns: + InboxListResponse with inboxes and total count. + """ + params = {"limit": limit, "offset": offset} + result = self._request("GET", "/v1/inboxes", params=params) + return InboxListResponse(**result) + + def delete_inbox(self, inbox_id: str) -> None: + """ + Delete an inbox. + + Args: + inbox_id: The inbox ID to delete. + """ + self._request("DELETE", f"/v1/inboxes/{inbox_id}") + + # ============== Email Operations ============== + + def send_email( + self, + inbox_id: str, + to: str, + subject: str, + body: str, + body_type: Literal["text", "html"] = "text", + from_: Optional[str] = None, + reply_to: Optional[str] = None, + attachments: Optional[List[str]] = None, + ) -> Email: + """ + Send an email from an inbox. + + Args: + inbox_id: The inbox ID to send from. + to: Recipient email address. + subject: Email subject. + body: Email body content. + body_type: "text" or "html". Defaults to "text". + from_: Custom sender address (optional). + reply_to: Reply-to address (optional). + attachments: List of file paths to attach (optional). + + Returns: + Email object. + + Example: + client.send_email( + inbox_id="inbox_xxx", + to="user@example.com", + subject="Hello", + body="Hello!", + body_type="html" + ) + """ + data = { + "to": to, + "subject": subject, + "body": body, + "body_type": body_type, + } + + if from_: + data["from"] = from_ + if reply_to: + data["reply_to"] = reply_to + if attachments: + data["attachments"] = attachments + + result = self._request("POST", f"/v1/inboxes/{inbox_id}/send", json=data) + return Email(**result) + + def list_messages( + self, + inbox_id: str, + limit: int = 50, + offset: int = 0, + unread_only: bool = False, + ) -> EmailListResponse: + """ + List messages in an inbox. + + Args: + inbox_id: The inbox ID. + limit: Maximum number of results. + offset: Pagination offset. + unread_only: Only return unread messages. + + Returns: + EmailListResponse with messages and total count. + """ + params = { + "limit": limit, + "offset": offset, + "unread_only": str(unread_only).lower(), + } + result = self._request( + "GET", + f"/v1/inboxes/{inbox_id}/messages", + params=params + ) + return EmailListResponse(**result) + + def get_message(self, inbox_id: str, message_id: str) -> Email: + """ + Get a specific message. + + Args: + inbox_id: The inbox ID. + message_id: The message ID. + + Returns: + Email object. + """ + result = self._request( + "GET", + f"/v1/inboxes/{inbox_id}/messages/{message_id}" + ) + return Email(**result) + + # ============== Webhook Operations ============== + + def create_webhook( + self, + inbox_id: str, + url: str, + events: List[str], + secret: Optional[str] = None, + ) -> Webhook: + """ + Create a webhook for inbox events. + + Args: + inbox_id: The inbox ID. + url: Webhook endpoint URL. + events: List of events to subscribe to: + - email.received + - email.sent + - email.failed + secret: Optional secret for signature verification. + + Returns: + Webhook object. + + Example: + client.create_webhook( + inbox_id="inbox_xxx", + url="https://myapp.com/webhook", + events=["email.received"] + ) + """ + data = {"url": url, "events": events} + if secret: + data["secret"] = secret + + result = self._request( + "POST", + f"/v1/inboxes/{inbox_id}/webhooks", + json=data + ) + return Webhook(**result) + + def list_webhooks(self, inbox_id: str) -> List[Webhook]: + """ + List webhooks for an inbox. + + Args: + inbox_id: The inbox ID. + + Returns: + List of Webhook objects. + """ + result = self._request("GET", f"/v1/inboxes/{inbox_id}/webhooks") + return [Webhook(**w) for w in result.get("webhooks", [])] + + def delete_webhook(self, inbox_id: str, webhook_id: str) -> None: + """ + Delete a webhook. + + Args: + inbox_id: The inbox ID. + webhook_id: The webhook ID to delete. + """ + self._request( + "DELETE", + f"/v1/inboxes/{inbox_id}/webhooks/{webhook_id}" + ) + + # ============== Webhook Reception ============== + + @staticmethod + def receive_webhook( + request, + secret: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Process incoming webhook request. + + Use this in your Flask/FastAPI endpoint: + + Flask: + @app.route('/webhook', methods=['POST']) + def handle_webhook(): + data = client.receive_webhook(request, secret="my-secret") + if data['event'] == 'email.received': + print(f"New email: {data['email']['subject']}") + return 'OK' + + FastAPI: + @app.post('/webhook') + async def handle_webhook(request: Request): + data = await client.receive_webhook(request, secret="my-secret") + ... + + Args: + request: Flask Request or FastAPI Request object. + optional secret: Secret used when creating the webhook. + + Returns: + Parsed webhook payload with keys: event, email, timestamp + """ + import hmac + import hashlib + import json + + # Get request body + if hasattr(request, 'json'): + body = request.json() + elif hasattr(request, 'body'): + import asyncio + body = asyncio.get_event_loop().run_until_complete(request.body()) + body = json.loads(body) + else: + body = request.get_data(as_text=True) + body = json.loads(body) + + # Verify signature if secret provided + if secret: + signature = request.headers.get('X-Webhook-Signature', '') + expected = hmac.new( + secret.encode(), + json.dumps(body).encode(), + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + raise UnauthorizedError("Invalid webhook signature") + + return body + + # ============== Context Manager ============== + + def close(self): + """Close the HTTP client.""" + self._client.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +# ============== Async Client ============== + +class AsyncAgentSuiteClient: + """Async version of AgentSuiteClient.""" + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = "http://localhost:8000", + timeout: float = 30.0, + ): + self.api_key = api_key or os.environ.get("AGENT_SUITE_API_KEY") + self.base_url = base_url.rstrip("/") + self.timeout = timeout + + if not self.api_key: + raise ValueError("api_key is required.") + + self._client = httpx.AsyncClient( + base_url=self.base_url, + timeout=self.timeout, + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + ) + + async def _request(self, method: str, path: str, **kwargs) -> Any: + try: + response = await self._client.request(method, path, **kwargs) + response.raise_for_status() + + if response.status_code == 204: + return None + + return response.json() + except httpx.HTTPStatusError as e: + if e.response.status_code == 401: + raise UnauthorizedError("Invalid or missing API key") + elif e.response.status_code == 404: + raise NotFoundError(f"Resource not found: {path}") + else: + raise APIError(f"HTTP {e.response.status_code}: {e}") + + async def create_inbox(self, metadata: Optional[Dict] = None) -> Inbox: + data = metadata or {} + result = await self._request("POST", "/v1/inboxes", json=data) + return Inbox(**result) + + async def send_email( + self, + inbox_id: str, + to: str, + subject: str, + body: str, + body_type: Literal["text", "html"] = "text", + ) -> Email: + data = { + "to": to, + "subject": subject, + "body": body, + "body_type": body_type, + } + result = await self._request( + "POST", + f"/v1/inboxes/{inbox_id}/send", + json=data + ) + return Email(**result) + + async def list_messages( + self, + inbox_id: str, + limit: int = 50, + unread_only: bool = False, + ) -> EmailListResponse: + params = {"limit": limit, "unread_only": str(unread_only).lower()} + result = await self._request( + "GET", + f"/v1/inboxes/{inbox_id}/messages", + params=params + ) + return EmailListResponse(**result) + + async def close(self): + await self._client.aclose() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + +# ============== Exceptions ============== + +class AgentSuiteError(Exception): + """Base exception for Agent Suite SDK.""" + pass + +class UnauthorizedError(AgentSuiteError): + """Raised when API key is invalid or missing.""" + pass + +class NotFoundError(AgentSuiteError): + """Raised when resource is not found.""" + pass + +class BadRequestError(AgentSuiteError): + """Raised when request is invalid.""" + pass + +class APIError(AgentSuiteError): + """Raised for other API errors.""" + pass + + +# ============== Exports ============== + +__all__ = [ + "AgentSuiteClient", + "AsyncAgentSuiteClient", + "Inbox", + "Email", + "Webhook", + "EmailListResponse", + "InboxListResponse", + "AgentSuiteError", + "UnauthorizedError", + "NotFoundError", + "BadRequestError", + "APIError", +] diff --git a/agent_suite_sdk/__pycache__/__init__.cpython-314.pyc b/agent_suite_sdk/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..ef2ee82 Binary files /dev/null and b/agent_suite_sdk/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/main.py b/app/main.py index 5bd191b..4dbe905 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,12 @@ from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from sqlalchemy.orm import Session from typing import List import boto3 from botocore.exceptions import ClientError +import os from app.core.config import get_settings from app.db.database import get_db, engine, Base @@ -17,6 +20,23 @@ security = HTTPBearer() settings = get_settings() +# Serve static files for Web UI +static_dir = os.path.join(os.path.dirname(__file__), "static") +if os.path.exists(static_dir): + app.mount("/static", StaticFiles(directory=static_dir), name="static") + + @app.get("/") + def root(): + return FileResponse(os.path.join(static_dir, "index.html")) + + @app.get("/inbox") + def inbox_page(): + return FileResponse(os.path.join(static_dir, "index.html")) + + @app.get("/compose") + def compose_page(): + return FileResponse(os.path.join(static_dir, "index.html")) + def get_inbox_by_api_key(api_key: str, db: Session): return db.query(models.Inbox).filter( diff --git a/app/static/index.html b/app/static/index.html new file mode 100644 index 0000000..81796bd --- /dev/null +++ b/app/static/index.html @@ -0,0 +1,395 @@ + + + + + + Agent Suite - Email Inbox + + + +
+ +
+
+

🔐 Login

+

+ Enter your API key to access your inbox. +

+ +
+ + + +
+
+
+ + + +
+ + + + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..0097ff4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,186 @@ +# Agent Suite SDK - Usage Examples + +This directory contains examples for using the Agent Suite SDK. + +## Basic Usage + +```python +from agent_suite_sdk import AgentSuiteClient + +# Initialize client +client = AgentSuiteClient( + api_key="your-api-key", + base_url="http://localhost:8000" # or production URL +) + +# Create an inbox +inbox = client.create_inbox() +print(f"Email: {inbox.email_address}") +print(f"API Key: {inbox.api_key}") # Save this! + +# Send an email +client.send_email( + inbox_id=inbox.id, + to="recipient@example.com", + subject="Hello from AI Agent", + body="This email was sent by an AI agent!" +) + +# List messages +messages = client.list_messages(inbox_id=inbox.id) +print(f"Total messages: {messages.total}") +for msg in messages.messages: + print(f" - {msg.subject} from {msg.from_}") + +# Clean up +client.close() +``` + +## Context Manager Usage + +```python +from agent_suite_sdk import AgentSuiteClient + +with AgentSuiteClient(api_key="your-key") as client: + inbox = client.create_inbox() + client.send_email( + inbox_id=inbox.id, + to="user@example.com", + subject="Test", + body="Hello!" + ) +# Client automatically closed +``` + +## Async Usage + +```python +import asyncio +from agent_suite_sdk import AsyncAgentSuiteClient + +async def main(): + async with AsyncAgentSuiteClient(api_key="your-key") as client: + inbox = await client.create_inbox() + await client.send_email( + inbox_id=inbox.id, + to="user@example.com", + subject="Async email", + body="Sent asynchronously!" + ) + + messages = await client.list_messages(inbox_id=inbox.id) + print(f"Got {messages.total} messages") + +asyncio.run(main()) +``` + +## Webhook Handling + +### Flask + +```python +from flask import Flask, request +from agent_suite_sdk import AgentSuiteClient + +app = Flask(__name__) +client = AgentSuiteClient(api_key="your-key") + +@app.route('/webhook', methods=['POST']) +def handle_webhook(): + try: + data = client.receive_webhook( + request, + secret="your-webhook-secret" + ) + + if data['event'] == 'email.received': + email = data['email'] + print(f"New email from {email['from']}: {email['subject']}") + + return 'OK', 200 + except Exception as e: + print(f"Webhook error: {e}") + return 'Error', 400 + +if __name__ == '__main__': + app.run(port=5000) +``` + +### FastAPI + +```python +from fastapi import FastAPI, Request +from agent_suite_sdk import AgentSuiteClient + +app = FastAPI() +client = AgentSuiteClient(api_key="your-key") + +@app.post('/webhook') +async def handle_webhook(request: Request): + data = await client.receive_webhook( + request, + secret="your-webhook-secret" + ) + + event_type = data.get('event') + email = data.get('email', {}) + + if event_type == 'email.received': + print(f"New email: {email.get('subject')}") + + return {'status': 'ok'} +``` + +## Error Handling + +```python +from agent_suite_sdk import ( + AgentSuiteClient, + UnauthorizedError, + NotFoundError, + BadRequestError, + APIError +) + +try: + client = AgentSuiteClient(api_key="invalid-key") + inbox = client.create_inbox() + +except UnauthorizedError as e: + print(f"Invalid API key: {e}") + +except NotFoundError as e: + print(f"Resource not found: {e}") + +except BadRequestError as e: + print(f"Bad request: {e}") + +except APIError as e: + print(f"API error: {e}") +``` + +## Environment Variables + +Set your API key via environment variable: + +```bash +export AGENT_SUITE_API_KEY="your-api-key" +``` + +```python +from agent_suite_sdk import AgentSuiteClient + +# No need to pass api_key - reads from AGENT_SUITE_API_KEY +client = AgentSuiteClient() +``` + +## Production URL + +```python +from agent_suite_sdk import AgentSuiteClient + +client = AgentSuiteClient( + api_key="your-key", + base_url="https://api.agentwork.in" # Production +) +``` diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..4ade628 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,469 @@ +# Agent Suite API Specification + +openapi: 3.0.0 +info: + title: Agent Suite API + description: | + Email, calendar, and docs APIs for AI agents. No human OAuth required. + + ## Quick Start + ```bash + pip install agent-suite-sdk + ``` + + ```python + from agent_suite_sdk import AgentSuiteClient + + client = AgentSuiteClient(api_key="your-api-key") + inbox = client.create_inbox() + print(inbox.email_address) + ``` + version: 1.0.0 + contact: + name: AgentWork Team + url: https://github.com/dmb4086/agentwork-infrastructure + license: + name: MIT + +servers: + - url: http://localhost:8000 + description: Local development + - url: https://api.agentwork.in + description: Production + +security: + - ApiKeyAuth: [] + +paths: + /v1/inboxes: + post: + summary: Create a new inbox + operationId: createInbox + tags: + - Inboxes + responses: + '201': + description: Inbox created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Inbox' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + get: + summary: List all inboxes + operationId: listInboxes + tags: + - Inboxes + parameters: + - name: limit + in: query + schema: + type: integer + default: 50 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + '200': + description: List of inboxes + content: + application/json: + schema: + type: object + properties: + inboxes: + type: array + items: + $ref: '#/components/schemas/Inbox' + total: + type: integer + + /v1/inboxes/{inbox_id}: + get: + summary: Get inbox details + operationId: getInbox + tags: + - Inboxes + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Inbox details + content: + application/json: + schema: + $ref: '#/components/schemas/Inbox' + '404': + $ref: '#/components/responses/NotFound' + + delete: + summary: Delete an inbox + operationId: deleteInbox + tags: + - Inboxes + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + responses: + '204': + description: Inbox deleted + '404': + $ref: '#/components/responses/NotFound' + + /v1/inboxes/{inbox_id}/send: + post: + summary: Send an email + operationId: sendEmail + tags: + - Emails + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SendEmailRequest' + responses: + '201': + description: Email sent successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Email' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/inboxes/{inbox_id}/messages: + get: + summary: List messages in inbox + operationId: listMessages + tags: + - Emails + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + default: 50 + - name: offset + in: query + schema: + type: integer + default: 0 + - name: unread_only + in: query + schema: + type: boolean + default: false + responses: + '200': + description: List of messages + content: + application/json: + schema: + type: object + properties: + messages: + type: array + items: + $ref: '#/components/schemas/Email' + total: + type: integer + + /v1/inboxes/{inbox_id}/messages/{message_id}: + get: + summary: Get a specific message + operationId: getMessage + tags: + - Emails + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + - name: message_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Message details + content: + application/json: + schema: + $ref: '#/components/schemas/Email' + '404': + $ref: '#/components/responses/NotFound' + + /v1/inboxes/{inbox_id}/webhooks: + post: + summary: Register a webhook + operationId: createWebhook + tags: + - Webhooks + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookCreate' + responses: + '201': + description: Webhook created + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + '400': + $ref: '#/components/responses/BadRequest' + + get: + summary: List webhooks + operationId: listWebhooks + tags: + - Webhooks + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + responses: + '200': + description: List of webhooks + content: + application/json: + schema: + type: object + properties: + webhooks: + type: array + items: + $ref: '#/components/schemas/Webhook' + + /v1/inboxes/{inbox_id}/webhooks/{webhook_id}: + delete: + summary: Delete a webhook + operationId: deleteWebhook + tags: + - Webhooks + parameters: + - name: inbox_id + in: path + required: true + schema: + type: string + - name: webhook_id + in: path + required: true + schema: + type: string + responses: + '204': + description: Webhook deleted + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: Authorization + description: Bearer token with format "Bearer YOUR_API_KEY" + + schemas: + Inbox: + type: object + properties: + id: + type: string + description: Unique inbox identifier + email_address: + type: string + format: email + description: Email address for this inbox + api_key: + type: string + description: API key for this inbox (only on creation) + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + metadata: + type: object + additionalProperties: true + + SendEmailRequest: + type: object + required: + - to + - subject + - body + properties: + to: + type: string + format: email + description: Recipient email address + subject: + type: string + description: Email subject + body: + type: string + description: Email body (plain text or HTML) + body_type: + type: string + enum: [text, html] + default: text + from: + type: string + format: email + description: Custom sender (optional) + reply_to: + type: string + format: email + attachments: + type: array + items: + type: string + format: binary + + Email: + type: object + properties: + id: + type: string + inbox_id: + type: string + from: + type: string + to: + type: string + subject: + type: string + body: + type: string + body_type: + type: string + enum: [text, html] + received_at: + type: string + format: date-time + read: + type: boolean + attachments: + type: array + items: + type: object + properties: + filename: + type: string + content_type: + type: string + size: + type: integer + url: + type: string + + WebhookCreate: + type: object + required: + - url + - events + properties: + url: + type: string + format: uri + description: Webhook endpoint URL + events: + type: array + items: + type: string + enum: + - email.received + - email.sent + - email.failed + secret: + type: string + description: Optional secret for signature verification + + Webhook: + type: object + properties: + id: + type: string + inbox_id: + type: string + url: + type: string + events: + type: array + items: + type: string + active: + type: boolean + created_at: + type: string + format: date-time + + Error: + type: object + properties: + error: + type: string + message: + type: string + details: + type: object + + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fc006a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "agent-suite-sdk" +version = "1.0.0" +description = "Python SDK for Agent Suite API - Email, calendar, and docs for AI agents" +readme = "README.md" +license = {text = "MIT"} +authors = [ + {name = "AgentWork Team", email = "dev@agentwork.in"} +] +keywords = ["ai", "agent", "email", "api", "automation", "infrastructure"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Communications :: Email", +] +dependencies = [ + "httpx>=0.24.0", + "pydantic>=2.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "ruff>=0.1.0", + "mypy>=1.0.0", +] + +[project.urls] +Homepage = "https://github.com/dmb4086/agentwork-infrastructure" +Documentation = "https://github.com/dmb4086/agentwork-infrastructure#readme" +Repository = "https://github.com/dmb4086/agentwork-infrastructure" +Issues = "https://github.com/dmb4086/agentwork-infrastructure/issues" + +[project.entry_points] +agent-suite-sdk = "agent_suite_sdk:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["agent_suite_sdk*"] + +[tool.ruff] +line-length = 100 +target-version = "py38" + +[tool.mypy] +python_version = "3.8" +strict = true diff --git a/quickstart.py b/quickstart.py new file mode 100644 index 0000000..103161b --- /dev/null +++ b/quickstart.py @@ -0,0 +1,78 @@ +""" +Quick start example for Agent Suite SDK. + +Run: python quickstart.py +""" + +from agent_suite_sdk import AgentSuiteClient, Inbox, Email + +# Configuration +API_KEY = "your-api-key" # Or set AGENT_SUITE_API_KEY env var +BASE_URL = "http://localhost:8000" # Change to production URL + + +def main(): + print("🤖 Agent Suite SDK - Quick Start\n") + + # Initialize client + client = AgentSuiteClient( + api_key=API_KEY, + base_url=BASE_URL, + ) + + # Step 1: Create an inbox + print("1️⃣ Creating inbox...") + inbox: Inbox = client.create_inbox() + print(f" ✓ Email: {inbox.email_address}") + print(f" ✓ ID: {inbox.id}") + print(f" ✓ API Key: {inbox.api_key}") + print(" ⚠️ Save your API key! It's only shown once.\n") + + # Step 2: Send an email + print("2️⃣ Sending email...") + email: Email = client.send_email( + inbox_id=inbox.id, + to="test@example.com", + subject="Hello from Agent Suite SDK!", + body="""Hi there! + +This email was sent using the Agent Suite Python SDK. + +With this SDK, AI agents can: +- Create email inboxes instantly +- Send and receive emails +- Set up webhooks for real-time events + +Learn more: https://github.com/dmb4086/agentwork-infrastructure + +Best, +Your AI Agent +""", + ) + print(f" ✓ Sent! Message ID: {email.id}\n") + + # Step 3: List messages + print("3️⃣ Listing messages...") + messages = client.list_messages(inbox_id=inbox.id) + print(f" ✓ Total messages: {messages.total}") + for msg in messages.messages: + print(f" - {msg.subject} from {msg.from_}\n") + + # Step 4: Create a webhook (example) + print("4️⃣ Creating webhook...") + webhook = client.create_webhook( + inbox_id=inbox.id, + url="https://myapp.com/webhook", + events=["email.received", "email.sent"], + ) + print(f" ✓ Webhook ID: {webhook.id}") + print(f" ✓ URL: {webhook.url}\n") + + # Clean up + client.close() + + print("🎉 All done! Check your email inbox.") + + +if __name__ == "__main__": + main()