TWDAgentsHub
AI Orchestrator SaaS for Vietnamese SMEs — Team Overview Report
A multi-tenant AI operations assistant that lets Vietnamese SME employees, accountants, managers, and CEOs interact via natural language to query ERP data, automate workflows, and receive proactive intelligence.
Inspired by OpenClaw (TS, local-first) and GoClaw (Go, enterprise) architectures. Built as a pnpm monorepo (Turborepo) with 3 apps: NestJS API + Admin UI (shadcn/ui) + Chat Widget (shadcn/ui). Shared types & Tailwind preset across all apps.
graph TD
subgraph Users["Users - Natural Language"]
U1["Employee"]
U2["Accountant"]
U3["Manager"]
U4["CEO"]
end
subgraph Channels["Channel Adapters"]
CH1["REST API"]
CH2["Web Chat"]
CH3["Telegram"]
end
subgraph Orchestrator["AI Orchestrator - Central Brain"]
O1["Intent Classifier"]
O2["Plan Generator"]
O3["Dispatcher"]
O4["Aggregator"]
O5["Verifier"]
end
subgraph Agents["Agent Layer - Domain Plugins"]
A1["Finance"]
A2["HR"]
A3["Sales"]
A4["Supply"]
A5["Compliance"]
end
subgraph ERP["ERP Connector Layer"]
E1["REST Client"]
E2["Circuit Breaker"]
E3["Redis Cache"]
end
subgraph Proactive["Proactive Intelligence"]
P1["Cron Scheduler"]
P2["Anomaly Detector"]
P3["Alert Dispatch"]
end
subgraph Output["Output Layer"]
OUT1["PDF/Excel Reports"]
OUT2["Action Executor"]
OUT3["Audit Trail"]
end
Users --> Channels
Channels -->|"UnifiedMessage"| Orchestrator
Orchestrator -->|"AgentTask"| Agents
Agents -->|"ERPRequest"| ERP
Proactive -->|"ProactiveAlert"| Channels
Agents -->|"AgentResult"| Output
ERP -->|"ERPResponse"| Agents
classDef userCls fill:#0891b211,stroke:#0891b2,stroke-width:1.5px
classDef chanCls fill:#05966911,stroke:#059669,stroke-width:1.5px
classDef orchCls fill:#d9770611,stroke:#d97706,stroke-width:1.5px
classDef agentCls fill:#0891b211,stroke:#0891b2,stroke-width:1.5px
classDef erpCls fill:#be185d11,stroke:#be185d,stroke-width:1.5px
classDef proCls fill:#65a30d11,stroke:#65a30d,stroke-width:1.5px
classDef outCls fill:#dc262611,stroke:#dc2626,stroke-width:1.5px
class U1,U2,U3,U4 userCls
class CH1,CH2,CH3 chanCls
class O1,O2,O3,O4,O5 orchCls
class A1,A2,A3,A4,A5 agentCls
class E1,E2,E3 erpCls
class P1,P2,P3 proCls
class OUT1,OUT2,OUT3 outCls
Monorepo Structure (pnpm workspaces + Turborepo)
NestJS 11 backend. All 6 layers. Serves admin static files at /admin/*
React + Vite + shadcn/ui. Settings, audit logs, usage charts, user mgmt. TanStack Table + recharts.
Embeddable React widget. Socket.io. Builds as UMD bundle (<100KB). Shared Tailwind preset.
Layer 1: Channel Adapters
Normalize messages from different channels into UnifiedMessage format. Each adapter implements ChannelAdapter interface.
Layer 2: AI Orchestrator (Central Brain)
Understands user intent, creates execution plans, coordinates domain agents, aggregates results, and verifies response quality. Uses tiered LLM strategy: fast model for internal steps, smart model for agent execution.
Layer 3: Agent Layer (Domain Plugins)
Plugin-based domain agents with 3-layer behavior system: Base (code, locked safety rules) + Soul (DB, tenant-customizable personality/rules/workflows) + Runtime (per-request user context). Tenants customize agents via admin UI soul editor — similar to GoClaw's SOUL.md but multi-tenant.
Safety rules, tool definitions, mandatory constraints. Locked — not editable by tenants.
Personality, company rules, workflows, terminology, tool toggles. Editable per tenant via admin UI.
User name, role, permissions, current date, conversation history. Injected per request.
Layer 4: ERP Connector
Abstraction layer for ERP APIs with hybrid model support: Twendee ERP + customer ERPs via ERPAdapter interface. Opossum circuit breaker, per-tenant auth, response caching.
Layer 5: Proactive Intelligence
AI that acts proactively without being asked. BullMQ cron jobs for scheduled analysis, event-triggered anomaly detection, configurable threshold alerts per tenant.
Layer 6: Output
Report generation (Vietnamese locale), confirmed action execution, and full audit trail for every AI decision. Every LLM call logged with input, plan, results, tokens, and reasoning.
4.1 Main Chat Flow (User Request → Response)
sequenceDiagram
participant U as User
participant CH as Channel Adapter
participant MW as Tenant Middleware
participant ORC as Orchestrator
participant LLM as LLM Service
participant AGT as Domain Agent
participant ERP as ERP Connector
participant DB as PostgreSQL
participant AUD as Audit Service
U->>CH: Send message
CH->>CH: parseIncoming to UnifiedMessage
CH->>MW: Pass with JWT
MW->>DB: SET app.current_tenant_id
MW->>ORC: OrchestratorInput
rect rgb(217,119,6,0.05)
Note over ORC,LLM: Orchestrator Pipeline
ORC->>LLM: Classify intent (fast model)
LLM-->>ORC: intent + targetAgents
ORC->>LLM: Generate plan (fast model)
LLM-->>ORC: ExecutionPlan
end
ORC->>AGT: Dispatch AgentTask
rect rgb(8,145,178,0.05)
Note over AGT,ERP: Agent Execution
AGT->>LLM: Tool calling (smart model)
LLM-->>AGT: Tool call request
AGT->>ERP: ERPRequest
ERP-->>AGT: ERPResponse
AGT-->>ORC: AgentResult
end
ORC->>LLM: Verify response (fast model)
LLM-->>ORC: Verified
ORC->>AUD: Log AuditEntry
ORC->>DB: Save conversation
ORC-->>CH: ChannelResponse
CH-->>U: Formatted reply
4.2 ERP Connector Flow (Circuit Breaker + Cache)
graph TD
REQ["Agent Tool Call"] --> CACHE{"Redis Cache?"}
CACHE -->|"HIT"| CACHED["Return cached ERPResponse"]
CACHE -->|"MISS"| AUTH["Get Tenant Auth Token"]
AUTH --> REG{"Adapter Registry"}
REG --> TWD["TwendeeERPAdapter"]
REG --> MISA["MISAAdapter"]
REG --> SAP["SAPAdapter"]
TWD --> CB{"Circuit Breaker"}
MISA --> CB
SAP --> CB
CB -->|"CLOSED"| HTTP["HTTP Request to ERP"]
CB -->|"OPEN"| STALE["Return stale cache"]
CB -->|"HALF-OPEN"| TEST["Test one request"]
HTTP --> STORE["Cache Response in Redis"]
STORE --> RES["Return ERPResponse"]
TEST -->|"Success"| RES
TEST -->|"Fail"| STALE
classDef hitCls fill:#05966911,stroke:#059669,stroke-width:1.5px
classDef missCls fill:#d9770611,stroke:#d97706,stroke-width:1.5px
classDef errCls fill:#dc262611,stroke:#dc2626,stroke-width:1.5px
class CACHED,RES hitCls
class STALE errCls
class REQ,AUTH missCls
4.3 Proactive Intelligence Flow
graph TD
CRON["BullMQ Scheduler"] --> JOB["Proactive Job Processor"]
EVENT["ERP Event Webhook"] --> JOB
JOB --> CTX["Set Tenant RLS Context"]
CTX --> DETECT{"Detection Type"}
DETECT -->|"Scheduled"| BRIEF["Daily CEO Briefing"]
DETECT -->|"Anomaly"| ANOMALY["Anomaly Detector"]
DETECT -->|"Threshold"| THRESH["Threshold Monitor"]
BRIEF --> ALERT["Generate ProactiveAlert"]
ANOMALY --> ALERT
THRESH --> ALERT
ALERT --> DISPATCH["Alert Dispatch Service"]
DISPATCH --> WEB["Web Chat Push"]
DISPATCH --> TG["Telegram Message"]
DISPATCH --> LOG["Audit Log"]
classDef trigCls fill:#65a30d11,stroke:#65a30d,stroke-width:1.5px
classDef alertCls fill:#d9770611,stroke:#d97706,stroke-width:1.5px
classDef outCls fill:#0891b211,stroke:#0891b2,stroke-width:1.5px
class CRON,EVENT trigCls
class ALERT,ANOMALY,THRESH alertCls
class WEB,TG,LOG outCls
4.4 Multi-Step Execution (Parallel + Sequential)
graph TD
PLAN["ExecutionPlan: mixed strategy"] --> BATCH1
subgraph BATCH1["Batch 1 - Parallel"]
STEP_A["Step A: Finance Agent"]
STEP_B["Step B: HR Agent"]
end
BATCH1 --> AGG1["Collect Results A + B"]
AGG1 --> STEP_C["Step C: Sales Agent
depends on A, B"]
STEP_C --> STEP_D["Step D: Aggregator
depends on C"]
STEP_D --> VERIFY["Verifier: Complete?"]
VERIFY -->|"Yes"| RESP["Final Response"]
VERIFY -->|"No"| RETRY["Retry or Clarify"]
classDef parallelCls fill:#0891b211,stroke:#0891b2,stroke-width:1.5px
classDef seqCls fill:#d9770611,stroke:#d97706,stroke-width:1.5px
classDef doneCls fill:#05966911,stroke:#059669,stroke-width:1.5px
class STEP_A,STEP_B parallelCls
class STEP_C,STEP_D seqCls
class RESP doneCls
Every data table uses RLS with tenant_id = current_setting('app.current_tenant_id'). Tenant context is set per request in middleware from JWT token. BullMQ workers manually set context before DB queries.
erDiagram
TENANTS ||--o{ USERS : has
TENANTS ||--o{ AGENT_SOULS : customizes
TENANTS ||--o{ CONVERSATIONS : owns
TENANTS ||--o{ AUDIT_LOGS : records
TENANTS ||--o{ PROACTIVE_RULES : configures
TENANTS {
uuid id PK
text name
text slug
enum llm_provider
text llm_api_key_encrypted
text erp_type
jsonb erp_config_encrypted
jsonb agent_permissions
}
USERS {
uuid id PK
uuid tenant_id FK
text email
text password_hash
enum role
jsonb permissions
}
AGENT_SOULS {
uuid id PK
uuid tenant_id FK
text agent_type
text personality
text company_rules
text workflows
jsonb terminology
jsonb enabled_tools
jsonb tool_config
}
CONVERSATIONS {
uuid id PK
uuid tenant_id FK
uuid user_id FK
text channel
jsonb messages
}
AUDIT_LOGS {
uuid id PK
uuid tenant_id FK
uuid user_id FK
text action
text input
jsonb plan
jsonb agent_results
text final_response
text llm_provider
int tokens_used
int duration_ms
}
PROACTIVE_RULES {
uuid id PK
uuid tenant_id FK
text name
enum type
text cron_expression
jsonb config
boolean enabled
jsonb recipients
}
| Layer | Technology | Purpose |
|---|---|---|
| Monorepo | pnpm workspaces + Turborepo | 3 apps + shared types + shared Tailwind |
| Runtime | Node.js 22+ / TypeScript 5.x | Core platform |
| Backend | NestJS 11 | Modular monolith with DI |
| Admin UI | React + Vite + shadcn/ui + TanStack Table | Settings, audit, usage, users |
| Chat Widget | React + Vite + shadcn/ui + Socket.io | Embeddable UMD bundle |
| Styling | Tailwind CSS (shared preset) | Consistent design system across FE apps |
| Database | PostgreSQL 16 + Drizzle ORM | RLS multi-tenancy |
| Cache/Queue | Redis 7 + BullMQ | Caching, job queue, token metering |
| LLM | Vercel AI SDK (@ai-sdk/*) | Multi-provider: OpenAI, Claude, Gemini, Ollama |
| Telegram | grammY + @grammyjs/nestjs | Bot API with webhooks |
| Auth | JWT + Passport.js | Multi-tenant authentication |
| ERP Resilience | Opossum + Axios | Circuit breaker + HTTP client |
| Charts | recharts | Usage dashboard charts (admin UI) |
| Reports | PDFKit + ExcelJS | Vietnamese locale PDF/Excel |
| Encryption | AES-256-GCM | API keys, credentials at rest |
| Testing | Vitest + Supertest | Unit + integration + E2E |
| Deployment | Docker + Docker Compose | Container orchestration |
One Docker image for API + Admin UI (static files baked in at build time). Chat widget deployed separately to CDN (embedded on customer websites). PostgreSQL + Redis as managed services or Docker containers.
graph TD
subgraph Internet
BROWSER["Browser / Mobile"]
CUST["Customer Website"]
end
subgraph NGINX["Nginx Reverse Proxy"]
SSL["SSL Termination"]
WS["WebSocket Upgrade"]
end
subgraph DOCKER["Docker Image - API + Admin"]
API["NestJS API
Port 3000"]
ADMIN["Admin UI
/admin/* static"]
FONTS["Vietnamese Fonts"]
end
subgraph DATA["Data Layer"]
PG[("PostgreSQL 16
+ RLS")]
REDIS[("Redis 7
Cache + Queue")]
end
CDN["CDN
chat-widget.umd.js"]
TELEGRAM["Telegram API"]
ERP["ERP Systems"]
BROWSER -->|"HTTPS"| NGINX
CUST -->|"script tag"| CDN
CDN -->|"WebSocket"| NGINX
NGINX --> API
NGINX -->|"WS upgrade"| API
TELEGRAM -->|"Webhook"| NGINX
API --> PG
API --> REDIS
API --> ERP
API --> TELEGRAM
classDef dockerCls fill:#0891b211,stroke:#0891b2,stroke-width:2px
classDef dataCls fill:#05966911,stroke:#059669,stroke-width:1.5px
classDef extCls fill:none,stroke-dasharray:5 5
class API,ADMIN,FONTS dockerCls
class PG,REDIS dataCls
class TELEGRAM,ERP,CDN extCls
Docker Build Pipeline (Multi-stage)
Chat Widget CDN
Widget built as UMD bundle (<100KB gzipped), uploaded to CDN on tag push. Customers embed via script tag:
Versioned paths (/v1/, /v2/) — customers control which version they embed. Widget connects to API via WebSocket.
| Decision | Choice | Rationale |
|---|---|---|
| Architecture | Modular Monolith (NestJS) | Fast to build in 2 months; clean module boundaries; split to microservices later |
| Orchestration | Pure TS + Vercel AI SDK | No LangGraph/CrewAI; simpler stack; built-in tool calling |
| LLM Cost | Tiered models (fast/smart) | GPT-4o-mini for internal steps; full model for agent execution. ~70% cost reduction |
| ERP Model | Hybrid + Adapter Pattern | ERPAdapter interface per ERP type; TwendeeERPAdapter first; others post-MVP |
| Multi-Tenancy | PostgreSQL RLS + Drizzle | DB-level isolation; middleware sets tenant context per request |
| Monorepo | pnpm workspaces + Turborepo | 3 apps share types + Tailwind; single Docker build; cached parallel builds |
| Frontend | shadcn/ui + Tailwind + TanStack Table | Zero bundle overhead; same design system for admin + chat widget |
| Deployment | Hybrid: 1 Docker image + CDN widget | API+Admin in one image (~180MB); widget on CDN for customer embedding |
| Agent Behavior | 3-layer system (Base+Soul+Runtime) | Safety locked in code; tenant customizes via admin soul editor; runtime context per request |
| Zalo OA | Deferred to post-MVP | Approval timeline uncertain; focus on REST + Web Chat + Telegram |
| Vietnamese LLM | Benchmark in Week 1 | Test GPT-4o, Claude, Gemini with finance/HR queries before building agents |
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Vietnamese NLP quality varies by LLM | Medium | High | Week 1 benchmark sprint; tenant can switch provider |
| Multi-tenant data leakage | Low | Critical | RLS at DB + middleware + integration tests with wrong tenant |
| 2-month timeline tight for 5 agents | High | High | Finance+HR first; others can be simplified for MVP |
| LLM hallucination on financial data | Medium | High | System prompt rules: never fabricate numbers; always cite ERP source |
| ERP API format varies per customer | High | High | ERPAdapter pattern; data transformation layer per adapter |
| RLS race condition in concurrent requests | Medium | High | Scope set_config to transaction; per-request context isolation |
- ERP API documentation — OpenAPI/Swagger spec available?
- Data residency — must data stay in Vietnam?
- Self-hosted model choice for Ollama — Vistral, Qwen2, or other?
- Per-tenant billing/token metering UX