An AI-powered sales email generation and delivery system showcasing enterprise-grade agentic patterns—multi-agent orchestration, safety guardrails, CSV-driven personalization, and async email delivery.
This is a portfolio project demonstrating practical AI engineering: building with the official OpenAI Python SDK against local LLM inference (Ollama) or cloud APIs, designing safety-critical workflows, and coordinating autonomous agents to solve real business problems.
The Sales Agent system guides users through a complete workflow:
- Compose a sales request + upload recipient list (CSV)
- Generate multiple draft emails via competing specialist agents (professional, humorous, concise)
- Score and auto-select the best draft
- Approve and mail-merge personalized tokens
- Send via SendGrid with delivery tracking
- Multi-Agent Orchestration: A central Sales Manager agent delegates to three specialist writing agents, each with unique personas. They compete via automated scoring to produce the best output.
- Flexible LLM Backend: Uses the OpenAI Python SDK with configurable
base_urland API keys—switch seamlessly between local Ollama, OpenAI-hosted models, or other compatible endpoints. - Safety Guardrails: Dual guardrail system prevents security risks:
- Input guardrails detect prompt injection and PII exposure
- Output guardrails catch credential/secret leaks
- Personalized Mail Merge: Recipients are extracted from CSV; approved emails auto-fill
[Recipient Name],[Your Name], and other tokens before sending. - Human Approval Loop: Users explicitly approve before email delivery, enabling rejection and regeneration at any stage.
- Observability: Structured JSON logging tracks guardrail decisions, agent reasoning, and SendGrid delivery status.
User Input + CSV
↓
[Input Guardrail] → Check for prompt injection & PII
↓
[Sales Manager Agent] → Coordinates three specialists
├─ Professional Agent (Mistral)
├─ Humorous Agent (Llama 3.2)
└─ Concise Agent (Qwen 2.5)
↓
[Candidate Scoring] → Rank drafts, select best match
↓
[Output Guardrail] → Verify no secrets leak
↓
[User Approval] → Review & approve or regenerate
↓
[Email Manager Agent] → Mail-merge tokens per recipient
↓
[SendGrid] → Send personalized emails
↓
[Delivery Report] → Log success/failure per recipient
- Python 3.12+
- Local LLM (or cloud API):
- Ollama with models:
mistral:7b,qwen2.5:3b,llama3.2:3b(or adjust inagent_setup.py) - OR OpenAI API account
- Ollama with models:
- SendGrid account and API key
- UV package manager (or pip)
-
Clone the repo and install dependencies:
cd Portfolio-SalesAgent uv sync # or: pip install -r requirements.txt
-
Create
.envfile in the root directory:# LLM Configuration (Ollama local endpoint) LLM_API_URL=http://localhost:11434/v1 LLM_API_KEY=ollama # Dummy key for local Ollama # OR use OpenAI cloud API # LLM_API_URL=https://api.openai.com/v1 # LLM_API_KEY=sk-... # Email Configuration SENDGRID_API_KEY=sg-... FROM_EMAIL=your-sales-email@company.com TO_EMAIL=default-recipient@example.com # Optional # Safety Thresholds (optional, defaults provided) RISK_THRESHOLD=0.75 TOXICITY_THRESHOLD=0.8
-
Start Ollama (if using local models):
ollama serve # In another terminal, pull models: # ollama pull mistral:7b # ollama pull qwen2.5:3b # ollama pull llama3.2:3b
-
Run the Gradio UI:
python interface.py
The UI will open at
http://localhost:7860(or the next available port). -
Use the application:
- Compose your sales message
- Provide a CSV with columns:
nameandemail - Click Generate and review candidate scores
- Click Approve & Send to deliver personalized emails
See Docs/Example-contacts.csv:
name,email
Alice Johnson,alice@techcorp.com
Bob Smith,bob@innovate.io
Carol White,carol@venture.comProtects against malicious input before agents process requests:
- Prompt Injection Detection: Identifies patterns like
ignore all previous instructions,you are now a,<|im_start|>system, etc. - PII Detection: Flags SSNs, credit cards, phone numbers, and email addresses
- Risk Scoring: Computes confidence; high-risk inputs are rejected with structured feedback
- Configurable Threshold:
RISK_THRESHOLDenv var controls sensitivity (default: 0.75)
Prevents secrets and credentials from leaking in generated emails:
- Secret Pattern Matching: Detects API keys, passwords, and common credential formats
- Structured Response: Returns pass/fail with detected patterns for logging
- Audit Trail: All guardrail decisions logged as JSON for compliance
Portfolio-SalesAgent/
├── agent_setup.py # Agent definitions, model initialization, tool registration
├── sales_manager.py # Central Sales Manager agent with guardrails
├── guardrails.py # Input/output guardrail implementations
├── email_service.py # SendGrid integration and email-sending tools
├── email_workflow.py # Agent orchestration, generation pipeline, sending logic
├── mail_merge.py # Text parsing, HTML normalization, and personalization
├── scoring.py # EmailCandidate model and quality scoring
├── interface.py # Gradio UI layer and callback wiring only
├── config.py # LLM client setup, environment validation
├── prompts.py # Instructions for all agents (professional, humorous, concise, etc.)
├── models.py # Pydantic models for guardrail outputs and agent responses
├── logger_config.py # Structured JSON logging configuration
├── email_logger.py # SendGrid delivery tracking and logging
├── pyproject.toml # Project metadata, dependencies (uv-compatible)
├── LICENSE # MIT License
├── README.md # This file
└── Docs/
├── img/
│ ├── sales_agent_flow.png # Architecture diagram
│ ├── screenshot1.png # UI: Compose stage
│ ├── screenshot2.png # UI: Review stage
│ └── screenshot3.png # UI: Approval/Send stage
└── Example-contacts.csv # Sample recipient list for testing
| Component | Technology | Notes |
|---|---|---|
| LLM Inference | OpenAI Python SDK | Compatible with Ollama, OpenAI, Azure OpenAI, etc. |
| Local Models | Ollama | Mistral 7B, Qwen 2.5, Llama 3.2 (configurable) |
| Agent Framework | OpenAI Agents SDK | Multi-step reasoning, tool use, guardrail hooks |
| UI/UX | Gradio 6.8+ | Interactive web interface for compose → review → approve flow |
| Email Delivery | SendGrid | Transactional email API with delivery tracking |
| Logging | python-json-logger | Structured JSON logs for observability and debugging |
| Config | python-dotenv | Environment-based configuration |
| Type Safety | Pydantic 2.12+ | Data validation for outputs and guardrail responses |
| Async/Await | asyncio | Non-blocking I/O for agent calls and email dispatch |
| Package Management | UV | Fast Python package installer and dependency resolver |
This project demonstrates:
- ✅ Modern AI Architecture: Multi-agent patterns with coordination, scoring, and decision-making
- ✅ SDK Flexibility: Show how to make OpenAI SDK work with any compatible endpoint (local LLM, cloud, self-hosted)
- ✅ Security-First Design: Dual guardrails, input validation, output scanning, structured audit logs
- ✅ User Experience: Gradio-based workflow balancing automation with human approval gates
- ✅ Production Readiness: Async execution, error handling, comprehensive logging, SendGrid integration
- ✅ Clean Code: Modular architecture, type hints, clear separation of concerns
| Issue | Cause | Solution |
|---|---|---|
Missing required environment variable 'LLM_API_KEY' |
.env file not loaded or missing |
Verify .env exists in root directory and load_dotenv() is called early in config.py |
ModuleNotFoundError: No module named 'agents' |
Missing OpenAI Agents SDK | Run uv sync or pip install openai-agents>=0.10.2 |
Connection refused on localhost:11434 |
Ollama not running | Start Ollama with ollama serve |
Model not found (e.g., mistral:7b) |
Model not pulled to local Ollama | Run ollama pull mistral:7b (and qwen2.5, llama3.2) |
SENDGRID_API_KEY errors |
Invalid or missing SendGrid key | Verify key format (sg-...) and SendGrid account is active |
| Guardrail errors after sending emails | PII or injection patterns in emails | Review guardrail patterns in guardrails.py; adjust RISK_THRESHOLD if too strict |
To extend this project:
- Add new writing agents: Create new
Agentinstances inagent_setup.pywith unique personas - Customize guardrails: Edit patterns in
guardrails.pyor add new guardrail functions - Switch LLM backends: Update
LLM_API_URLand model names in.env - Adjust scoring logic: Modify candidate evaluation in
scoring.py→_score_email() - Monitor email delivery: Check
email_logger.pyfor SendGrid webhook handling and status updates
Author: Ben Walker (BenRWalker@icloud.com)
License: MIT License © 2025 — See LICENSE for details.
Ready to generate your first sales email? Update your .env, provide a CSV of contacts, and click "Generate Email" in the Gradio UI!



