Overview
The Djezzy Commercial Copilot is a multi-agent assistant that lets Djezzy's commercial department ask natural-language questions over five commercial data feeds and get back narrative answers with live charts, in seconds instead of a BI ticket queue. I led the 5-engineer team (four engineers plus myself) that built it over an initial 6-week sprint, and drove the majority of the architecture and implementation across the monorepo.
Problem
Commercial managers who needed to understand yesterday's numbers had to file a request and wait on an already-overloaded BI queue. Manual extraction introduced interpretation error, most damagingly by comparing a holiday against an ordinary day a year earlier and drawing the wrong conclusion. And 13 months of rich seasonal data sat mostly unmined for correlations that could explain demand shifts.
Context
The system serves Djezzy's commercial department: sales, marketing, and planning staff as end users, commercial management as dashboard consumers, and BI/data teams as the group whose workload the Copilot exists to relieve. It's a monorepo with five independently deployable services (api, workers, mcp-proxy, etl, and frontend) sharing one Pydantic/SQLAlchemy contract package so the output schema can never drift between services.
My Role
I led the team of five (four engineers plus myself), owned the shared architecture (the monorepo layout, the Temporal/LangGraph runtime spine, the async delivery pipeline, and the CI gates), and did the majority of the implementation work across services, based on commit history. The rest of the team built out their assigned deployables against that shared spine.
Goals
- Let any authorized commercial user answer their own data questions in natural language
- Deflect repetitive ad-hoc extraction requests away from the BI team
- Collapse time-to-insight from hours or days to seconds
- Enforce holiday- and event-aware baselines so period comparisons are mathematically fair
- Capture solved analyses in shared memory so the department never recomputes the same answer twice
- Keep external web access human-approved and domain-whitelisted
Technical Decisions
- Split deployables (api, workers, mcp-proxy, etl, frontend) from repos: one monorepo, five independently scaling services, sharing a single Pydantic output contract and SQLAlchemy model set through packages/common
- Use Temporal for the agent turn so a heavy analytical query survives worker crashes and retries instead of trapping a user behind a spinner, verified with Temporal's own Replayer determinism tests rather than happy-path checks alone
- Use LangGraph so each persona (Explorer/Researcher/Planner) is a real compiled subgraph with its own tool-calling loop, rather than one prompt trying to do everything
- Validate every LLM-generated SQL query with sqlglot before execution, since the query author (the model) is untrusted. Validation runs after generation and before the query ever touches the warehouse
- Keep the LLM self-hosted (Qwen served via vLLM) rather than calling a third-party API, since commercial data can't leave Djezzy's own infrastructure
- Gate every external web lookup behind human approval and a domain whitelist through a dedicated MCP proxy service, so the agent can add context without unsupervised internet access
- Deliver results over Kafka plus WebSocket instead of polling, with a per-task cache tier so a cache hit still notifies the user without re-running the pipeline
Architecture
A React/Vite chat UI talks to a FastAPI BFF that does no agent reasoning itself; it starts a Temporal workflow and returns a task ID. That workflow checks a shared semantic cache, then runs one LangGraph turn as a durable activity: the persona path decides which tools are available among guarded text-to-SQL, shared memory search, date normalization, and HITL-gated web search. If the agent needs web context, the workflow pauses on a signal until a human approves it. The final answer is always a strict text-plus-charts contract, cached and published as a Kafka completion event, delivered to the frontend over WebSocket.
Async query pipeline
never blockstask_id in milliseconds and the connection is free. Durable: a Temporal workflow does the slow work and the answer arrives over a socket whenever it is ready. Every generated SQL statement is parsed by sqlglot before it reaches a read-only warehouse, and any web lookup stops for human approval.Key Features
- Three personas with distinct reasoning depth and tool access: descriptive, diagnostic, and prescriptive
- Guarded text-to-SQL against a compact, read-only analytical warehouse
- Date-normalization engine covering Eid, Ramadan, exam periods, and commercial campaigns
- pgvector-backed shared memory with a KPI-availability and time-range gate before any cached answer can be served
- Hidden-correlation tool that computes comparisons in code rather than trusting a naive generated-SQL average
- Asynchronous, durable execution with bounded per-activity retries and worker-crash recovery
- Role-based auth (analyst/manager/admin/viewer) with a full JWT signup/login/refresh/reset flow
Challenges
- Keeping a self-hosted LLM's structured-output guarantees honest: the deployed model silently ignored one guided-decoding parameter, so tool calls and JSON answers failed validation until the team switched to the decoding standard that model actually honors
- Coordinating five people on five parallel branches inside one repo without cross-deployable conflicts, which is exactly why the shared contract package exists
- Building fairness into date comparisons instead of leaving it to individual analyst discipline, since a naive before/after average walks straight into day-of-week effects
- Proving durability claims instead of assuming them: worker-crash recovery needed Temporal's Replayer tool, not a passing happy-path test
- Scoping a broad BI vision into a 6-week delivery window without losing the asynchronous core, the agentic engine, or the multi-modal output
Results / Outcomes
- Shipped a working agentic pipeline verified live end-to-end: prompt in, durable Temporal execution, Kafka/WebSocket delivery, cached answer read back correctly
- Landed guarded text-to-SQL, the LangGraph orchestrator with all three personas, the async delivery pipeline, shared semantic memory, date normalization, and hidden-correlation discovery
- Shipped a real auth system (role-based access, purpose-scoped JWTs) and a working chat frontend with live chart rendering
- Ran the team's process end to end: one PR per tracked issue, CI gates (lint, types, tests, container smoke test) green on every merge
What I Learned
- Self-hosted model serving trades API convenience for real control over data residency, and it moves 'does the model honor this exact decoding flag' from someone else's problem to yours
- Durability is a testable property, not an assumption. Crash-recovery claims need a tool like Temporal's Replayer, not a green happy-path suite
- A monorepo of independently deployable services only stays coherent if the shared contract is enforced as code through one imported package, not as a convention five people are trusted to remember
- Agentic systems read clearest when each tool maps to one business rule (validate the SQL, normalize the date, gate the memory hit) rather than one large prompt trying to hold every rule at once
Future Improvements
- Finish the remaining infrastructure goals: production Kubernetes/Helm deployment and hardened observability
- Move the self-hosted reasoning model to a larger Qwen3 model once VPS capacity allows
- Close the SSR/morning-dashboard gap left by the frontend's move from Next.js to Vite