# CRM AI Demo - Project Guidelines ## Project B2B stainless steel jewelry wholesaler (Yehwang, NL) CRM with AI-powered sales/service assistant bridge. Chatwoot webhook → LLM agent (tool-use) → Chatwoot reply + Twenty CRM opportunity creation. ## Tech Stack - **Bridge**: Node.js 24, CommonJS (`"type": "commonjs"`), pure Node built-ins (`http`/`fetch`/`crypto`) + `jose` + `pg`. No Express. No test/lint/build scripts — verify by running the server and hitting `/healthz`. - **LLM**: OpenAI-compatible provider via `bridge/llm.config.json`. Active: `aliyun` → DashScope `qwen-plus-latest` (`enableThinking: false`). `server.js` header comment says "glm-5.2/ark" — **ignore it, trust `llm.config.json`**. - **Messaging**: Chatwoot (self-hosted, external source checkout at `./chatwoot/` — has its own `.git/`). - **CRM**: Twenty CRM (self-hosted, external source checkout at `./twenty/` — may be absent). - **Vector DB**: pgvector on shared `crm-postgres` (pg18). Three DBs: `chatwoot`, `twenty`, `products`. KB RAG table: `products.kb_docs`. - **Product Search**: external recommend API (`RECOMMEND_URL`, default `http://recommend.internal.yw.com`). - **Order/Inventory APIs**: trading-service (`ORDER_API_URL`) and eta-service (`INVENTORY_API_URL`). Mock server available: `node mock_services.js` (port 9505). - **Embedding**: DashScope `text-embedding-v4` (dim 1024), configured in `kb.config.json`. Query translation fallback uses `qwen-turbo`. ## Architecture ``` Chatwoot (external) ──webhook──► Bridge (:4000) ──LLM tool-use──► Chatwoot reply │ ├── pgvector (products.kb_docs) KB RAG (search_kb) ├── recommend API product catalog (search_products) ├── trading-service order tracking (search_orders) ├── eta-service inventory/stock (search_inventory) └── Twenty CRM GraphQL create Opportunity ``` Bridge routes (`bridge/server.js`): - `GET /healthz` (and `/`) — liveness, returns `agentBot: !!AGENT_BOT_SECRET` - `GET /conversation/:id` and `GET /conversation/:id/html` — debug views - `POST /agent-bot` — Chatwoot AgentBot outgoing webhook (HMAC-signed) - `POST /chatwoot-webhook` — Chatwoot `message_created` webhook (fallback entry) Agent tool-use loop: LLM autonomously calls `search_kb`, `search_products`, `search_orders`, `search_inventory`, or `escalate_human`. Single `AGENT_SYSTEM` persona — no prompt switching. Per-conversation lock (`locks` Set) prevents concurrent LLM calls; `createdOpps` Set prevents duplicate Opportunities. ## Startup ```bash # Full stack (requires Docker Desktop): bash start.sh # Shared infra only (no Chatwoot/Twenty source needed): docker compose up -d --build # Local dev (no Docker): cd bridge && node server.js # Node 24+; needs PG_URL etc. as env vars # Mock business APIs (for dev without real trading/eta services): node mock_services.js # Listens on :9505 ``` `start.sh` conditionally starts Twenty (if `./twenty/docker-compose.yaml` exists) and Chatwoot (if `./chatwoot/docker-compose.yml` exists — note: currently runs unconditionally, will error if absent). Shared infra always starts. Logs / health: ```bash docker logs -f crm-bridge curl http://localhost:4000/healthz ``` **No rebuild needed for code changes** — `docker-compose.yml` volume-mounts `bridge/*.js` and `*.config.json` read-only. `docker restart crm-bridge` picks up edits. Only rebuild the image when changing `package.json` deps. ## Key Files ``` bridge/ server.js Main bridge: routes, LLM agent loop, tool impls, Chatwoot/Twenty API calls (~1250 lines) index_kb.js KB vectorizer: customer-service-kb/*.md → embed → products.kb_docs (DROPS + recreates table each run) register_agent_bot.js Registers Chatwoot AgentBot (idempotent). Run: node register_agent_bot.js llm.config.json LLM provider config. Active provider key: "aliyun" kb.config.json Embedding config (model, dim, translateModel) + DashScope creds chatwoot.config.json Chatwoot API connection (baseURL, accountId, accessToken, inboxId) twenty.config.json Twenty CRM connection (baseURL, workspaceId, apiKey JWT) Dockerfile node:24-alpine, npm install --omit=dev, copies server.js + configs customer-service-kb/ FAQ markdown (source for index_kb.js). Chunks split on ### Q:. English; AI translates per customer language. infra/init-db.sh postgres init: creates DBs chatwoot/twenty/products + enables pgvector on chatwoot & products. docker-compose.yml Shared infra: pgvector/pgvector:pg18 + redis:8-alpine + bridge mock_services.js Mock trading/eta API server (:9505) for dev without real backends add_note.js CLI utility: attach a text note to a Twenty opportunity create_workflow.js CLI utility: create Twenty automation workflow (new opp → auto follow-up task) find_keys.js CLI utility: find Twenty signing key tables in postgres gen_apikey.js CLI utility: generate Twenty API key JWT (needs ENCRYPTION_KEY + PG_DATABASE_URL) landing.html Multilingual landing page start.sh One-shot launcher for all compose stacks docs/twenty-crm-guide.md Twenty CRM integration guide: which GraphQL ops bridge uses, data model, extension ideas ``` ## Environment Variables Env vars always override `*.config.json` values. Key overrides in `server.js`: - `CW_BASE_URL` — overrides `chatwoot.config.json` baseURL - `TWENTY_BASE_URL` — overrides `twenty.config.json` baseURL - `PG_URL` — postgres connection (default `postgres://postgres:postgres@localhost:5432/products`) - `AGENT_BOT_SECRET` — HMAC signing key for AgentBot webhook verification - `RECOMMEND_URL` — product recommendation API base URL - `ORDER_API_URL` — trading-service base URL (order queries) - `ORDER_API_KEY` — auth key for trading-service - `INVENTORY_API_URL` — eta-service base URL (stock/ETA queries) - `INVENTORY_API_KEY` — auth key for eta-service - `DASHSCOPE_API_KEY` — overrides `kb.config.json` apiKey (avoids hardcoding in config) - `PORT` — bridge listen port (default 4000) Docker-compose sets these for the bridge container (using container names for inter-service routing, `host.docker.internal` for host-side services). ## Conventions & Gotchas - **Config loading**: `loadCfg()` reads `*.config.json` from `__dirname` (the bridge dir). Env vars always win — check `process.env.*` overrides in `server.js` before editing config files. - **KB re-indexing**: `index_kb.js` **DROPS and recreates** `kb_docs` every run (no incremental update). Any model/dimension change requires a full re-vectorize. Run `node index_kb.js` after editing any `customer-service-kb/*.md` file. - **KB doc format**: YAML frontmatter (`title`/`category`/`keywords`/`source_url`) + `### Q:`/answer blocks. Embedding text = `keywords + Q + A`. Source URLs are mapped per-section in `index_kb.js` `SECTION_URL_MAP`. - **Agent persona is the source of truth** for sales policy (discount tiers, minimum order €30, contacts). `AGENT_SYSTEM` in `server.js` encodes business rules; `customer-service-kb/` encodes policy FAQs. Keep them consistent. - **Language rule**: replies must mirror the customer's language, even when tool results are English. Enforced in `AGENT_SYSTEM`. - **Multi-language KB recall**: when cosine similarity < 0.55 and query isn't English, `searchKB` auto-translates to English via `qwen-turbo` and merges results. Rarely triggers — `text-embedding-v4` handles most EU languages well. - **Duplicate `addChatToOpportunity` call**: `server.js` lines ~1086-1088 call this function twice for the same conversation/opportunity. Likely a bug — second call creates a duplicate note. - **`start.sh` Chatwoot unconditional**: the Twenty section checks for `docker-compose.yaml` existence first, but the Chatwoot section runs `docker compose up` unconditionally — will fail if `./chatwoot/` is absent. - **Not a git repo**: the project root has no `.git/`. `./chatwoot/` has its own `.git/` (separate checkout). `./twenty/` may or may not be present. - **Secrets in configs**: `*.config.json` and `docker-compose.yml` contain real API keys/tokens (DashScope, Chatwoot access token, Twenty JWT, `AGENT_BOT_SECRET`). Treat as sensitive; do not commit to public repos. - **Resource limits**: postgres 0.5 CPU/512M, redis 0.3 CPU/256M, bridge 0.5 CPU/512M (in `docker-compose.yml`). LLM/embedding calls are external network — watch latency under load. - **Dockerfile base image**: `node:24-alpine`. The comment says "Node 18+" but `FROM node:24-alpine`. Node 24 has built-in `fetch`/`FormData`/`Blob`. - **Bridge auto-creates follow-up tasks**: after creating an Opportunity, `createLeadInCRM` auto-creates a Task due in 3 days, linked to the opportunity. This is in bridge code, not Twenty's workflow engine (Twenty API key lacks permission to create workflows via GraphQL). - **Opportunity extraction logic**: `EXTRACT_SYSTEM` requires 3 elements to set `ready=true`: company name + contact info (email/phone/name) + product/need. Missing any → `ready=false`, no CRM record created.