A multi-agent digital marketing assistant. Give it a business objective and a brand tone; it writes the copy, plans the campaign, scores its own work against a rubric, and reworks itself until it passes.
Aria is a digital marketing assistant built as a multi-agent system. You describe a business objective and a brand tone; Aria orchestrates a team of specialised agents that:
- write marketing copy (posts, ads, social text),
- plan the campaign (publishing calendar, audience segmentation, budget),
- evaluate the result against a fixed quality rubric, and
- improve the copy automatically when the evaluation falls short.
A separate flow handles customer support: it answers questions and escalates to a human whenever it is not confident.
The whole thing is model-agnostic: the application never talks to a model provider directly. Every call goes through a gateway using a generic alias, so you can swap the model behind Aria without touching a single line of code, and no provider name ever leaks into the app.
- What is Aria?
- Key features
- Architecture
- How it works
- Tech stack
- Quick start (one command)
- Manual setup
- Configuration
- API reference
- Project layout
- Documentation
- Conventions
- Multi-agent orchestration with a deterministic control flow (no LLM decides the routing).
- Self-improving campaigns: a bounded feedback loop where the analyst's weaknesses are fed back into the copywriter, so each rework is directed, not a blind retry.
- Rubric-based scoring: campaigns are graded 0–100 against six weighted criteria; approval is derived deterministically from the score.
- Confidence-based support: the support agent answers only when sure, and escalates to a human otherwise.
- Model-agnostic gateway: swap models via configuration; automatic fallback if the primary provider fails or rate-limits.
- Bilingual output (
es/en), selected per run and threaded from the UI into every agent. - Full traceability: every agent emits start/end traces, streamed live to the UI over SSE and persisted as an audit record.
- Privacy & compliance built in: retention purge, full erasure endpoint, security headers, rate limiting, and an ISO/IEC 27001 & 42001 control mapping.
Four layers, each with a single responsibility. The backend never reaches a model provider directly.
┌──────────────┐ ┌───────────────────────┐ ┌──────────────┐ ┌────────┐
│ Frontend │ ──▶ │ Backend │ ──▶ │ Gateway │ ──▶ │ Models │
│ (Next.js) │ │ (FastAPI + LangGraph) │ │ (LiteLLM) │ │ │
└──────────────┘ └───────────────────────┘ └──────────────┘ └────────┘
:3000 :8000 :4000
| Layer | Tech | Responsibility |
|---|---|---|
| Frontend | Next.js 15, React 19, TypeScript, Tailwind | Campaign form, live trace timeline, support, history, technical graph view |
| Backend | FastAPI, LangGraph, SQLModel | Agent orchestration, state, traces, persistence, API |
| Gateway | LiteLLM proxy (config only) | Resolves generic aliases to a real provider/model; automatic fallback |
| Models | any OpenAI-compatible model | Text (and image) generation behind a generic alias |
| Database | Postgres 16 (SQLite for standalone local runs) | Append-only run history as an audit record |
| Agent | LLM? | What it does |
|---|---|---|
| Orchestrator | No (deterministic) | Routes the flow, enforces the iteration limit |
| Content | Yes | Writes marketing copy from the objective and brand tone |
| Campaigns | Yes | Builds the campaign plan (calendar, audience, budget) |
| Analytics | Yes | Scores the campaign against a rubric, extracts weaknesses |
| Support | Yes | Answers customer questions; escalates when unsure |
See AGENTS.md for each agent's state and guarantees, and docs/SKILLS.md for their concrete capabilities.
orchestrator → content → campaigns → analytics
│
approved ──────────────────→ END
rejected & iterations < 2 ─→ adjust ─→ content
rejected & iterations ≥ 2 ─→ END (hard limit)
- The orchestrator receives the objective and routes to content.
- Content writes the copy; campaigns turns it into a plan.
- Analytics scores the plan 0–100 against a fixed rubric (objective alignment, clarity, persuasion & CTA, brand-tone fit, platform fit, feasibility) and extracts the concrete weaknesses.
- If the score is ≥ 70 the campaign is approved and the flow ends.
- If not, the orchestrator loops back to content, injecting the weaknesses so the next attempt fixes them specifically. This repeats at most twice (a hard limit that guarantees the loop always terminates).
Approval is never parsed from free LLM text: it is derived deterministically
from score ≥ MIN_SCORE, so every routing decision is auditable.
An independent flow with its own state. The support agent answers FAQs directly when confident; otherwise it returns an escalation marker and flags the case for a human. It never touches campaign state.
Free tier friendly: every piece can run on a free/local model and open-source tooling.
- Backend: Python 3.12, FastAPI, LangGraph, SQLModel, slowapi (rate limiting), uv (package manager).
- Frontend: Next.js 15, React 19, TypeScript 5, Tailwind CSS, framer-motion, mermaid, pnpm (package manager).
- Gateway: LiteLLM proxy (configuration only, no custom code).
- Data: Postgres 16 (SQLite for standalone local runs).
- Local model: Ollama (optional, demonstrates model-agnosticism).
- Infra: Docker Compose, GNU Make for one-command orchestration.
- Observability: structured JSON logging; optional LangSmith tracing.
Prerequisites: Docker (Desktop on macOS/Windows), pnpm, and uv.
# 1. Clone
git clone <your-repo-url> aria
cd aria
# 2. Configure secrets (see "Configuration" below for what each var means)
cp .env.example .env
cp backend/.env.example backend/.env
# then edit .env and backend/.env with your gateway key and provider key
# 3. Bring up EVERYTHING: backend, Postgres, gateway, local model runner,
# and the Next.js frontend dev server.
make devThat's it. make dev builds and starts the Docker stack and then runs the
frontend in the foreground.
| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| Backend API | http://localhost:8000 |
| API docs (Swagger) | http://localhost:8000/docs |
| Gateway | http://localhost:4000 |
make dev # bring up the whole stack + the frontend (default)
make up # start only the backend/db/gateway stack (Docker), in the background
make front # run only the Next.js frontend in the foreground
make down # stop and remove the Docker stack (keeps the db volume/data)
make restart # recreate the Docker stack
make logs # follow the Docker stack logs
make ps # show the Docker stack status
make clean # stop the stack and DELETE the db volume (WARNING: wipes all data)
make studio # run LangGraph Studio for the campaign graph (outside Docker)
make help # list all targetsPrefer to run things one by one? Here is each layer, copy-paste ready.
cd backend
uv sync --extra dev # install deps into a local .venv
.venv/bin/python -m pytest -q # run the test suite
uv run uvicorn app.main:app --reload # serve on http://localhost:8000The backend expects a gateway at GATEWAY_URL. For standalone local runs it
defaults to a SQLite database (sqlite:///./app.db), so you do not need Postgres.
cd frontend
pnpm install
pnpm dev # serve on http://localhost:3000
pnpm typecheck # type-check without emittingNEXT_PUBLIC_API_BASE points the client at the backend (defaults to
http://localhost:8000).
cd gateway
uv run litellm --config config.yaml --port 4000The real provider/model behind each generic alias lives only in
gateway/config.yaml.
docker compose up -d --build # start backend, db, gateway, ollama
docker compose logs -f # follow logs
docker compose down # stop (keeps data)
docker compose down -v # stop and wipe the db volumeSecrets are environment-only and git-ignored. Copy the examples and fill in real values; a startup guard rejects placeholder values.
Root .env (used by Docker Compose for backend, db, gateway, local model):
| Variable | Meaning |
|---|---|
GATEWAY_KEY |
Auth key the backend uses to call the gateway |
MODEL_ALIAS |
Generic model alias (default primary-model) |
MODEL_LOCAL_ALIAS |
Alias for the local model (local-model) |
PROVIDER_API_KEY |
Real provider key, consumed only by the gateway |
PROVIDER_FALLBACK_API_KEY |
Key for the automatic fallback provider |
PROVIDER_IMAGE_* |
Image-generation provider endpoints and key (gateway only) |
POSTGRES_USER / _PASSWORD / _DB |
Postgres credentials |
DATABASE_URL |
Connection string (Postgres in Docker; point at Supabase in deploy) |
TEMPERATURE / MAX_TOKENS |
Generation defaults |
DATA_RETENTION_DAYS |
0 keeps runs forever; a positive value purges older runs on startup |
DEMO_MODE |
Force a failing first iteration so the improvement cycle is demoable |
backend/.env mirrors the model/gateway vars and adds RATE_LIMIT
(per-client rate limit on the POST endpoints, e.g. 10/minute) and
GATEWAY_TIMEOUT.
No provider names anywhere in the app. Only the gateway config knows which real model sits behind
primary-model,fallback-modelandlocal-model.
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Service info |
GET |
/health |
Service status + gateway connectivity |
POST |
/campaign |
Run the campaign graph; stream traces over SSE, ending with a done event carrying the final result |
POST |
/support |
Run the support flow; return the final answer + escalation flag |
GET |
/history |
Persisted runs, newest first, with totals for pagination |
DELETE |
/history/campaign/{id} |
Delete one campaign run |
DELETE |
/history/support/{id} |
Delete one support run |
GET |
/graph |
Compiled graph structure, agents, state schema, parameters |
GET |
/docs |
Swagger UI |
Example, start a campaign:
curl -N -X POST http://localhost:8000/campaign \
-H "Content-Type: application/json" \
-d '{
"objective": "Launch a new specialty coffee subscription",
"brand_tone": "warm, artisanal, a little playful",
"language": "en"
}'.
├── backend/ FastAPI + LangGraph service
│ └── app/
│ ├── main.py app, middleware, router wiring
│ ├── api/ routers: campaign, support, history, graph, image
│ ├── graph/ LangGraph graph, routing, metrics, nodes, state, traces
│ ├── llm/ call_agent(): the only door to the gateway (Contract 2)
│ └── persistence/ SQLModel storage (audit record)
├── frontend/ Next.js app (campaign, support, history, technical views)
├── gateway/ LiteLLM proxy config (generic model aliases)
├── docs/ spec, skills, handoff + deployment + ISO/GDPR compliance docs
├── docker-compose.yml
├── Makefile one-command dev orchestration
└── AGENTS.md the agents
| Document | What it covers |
|---|---|
| docs/SPEC.md | Specification: scope, architecture, contracts, API, constraints |
| AGENTS.md | Every agent: role, state, invocation, guarantees |
| docs/SKILLS.md | The concrete capabilities (skills) of each agent |
| docs/HANDOFF.md | Implementation status and detailed run instructions |
| docs/compliance/ | ISO/IEC 27001 & 42001 control mapping, GDPR record of processing |
| docs/deploy-railway.md | Deployment guide |
- Work on a feature branch; merge to
mainvia PR. - Commits:
type:short description, in English, no parentheses. - Code, comments and identifiers are English-only. Agent output is
user-facing and bilingual (
es/en). - No provider names anywhere in the application or docs; generic aliases only.
Released under the MIT License.
- Andrés Torres Mamani
- Michelle Zulay Gelves Contreras
- Naizabeth De Los Angeles Bermudez Davila
- Roberto Antonio Molero Losada