ThulasiramanandClaude Sonnet 5 3c50fb4ccb Guard analytics agent against claiming a connected data source is missing
The free-tier chain (GMI MiniMax / OpenRouter) occasionally answered
"Google Analytics isn't connected" from pattern-matching instead of
calling crm_dashboard, even though the tool works correctly. Add an
explicit rule forbidding that claim unless the tool was actually called
and returned an error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:58:30 +05:30
2026-09-02 09:23:53 +05:30
2026-08-29 14:01:56 +05:30
2026-08-29 14:01:56 +05:30
2026-08-29 14:01:56 +05:30
2026-08-29 14:01:56 +05:30
2026-08-29 14:01:56 +05:30
2026-08-29 14:01:56 +05:30

WeLe Agentic AI

A LangGraph multi-agent service that sits beside the WeLe CRM and answers questions about live CRM data — with charts, tables and generated documents rather than paragraphs of numbers.

Runs as its own service (default :4000), not inside the CRM. The CRM stays a CRM; the assistant can be restarted, scaled, or given a second channel without touching it.


Architecture

CRM Chat UI  ─┐
(WhatsApp)   ─┤   1. CHANNEL GATEWAY      auth (CRM JWT) · session · rate limit · normalise
(Website)    ─┤        ↓
(Mobile)     ─┘   2. ORCHESTRATION        intake → supervise (supervisor chain) → finalize
                       ↓
                  3. GUARDRAILS           RBAC · policy · approval · content safety · audit
                       ↓                  (enforced at the tool boundary, not the graph)
                  4. AGENT LAYER          8 read-only domain agents (agent chain), in parallel
                       ↓
                  5. TOOLS                reads → MongoDB   |   writes → CRM REST API
                       ↓
                  6. DATA                 MongoDB (live CRM) · Redis (memory + cache)
                       ↓
                  7. OUTPUT               typed blocks: metrics · chart · table · file · text

Key decisions

Reads hit Mongo, writes go through the CRM API. Direct Mongo gives rich aggregation for reporting; routing writes through the CRM's own routes keeps lead scoring, stage transitions, Socket.IO broadcasts and its audit trail intact. Writing to Mongo directly would produce records the CRM never learns about.

Guardrails live in defineTool, not in graph nodes. A node-level check is bypassed the moment someone adds an edge. A wrapper around the tool itself cannot be: if a model can call it, it went through RBAC → policy → approval → audit.

Sub-agents are read-only; the supervisor performs every write. The approval gate uses LangGraph's interrupt(), which suspends the graph owning the checkpointer. A sub-agent invoked as a tool is a nested graph with no checkpointer — an interrupt raised there bubbles out as an error, gets caught as a failed delegation, and the operator is never actually asked while the assistant claims something "needs approval". Hoisting writes to the supervisor puts every interrupt in the checkpointed graph.

Claude only, with a model failover chain. Each tier resolves an ordered list of Claude model ids tried left to right, so an overloaded or erroring model hands the turn to the next instead of failing the request:

LLM_CHAIN_SUPERVISOR=claude-opus-5,claude-sonnet-5
LLM_CHAIN_AGENT=claude-sonnet-5,claude-haiku-4-5

Claude 5 API rules are encoded once in orchestration/providers.js: temperature/top_p/budget_tokens are removed (a 400 if sent), thinking is adaptive and must not be disabled, and depth is tuned with output_config.effort.

Answers are typed blocks, not strings. The frontend renders metrics rows, charts, tables and file cards natively. Same payload can drive a different channel later.


Setup

npm install
cp .env.example .env      # fill in ANTHROPIC_API_KEY, MONGODB_URI, CRM_JWT_SECRET
npm start                 # or: npm run dev

CRM_JWT_SECRET must match the CRM's JWT_SECRET — that is how a CRM login is accepted here, with the same role and permissions.

Redis is optional: if unreachable the service falls back to an in-process store and logs a warning, so local dev works without it.

Frontend

The chat page is frontend/src/pages/AIAssistant.jsx in the CRM repo, routed at /assistant.

# in the CRM frontend
echo "VITE_AGENT_URL=http://localhost:4000" > .env.local
npm run dev

API

Method Path Purpose
POST /api/agent/chat One turn, buffered JSON
POST /api/agent/stream One turn, SSE progress + final blocks
POST /api/agent/approve Resume a run paused for approval
GET /api/agent/sessions Caller's recent conversations
GET /api/agent/sessions/:id One conversation's history
DELETE /api/agent/sessions/:id Delete a conversation
GET /api/agent/capabilities Agents/tools this caller may use
GET /api/agent/audit/:sessionId Tool-call audit trail (admin)
GET /artifacts/:id Download a generated file
GET /health Mongo / Redis / CRM reachability

All routes take Authorization: Bearer <CRM token>.


Agents

Agent Owns
Lead Search, profiles, funnel breakdowns, trends, stale-lead hygiene
Analytics Business snapshot, conversion funnel, team performance, source ROI, enrolments
Conversation WhatsApp/FB/IG inbox — history, triage, escalations, volumes
Scheduling Follow-ups and demos, overdue work, the calendar
Campaign Broadcast campaigns and delivery performance
Calls Telephony volumes, answer rates, call records
Course Course/batch catalogue, workshop funnel
People Users, roles, trainers, DSR submissions

An agent whose whole toolset is behind permissions the caller lacks is hidden from routing entirely, rather than being offered and then failing.


Output

create_metrics, create_chart, create_table, generate_document (xlsx / docx / pptx / pdf / csv).

Charts are rendered server-side to SVG (theme-aware in the browser) and rasterised to PNG via @resvg/resvg-js for embedding in documents — all pure-JS or prebuilt-binary, no native toolchain needed on Windows.

The palette is validated for colour-vision deficiency in both light and dark modes. Three light-mode slots fall below 3:1 contrast, so every chart ships direct labels and a data table — identity is never carried by colour alone.


Data notes discovered against the live database

These are encoded in the tools and the supervisor prompt, and matter for anyone extending this:

  • Lead.enrolled is false on every one of the ~4,500 lead records — nothing in the CRM sets it. Enrolment and revenue truth lives in enrollmentlogs (paymentStatus = SUCCESS). Reporting off the lead flag reports a permanent zero.
  • enrollmentlogs.primaryMobile stores bare 10-digit mobiles while leads.phone_number is 91-prefixed. A naive join matches nothing; paidPhoneVariants() in tools/crm/_shared.js handles it.
  • Phone number is the join key across every collection.
  • current_stage: converted is used on only a handful of leads — it is not a reliable conversion signal.
  • Call logs stop in April 2026; an empty window means an inactive feed, not zero activity.

Voice

Speech-to-speech lives in voice-service/ (Python, GPU) and enters through src/gateway/voice.js as a WebSocket channel — so a spoken question runs the same graph, agents and guardrails as a typed one.

npm run voice     # start the GPU service on :4100 first
npm start         # the agent service exposes ws://localhost:4000/api/agent/voice

Silero VAD does the endpointing, ai4bharat/indic-conformer-600m-multilingual transcribes 22 Indian languages, openai/whisper-small handles English and identifies the language, and ai4bharat/indic-parler-tts speaks the reply. Language defaults to auto-detect per utterance, so Tamil and English can be mixed across turns without touching a setting.

The interesting problem was not audio: a turn takes 17–46 s, and that is dead silence in voice. So the bridge speaks an acknowledgement within ~1 s, narrates each agent delegation aloud, and reads the answer sentence-by-sentence as it is composed. Barge-in is server-side — the VAD opening a turn cancels TTS mid-word.

Details, protocol and tuning: voice-service/README.md.

Claude notes

  • Models: claude-opus-5 (supervisor), claude-sonnet-5 (agents), claude-haiku-4-5 (fallback). Use the exact ids — never append date suffixes.
  • Roughly $0.10–0.30 per question (supervisor + sub-agents + tool loops ≈ 20–60k tokens).
  • Check the account can serve a request with node scripts/t-providers.mjs.

Tests

Diagnostics against live data and the real API (no mocks):

node scripts/t-providers.mjs  # can Claude serve a request right now?
node scripts/t-llm.mjs        # Claude 5 models + tool calling
node scripts/t-tools.mjs      # all read tools against live Mongo
node scripts/t-chart.mjs      # renders every chart form to SVG + PNG
node scripts/t-docs.mjs       # builds xlsx/docx/pptx/pdf/csv
node scripts/t-docverify.mjs  # structural verification of those files
node scripts/t-e2e.mjs        # full agent turns over HTTP
node scripts/t-stream.mjs     # SSE streaming
node scripts/t-approve.mjs    # approval interrupt + resume (--approve to accept)

Adding a channel

Add an entry to src/gateway/channels.js declaring how the principal is established, whether it may mutate CRM state, and which block types it can render — then an adapter producing a normalised request. The orchestrator does not change. Public channels should be readOnly: true; the policy engine then refuses every non-read tool on them.

S
Description
No description provided
Readme
379 KiB
Languages
JavaScript 97.6%
Python 1.9%
Dockerfile 0.5%