An enterprise-scale customer support automation system using coordinated AI agents to handle Tableau-related support requests for FinTech Analytics Corp.
Large financial institutions with 5,000+ Tableau users face:
- High support volume: Hundreds of daily tickets across multiple departments
- Complex prioritization: Trading issues need immediate attention vs. training questions
- Specialized knowledge: Different issue types require different expertise
- 24/7 availability: Global operations require round-the-clock support
- 🧭 Router Agent: Intelligent ticket classification and routing
- 🔧 Technical Support Agent: Tableau troubleshooting and solutions
- 👤 Account Management Agent: User access and licensing management
- Smart Classification: Automatic categorization by issue type and priority
- Department-Aware: Trading dept = Critical, others = contextual priority
- Specialist Routing: Technical vs. account issues handled by experts
- Escalation Logic: Complex issues automatically escalated to humans
- Enterprise Context: Realistic departmental data and business logic
- User Directory Lookup: ticket submission looks up the user's email against the real seeded directory and derives their department automatically, rather than asking them to self-report it
- Self-Learning Subject Ranking: the ticket form's subject dropdown is ranked by real historical submission frequency, not a static list — recurring issues climb the ranking over time
- Resolution Caching: a repeat issue (same subject as a prior resolved ticket) reuses that resolution directly instead of re-running knowledge base retrieval and the LLM
Running the Project with Docker (Recommended) This project uses Docker and Docker Compose to build and run all microservices and the Streamlit demo interface easily.
Prerequisites Install Docker Desktop (Windows/Mac) or Docker Engine and Docker Compose (Linux) Ensure Docker daemon is running Build and Start All Services From the root of the cloned repository (where your docker-compose.yml file is located), run: docker compose up --build
This command will:
Build Docker images for each AI agent and the Streamlit demo app
Start containers including Redis (message queue) and Postgres (persistence)
Run a one-off db-seed job that populates departments, a Faker-generated user
population, and the technical knowledge base before any agent starts
Map ports: Router Agent on localhost:8001 Technical Agent on localhost:8002 Account Agent on localhost:8003 Streamlit Demo Interface on localhost:8501 Access the Demo Interface
Open your browser and go to: http://localhost:8501
Use the interface to submit support tickets and see multi-agent coordination in action. Stop the System To stop and remove all containers, run: docker compose down
Running Locally Without Docker If you prefer to run services manually on your machine:
-
Install Dependencies pip install -r requirements.txt
-
Start Redis (required — agents no longer fall back silently if Redis is unreachable; they log a warning and report
degradedon their/healthendpoint, and escalation messages will not be recorded) redis-server (ordocker run -p 6379:6379 redis:7-alpine) -
Set up the database. By default
DATABASE_URLfalls back to a local SQLite file (sqlite:///./support.db) — nothing to install. To use Postgres instead, run one locally (ordocker run -p 5432:5432 -e POSTGRES_PASSWORD=... postgres:16-alpine) and exportDATABASE_URL=postgresql://user:pass@localhost:5432/dbname. Either way, seed it once: python -m scripts.seed_db -
(Optional) Enable the LLM layer. Copy
.env.exampleto.envand setOPENROUTER_API_KEY(free at https://openrouter.ai/keys) to turn on LLM-backed classification and response generation for the cases the rule engine can't confidently handle on its own. Without it, every agent runs in pure rules-only mode — nothing breaks, it's just less capable on ambiguous tickets. Docker Compose picks up.envautomatically; for local runs export the variables in your shell instead.(Optional) Enable internal-service auth. Set
INTERNAL_API_TOKEN(in the same.env, or exported) to require a shared-secret header between the demo app and the three agents. Unset by default — fine for local dev on your own machine; set it if you're exposing agent ports beyond localhost. -
Start All Agents (run each from the project root, using
-mso thesharedpackage resolves correctly) Terminal 1: Router Agent python -m agents.router_agent.main
Terminal 2: Technical Agent python -m agents.technical_agent.main
Terminal 3: Account Agent python -m agents.account_agent.main
- Launch Demo Interface streamlit run demo/streamlit_interface.py
pip install -r requirements.txt pytest
The suite covers the routing/classification, knowledge-base matching, and license-capacity
logic directly, plus FastAPI TestClient tests for each agent's HTTP endpoints (including
/health). No running Redis, Postgres, or Docker is required — the database layer is
exercised against an isolated in-memory SQLite database created fresh per test
(tests/conftest.py), and agents degrade gracefully when Redis is unreachable rather
than failing to start.
Lint: pip install -r requirements-dev.txt && ruff check . GitHub Actions
(.github/workflows/ci.yml) runs lint, tests, and a docker compose build on every push
and PR; an optional live LLM smoke test (scripts/llm_smoke_test.py) runs only when the
OPENROUTER_API_KEY repo secret is configured.
Tickets, departments, users, licenses, the technical knowledge base, and escalations are
persisted via SQLAlchemy models in shared/db/. scripts/seed_db.py populates the same
departmental data the original demo hardcoded, plus a Faker-generated user population
matching each department's user count — it's idempotent, so re-running it is a no-op once
seeded. Account-related reads/writes go through shared/tableau_service.py's
TableauBackend interface (SimulatedTableauBackend today); a future integration with the
real Tableau REST API can implement the same interface without touching agent code.
Each agent tries fast, deterministic keyword rules first; only when the rule signal is
genuinely weak does it fall through to an LLM call via OpenRouter (shared/llm_client.py).
This keeps the system fully functional — same answers as before — with OPENROUTER_API_KEY
unset, and adds real capability when it's configured:
- Router classifies with keyword scoring and computes a real confidence from the score margin; below a threshold, it asks the LLM for a second opinion. Business-rule priority floors (e.g. Trading/Risk/Executive → at least HIGH) apply to the LLM's suggestion exactly as they do to the rule engine's own default — that's policy, not something to infer.
- Technical agent first checks whether an earlier ticket with the exact same subject
already has a recorded resolution — if so, it reuses that resolution verbatim and skips
KB retrieval and the LLM entirely (
resolution_cache.py). Only on a cache miss does it retrieve the best-matching knowledge base articles and ask the LLM to write a grounded answer citing only those articles (RAG) — it's instructed to escalate rather than invent steps the KB doesn't support. If the LLM is unavailable, it falls back to serving the top article directly, with that article's own escalation flag — the same behavior the agent had before the LLM existed. - Account agent uses rule keywords to detect add/remove/permission requests (and
always extracts a literal email via regex — no LLM needed for that); only a request with
no rule match at all goes to the LLM for intent extraction. Execution is always
deterministic — capacity checks and provisioning run against
TableauBackend, never the model's judgment.
Anything an agent escalates lands in the Human Review tab, not a fire-and-forget queue.
For each pending escalation you see the full ticket context and the agent's draft response,
and can Approve & Send it as-is, edit it before sending, or Reject it (leaving the
ticket escalated for manual handling outside the system). Every decision is recorded as a
human_review ticket event — who reviewed it, what they decided, and the final text — so
there's a full audit trail from ticket submission through resolution.
- Service auth — a shared-secret
X-Internal-Tokenheader, checked by a FastAPI dependency (shared/auth.py) on every agent's business endpoint (/healthstays open for infra healthchecks). Opt-in viaINTERNAL_API_TOKEN; unset means auth is disabled, so local dev and the test suite don't need to know about it. - Structured logging — every log line is a JSON object (
shared/logging_config.py), and every log emitted while handling a ticket carries that ticket's ID, so you can grep one ticket's full story across the router, technical/account agent, and orchestrator logs. - LLM availability tracking — every
complete_json()attempt (success or failure, and why) is logged to the database and surfaced on the System Architecture tab, so you can see exactly how often the LLM layer is actually available versus falling back to rules.
Want a public URL instead of running locally? See docs/DEPLOYMENT.md
for step-by-step setup across three platforms — Neon (Postgres), Render (the three
agents), Streamlit Community Cloud (the demo UI) — chosen specifically because none of
them require a credit card, so there's no billing mechanism attached anywhere in the
stack that could ever charge you. Trade-off: Render's free services sleep after ~15 min
idle, so the first request after a quiet period is slow to wake up — fine for a
portfolio demo, not for real traffic.
- 🚨 Critical: Trading dashboard outages (2-second resolution)
- 👥 Account: New user provisioning with license checking
- 🔍 Technical: Database connectivity troubleshooting
- 📈 Training: Chart creation guidance and resources
- Backend: FastAPI, Python 3.11+, Pydantic data models
- Persistence: PostgreSQL (SQLite for local dev), SQLAlchemy ORM
- Intelligence: Rule-based classification + OpenRouter LLM fallback (RAG for technical support)
- Communication: HTTP REST APIs, Redis message queuing
- Security & Observability: Shared-secret internal auth, structured JSON logging, LLM availability tracking
- Frontend: Streamlit interactive interface
- Deployment: Docker containers locally; Neon + Render + Streamlit Community Cloud for a $0 public deployment (no card required anywhere — see
docs/DEPLOYMENT.md) - CI: GitHub Actions (lint, tests, Docker build)
- 87% automated resolution rate for common issues
- < 2 second average response time across all agents
- 24/7 availability without human intervention required
- Contextual responses based on department and user role
Access the interactive demo at http://localhost:8501 to see agents collaborating in real-time to solve enterprise Tableau support scenarios.
See docs/UPGRADE_PLAN.md for the phased plan to turn this from a
demo into a working system. Done so far: a single orchestration path with env-driven config
and real failure handling (Phase 0); persistence — tickets, departments/users/licenses, and
the technical knowledge base now live in a database instead of Python literals, with a real
(simulated) Tableau backend and a dashboard that reports actual numbers (Phase 1); hybrid
intelligence — rules stay the fast/free default, an OpenRouter LLM handles ambiguous
classification and RAG-based technical responses when configured (Phase 2); a closed
escalation loop — a Human Review tab with Approve/Edit/Reject actions and a full audit trail,
so nothing an agent escalates is ever fire-and-forget (Phase 3); hardening — internal
service auth, structured JSON logging with ticket correlation, typed LLM error handling with
availability tracking, and CI (Phase 4); and a $0 cloud deployment path across Neon, Render,
and Streamlit Community Cloud — no credit card required anywhere in the stack (Phase 5). All
five phases are done.
Since then: live deployment debugging (a Streamlit Cloud dependency-resolution failure, a
GitHub Actions workflow file rejection, and an OpenRouter free-tier model being discontinued
mid-project — see docs/UPGRADE_PLAN.md's "Real-world update" note), and a UI/UX pass
covering cold-start signaling, a directory-backed ticket form, self-learning subject ranking,
and resolution caching to cut repeat LLM calls — see
docs/UI_UX_PLAN.md for the full history.
Built as a portfolio demonstration of multi-agent AI coordination and enterprise software architecture.