- Bridge server with LLM agent tool-use (search_kb, search_products, search_orders, search_inventory, escalate_human) - pgvector RAG knowledge base (95 FAQ chunks) - Auto opportunity creation in Twenty CRM - Auto follow-up task workflow - Chatwoot AgentBot integration (HMAC, webhook) - Docker compose infrastructure (PG18, Redis 8.8, node24-alpine) - Configuration templates (example files) - real secrets excluded via .gitignore
9.2 KiB
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→ DashScopeqwen-plus-latest(enableThinking: false).server.jsheader comment says "glm-5.2/ark" — ignore it, trustllm.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, defaulthttp://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 inkb.config.json. Query translation fallback usesqwen-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, returnsagentBot: !!AGENT_BOT_SECRETGET /conversation/:idandGET /conversation/:id/html— debug viewsPOST /agent-bot— Chatwoot AgentBot outgoing webhook (HMAC-signed)POST /chatwoot-webhook— Chatwootmessage_createdwebhook (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
# 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:
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— overrideschatwoot.config.jsonbaseURLTWENTY_BASE_URL— overridestwenty.config.jsonbaseURLPG_URL— postgres connection (defaultpostgres://postgres:postgres@localhost:5432/products)AGENT_BOT_SECRET— HMAC signing key for AgentBot webhook verificationRECOMMEND_URL— product recommendation API base URLORDER_API_URL— trading-service base URL (order queries)ORDER_API_KEY— auth key for trading-serviceINVENTORY_API_URL— eta-service base URL (stock/ETA queries)INVENTORY_API_KEY— auth key for eta-serviceDASHSCOPE_API_KEY— overrideskb.config.jsonapiKey (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.jsonfrom__dirname(the bridge dir). Env vars always win — checkprocess.env.*overrides inserver.jsbefore editing config files. - KB re-indexing:
index_kb.jsDROPS and recreateskb_docsevery run (no incremental update). Any model/dimension change requires a full re-vectorize. Runnode index_kb.jsafter editing anycustomer-service-kb/*.mdfile. - KB doc format: YAML frontmatter (
title/category/keywords/source_url) +### Q:/answer blocks. Embedding text =keywords + Q + A. Source URLs are mapped per-section inindex_kb.jsSECTION_URL_MAP. - Agent persona is the source of truth for sales policy (discount tiers, minimum order €30, contacts).
AGENT_SYSTEMinserver.jsencodes 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,
searchKBauto-translates to English viaqwen-turboand merges results. Rarely triggers —text-embedding-v4handles most EU languages well. - Duplicate
addChatToOpportunitycall:server.jslines ~1086-1088 call this function twice for the same conversation/opportunity. Likely a bug — second call creates a duplicate note. start.shChatwoot unconditional: the Twenty section checks fordocker-compose.yamlexistence first, but the Chatwoot section runsdocker compose upunconditionally — 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.jsonanddocker-compose.ymlcontain 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+" butFROM node:24-alpine. Node 24 has built-infetch/FormData/Blob. - Bridge auto-creates follow-up tasks: after creating an Opportunity,
createLeadInCRMauto-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_SYSTEMrequires 3 elements to setready=true: company name + contact info (email/phone/name) + product/need. Missing any →ready=false, no CRM record created.