An autonomous AI agent that handles customer quotation workflows end-to-end, with a mandatory human-in-the-loop approval gate before any email is sent.
A customer sends a quote request by email. The AI Finance Officer reads it, searches inventory, calculates pricing with bulk discounts, drafts a professional quotation email, and pauses for human approval via Telegram. Only after explicit approval does it send the email.
Every action is logged to an auditable step trace.
- Ingests requests — Reads quote requests from incoming emails or manual triggers
- Checks inventory — Semantic search over a product catalog stored in Qdrant Cloud
- Calculates quotes — Applies line-item pricing and automatic 5% bulk discounts for quantities over 100
- Drafts emails — Generates professional HTML quotation emails with styled tables
- Requests approval — Sends a structured summary to a human approver via Telegram with Approve/Reject buttons
- Sends on approval — Dispatches the email via Gmail SMTP only after explicit human consent
- Logs everything — Every tool call is recorded as a step in Neon Postgres for full auditability
Customer Request
|
v
FastAPI Backend ------> Ollama (External LLM)
| |
|----> Qdrant Cloud |
|----> Neon PostgreSQL |
|----> Upstash Redis |
|
v
Telegram Bot <---- Human Approver
|
v
Gmail SMTP ------> Customer Inbox
| Layer | Technology |
|---|---|
| Backend API | FastAPI, SQLAlchemy, Alembic, Pydantic Settings |
| AI / LLM | Ollama (kimi-k2.6:cloud) with tool-calling and text-parsing fallback |
| Vector Search | Qdrant Cloud (product embedding and semantic search) |
| Database | Neon PostgreSQL (tasks, employees, approvals, audit steps) |
| Cache | Upstash Redis |
| Embedding | External OpenAI-compatible Embedding API |
| Approval Channel | Telegram Bot API (inline keyboard buttons) |
| Email Delivery | Gmail SMTP with multipart HTML + plain text |
| Dashboard | Next.js 14, TypeScript, Tailwind CSS, lucide-react |
| Infrastructure | Managed cloud services (Neon, Qdrant Cloud, Upstash) |
| Requirement | How to Obtain |
|---|---|
| Ollama host | ollama.com — run ollama pull kimi-k2.6:cloud on a reachable host |
| Neon account | neon.tech — create a PostgreSQL project |
| Qdrant Cloud account | qdrant.tech/cloud — create a cluster |
| Upstash Redis account | upstash.com — create a Redis database |
| Embedding API provider | OpenAI, Jina AI, Cohere, etc. — obtain an API key |
| Telegram Bot Token | Message @BotFather on Telegram |
| Telegram Chat ID | Message @userinfobot on Telegram |
| Gmail App Password | Google Account → Security → 2-Step Verification → App Passwords |
git clone https://github.com/Ladychka/Orchestrix.git
cd Orchestrix
cp .env.example .envEdit .env and fill in your credentials:
# Ollama (must be reachable over the network)
OLLAMA_URL=https://your-ollama-host.example.com
OLLAMA_MODEL=kimi-k2.6:cloud
# Neon PostgreSQL
DATABASE_URL=postgresql://user:password@your-project.neon.tech/dbname?sslmode=require
# Qdrant Cloud
QDRANT_URL=https://your-cluster.qdrant.io
QDRANT_API_KEY=your_qdrant_api_key_here
# Upstash Redis
UPSTASH_REDIS_REST_URL=https://your-project.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_upstash_token_here
# Embedding API (OpenAI-compatible)
EMBEDDING_BASE_URL=https://api.openai.com/v1
EMBEDDING_API_KEY=your_embedding_api_key_here
EMBEDDING_MODEL=text-embedding-3-small
# Telegram Bot
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_APPROVER_CHAT_ID=your_chat_id_here
# Gmail SMTP
EMAIL_USER=your@gmail.com
EMAIL_PASS=your_app_password_herecd backend
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txtuvicorn main:app --port 8000cd ../frontend
npm installSet API_URL to your backend endpoint (e.g. http://localhost:8000 for local development):
export API_URL=http://localhost:8000
npm run devThe frontend will be available at http://localhost:3000 and proxies API requests to the backend.
cd backend
python init_db.py
python -m ingestion.ingestcurl http://localhost:8000/health
# Expected: {"status": "ok"}curl -X POST http://localhost:8000/tasks \
-H "Content-Type: application/json" \
-d '{"from_email":"customer@example.com","subject":"Quote request","body":"Please quote 200 units of SKU-101.","employee_id":1}'Or use the dashboard at http://localhost:3000 and click Trigger Demo Task.
Poll the task to see the step trace grow:
curl http://localhost:8000/tasks/1The agent runs these tools in order:
| Step | Tool | Purpose |
|---|---|---|
| 0 | check_inventory |
Searches Qdrant Cloud for the requested SKU |
| 1 | calculate_quote |
Computes line items, discounts, and total |
| 2 | draft_quotation_email |
Generates HTML + plain text email body |
| 3 | request_approval |
Pauses and sends Telegram approval request |
You will receive a message like:
APPROVAL REQUEST — Task #1
━━━━━━━━━━━━━━━━━━━━
Quotation Summary
- Customer: customer@example.com
- Product: ProBook 15-inch Laptop (SKU-101)
- Quantity: 200 units
- Unit Price: $899.00
- Bulk Discount: -$8,990.00
- Total: $170,810.00 USD
Warning: Only 45 units currently in stock.
[ Approve & Send ] [ Reject ]
Tap Approve & Send — the email is dispatched immediately.
The task status changes to completed. A new send_email step appears in the trace. The HTML quotation email arrives in the customer's inbox with a styled table, gradient header, and validity banner.
| Decision | Rationale |
|---|---|
| Human-in-the-Loop Approval | The AI cannot send emails autonomously. Every quotation requires explicit human approval via Telegram, creating a trust and safety boundary. |
| Full Audit Trail | Every tool call is persisted as a TaskStep in Neon Postgres. The dashboard renders this as a live timeline for complete transparency. |
| External LLM (Ollama) | No external API keys or rate limits for the LLM layer. Runs on a host you control. The agent uses tool-calling when supported, with a text-parsing fallback. |
| Exact SKU Lookup | Semantic search alone matched wrong products for SKU codes. We added payload-filtered exact lookup for SKU strings while keeping semantic search for product descriptions. |
| Multipart HTML Emails | Plain text emails look unprofessional. The system sends both HTML (styled table, gradient header, validity banner) and plain text (fallback) in a single MIME message. |
| Managed Cloud Infrastructure | PostgreSQL (Neon), vector search (Qdrant Cloud), and cache (Upstash Redis) are fully managed, eliminating operational overhead of self-hosted containers. |
| Problem | Solution |
|---|---|
| Backend returns 500 on tasks | Verify Ollama is reachable at OLLAMA_URL. Check that QDRANT_URL, QDRANT_API_KEY, DATABASE_URL, and embedding API credentials are correct. |
| Telegram messages not arriving | Verify TELEGRAM_BOT_TOKEN and TELEGRAM_APPROVER_CHAT_ID. The bot starts automatically when the backend boots. |
| Emails not sending | Use a Gmail App Password, not your regular account password. Requires 2-Step Verification to be enabled first. |
| Tasks stuck at "received" | The orchestrator crashed before calling tools. Check backend logs for Ollama connection or model errors. |
| Frontend is unstyled white page | The Next.js build failed. Check npm run build output and reinstall dependencies with npm install. |
| Embedding API errors | Ensure EMBEDDING_BASE_URL points to an OpenAI-compatible endpoint and EMBEDDING_API_KEY is valid. |
Orchestrix/
├── backend/
│ ├── app/
│ │ ├── api/ # REST endpoints (tasks, employees)
│ │ ├── core/ # Pydantic Settings configuration
│ │ ├── orchestrator/ # Ollama agent loop with tool routing
│ │ ├── services/ # Qdrant, Telegram, Ollama, Embedding, Redis clients
│ │ ├── tools/ # check_inventory, calculate_quote, draft_email, send_email, log_step
│ │ └── models/ # SQLAlchemy: Task, TaskStep, Approval, AIEmployee
│ ├── ingestion/ # products.json → Qdrant embedding pipeline
│ └── init_db.py # Seeds the AI Finance Officer
├── frontend/
│ └── src/app/
│ ├── page.tsx # Dashboard: employee card + recent tasks
│ ├── tasks/page.tsx # Full task list with status filters
│ └── tasks/[id]/ # Task detail with live step timeline
├── .env.example # Cloud environment variable template
└── README.md
# Verify tools in isolation
cd backend
python test_tools.py
# Trigger a task via API
curl -X POST http://localhost:8000/tasks -H "Content-Type: application/json" \
-d '{"from_email":"test@test.com","subject":"Quote","body":"Quote 50 units of SKU-102","employee_id":1}'
# Poll task status
curl http://localhost:8000/tasks/1MIT