A NestJS AI agent driving the ShopNexus e-commerce platform, built on the Strands Agents SDK. The agent supports both Buyers (product discovery, sizing, preference recall, order status) and Sellers (store policies, customer service, dispute guidance).
LLM inference is routed through an OpenAI-compatible LiteLLM Proxy, and long-term personalization memory is persisted locally in PostgreSQL + pgvector using a 2-branch hybrid retrieval engine.
| Core feature | Module | Tech / Protocol |
|---|---|---|
| LLM inference | src/llm |
LiteLLM Proxy (/v1/chat/completions) → Bedrock (Claude 3.5 Sonnet) / OpenAI / Gemini / Ollama |
| Personalization memory | src/memory |
PostgreSQL 16 + pgvector (retrieval.user_memories) + LiteLLM embeddings |
| Chat sessions (new / list) | src/session |
In-memory / data-plane session tracking |
| MCP tools + runtime install | src/mcp |
Strands McpClient → MCP Gateway / remote MCP servers |
| Web search | src/tools/web-search |
Tavily Search API (optional) |
| Output pagination | src/agent/output-cap |
Spills oversized tool results (>20KB) to disk with paging via read_output |
Assembled per turn in AgentService.prepareAgent (src/agent/agent.service.ts):
| Tool | Source | What it does |
|---|---|---|
remember_fact |
src/memory/remember-fact |
Persists buyer sizing/preferences or seller policies with PII guardrails |
search_memory |
MemoryManager.getTools() |
Actively recalls past facts using a hybrid vector + full-text search query |
read_output |
src/agent/output-cap |
Pages through truncated tool output that exceeded the 20KB cap |
install_mcp |
src/mcp |
Registers a remote MCP server by URL at runtime |
web_search |
src/tools/web-search |
Live web search via Tavily (only if TAVILY_API_KEY is set) |
| Gateway / installed | src/mcp |
Domain tools exposed via the MCP Gateway (product search, cart, orders) |
The model discovers tools from their definitions (tool specs) — the system prompt does not hardcode a tool list, so adding or removing tools requires no prompt changes.
flowchart LR
Client -->|/chat, /sessions or CLI| Agent[AgentService]
Agent --> Llm[LlmService]
Agent --> Mem[MemoryService]
Agent --> Mcp[McpService]
Agent --> Web[web_search]
Llm -->|OpenAI Chat API :4000| LiteLLM[(LiteLLM Proxy)]
LiteLLM -->|Converse| Bedrock[(AWS Bedrock: Claude 3.5)]
LiteLLM -.->|Alternative| OtherLLMs[(Gemini / OpenAI / Ollama)]
Mem -->|Embeddings :4000| LiteLLM
Mem -->|Hybrid SQL :5432| Postgres[(PostgreSQL + pgvector)]
Mcp -->|SSE / HTTP| Gateway[(MCP Gateway / Target MCPs)]
Web -->|HTTPS| Tavily[(Tavily API)]
sequenceDiagram
actor U as User (Buyer / Seller)
participant A as AgentService
participant M as MemoryService
participant L as LiteLLM (via Strands loop)
participant T as Tools (MCP / Tavily / Memory)
U->>A: chat(actorId, sessionId, message, context)
A->>M: createManager(actorId, role)
Note over A,M: MemoryManager auto-injects relevant <memory> block into prompt
M-->>A: memory block ([size_fit], [preference], [shop_policy])
A->>L: invoke(message, tools) with grounding systemPrompt
loop while stopReason == tool_use (Strands loop)
L-->>A: tool_use (e.g. search_products, remember_fact)
A->>T: run tool
Note over A,T: AfterToolCall hook caps oversized output (spill + read_output)
T-->>A: result
A->>L: feed result back
end
L-->>A: final answer (stopReason == end_turn)
A->>M: saveTurn(user, assistant)
A-->>U: answer (streamed in the CLI)
flowchart TD
UserFact["User shares fact<br/>(size, preference, policy)"] --> Guardrails{"PII Guardrails<br/>(cards, OTP, passwords, CCCD)"}
Guardrails -->|Blocked| Reject["Reject: Sensitive PII"]
Guardrails -->|Passed| LiteLLM["Generate Vector<br/>(LiteLLM /v1/embeddings)"]
LiteLLM --> Store["Insert into retrieval.user_memories<br/>(user_id, role, category, embedding)"]
Store --> Supersede["Check & supersede older<br/>conflicting facts in same category"]
Query["Turn prompt or search_memory"] --> HybridSearch["Hybrid Search:<br/>Cosine Distance + Postgres FTS"]
Store --> HybridSearch
HybridSearch --> RRF["Reciprocal Rank Fusion (RRF)<br/>score = 1/(60+r_vec) + 1/(60+r_fts)"]
RRF --> Block["Top-K injected as bounded <memory> block"]
- Grounding system prompt (
buildSystemPrompt): Rebuilt each turn with the real current datetime (ISO format), role-specific guidelines (buyerorseller), and active page context (product,cart,order,home). Instructs the model to trust tool results over training priors and forbids fabricating inventory, stock, or prices. - E-commerce personalization memory: Facts are partitioned by role (
buyer|seller) across 5 dedicated categories:size_fit(shoe/apparel sizes),preference(brands, colors, price range),profile(preferred delivery location, nickname),feedback(past purchase sentiment), andshop_policy(seller return/warranty policies). Outdated facts are automatically markedsuperseded_at = NOW()when superseded. - Sensitive data guardrails (
src/memory/remember-fact.ts): Pre-write guardrails block credit/debit card numbers, CVV/CVC, OTP/PIN codes, passwords/API keys, Vietnamese national IDs (CCCD/CMND), and bank account numbers before any data reaches the database. - Hybrid vector + lexical retrieval: Uses PostgreSQL 16
pgvectorfor cosine distance and full-text search (to_tsvector('simple')) with Unicode NFC normalization. Merged using Reciprocal Rank Fusion (RRF) to match both conceptual meaning and exact tokens (e.g. "size 42", "Nike Pegasus", "Quận 1"). - Bounded tool output (
src/agent/output-cap.ts): AnAfterToolCallEventhook caps tool results exceedingAGENT_TOOL_OUTPUT_CAP(default 20,000 chars); excess text spills to a temp file that the model pages through viaread_output(id, offset/limit). - Bounded history:
SlidingWindowConversationManagerpreserves recent messages while maintaining tool call/result pairs and pruning older turns so multi-cycle tool execution does not cause context exhaustion. - Provider-agnostic LLM Gateway: Requests use the standard OpenAI Chat Completion protocol pointed at the local LiteLLM Proxy. Switching from Claude to GPT-4o, Gemini 2.5, or local Ollama requires updating only
docker/litellm-config.yamland.env. - Runtime MCP install: Say "install this MCP: https://example.com/mcp" and the
install_mcptool registers the server (persisted indata/mcp-servers.json). The agent rebuilds its toolset dynamically for subsequent turns.
All configuration is parsed and validated once using Zod into a frozen object in src/config.ts. Defaults live in code, so most environment variables are optional. See .env.example.
| Env | Default | Effect |
|---|---|---|
AGENT_MODEL_ID |
claude-3.5-sonnet |
Model name routed by LiteLLM Proxy |
LITELLM_BASE_URL |
http://localhost:4000/v1 |
Base URL of the LiteLLM Proxy |
LITELLM_API_KEY |
sk-local-dev-key |
Master key for LiteLLM Proxy authentication |
DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/shopnexus_agent |
PostgreSQL connection string for pgvector memory |
EMBEDDING_MODEL_ID |
text-embedding-default |
Embedding model ID in litellm-config.yaml (1536 dimensions) |
AGENT_MAX_TOKENS |
4096 |
Max output tokens generated per model call |
AGENT_THINKING_BUDGET |
0 |
> 0 enables Claude extended thinking tokens (temperature forced to 1) |
AGENT_MAX_TURNS |
0 (unbounded) |
Cap on tool-use cycles per turn |
AGENT_TOOL_OUTPUT_CAP |
20000 |
Max characters allowed in a tool result before disk spillover |
AGENT_MEMORY_ENTRY_CAP |
500 |
Max characters per injected memory entry |
AGENT_CONVERSATION_WINDOW |
40 |
Recent messages retained in the conversation sliding window |
TAVILY_API_KEY |
— | Enables the web_search tool when provided |
pnpm install
cp .env.example .envdocker compose -f docker/docker-compose.yml up -dThis starts:
- LiteLLM Proxy on port
4000(configured withdocker/litellm-config.yaml). - PostgreSQL 16 + pgvector on port
5432(automatically executesdocker/init-db/01-init-memory.sqlto initialize extensions and theretrieval.user_memoriestable).
Verify proxy health:
curl http://localhost:4000/healthpnpm run build# Interactive CLI: streams thinking, tool calls, outputs, and final responses
pnpm run chat
# Scripted demo
pnpm run demo
# Start NestJS HTTP API server on :3000
pnpm run start:devcurl -s http://localhost:3000/chat \
-H 'Content-Type: application/json' \
-d '{
"actorId": "buyer-123",
"message": "Recommend a pair of running shoes under $100. I wear size 42.",
"context": {
"role": "buyer",
"locale": "en",
"page": "product"
}
}'curl -s http://localhost:3000/chat \
-H 'Content-Type: application/json' \
-d '{
"actorId": "seller-456",
"message": "A customer is asking for a return on an opened electronic item. What is our policy?",
"context": {
"role": "seller",
"locale": "en"
}
}'# Create a new chat session explicitly:
curl -s -X POST http://localhost:3000/sessions \
-H 'Content-Type: application/json' \
-d '{"actorId": "buyer-123"}'
# List active sessions for an actor:
curl -s 'http://localhost:3000/sessions?actorId=buyer-123'agent/
├── docker/
│ ├── docker-compose.yml # LiteLLM Proxy & PostgreSQL pgvector services
│ ├── litellm-config.yaml # Upstream LLM and Embedding routing definitions
│ └── init-db/
│ └── 01-init-memory.sql # Schema migration: pgvector extension & user_memories table
├── src/
│ ├── agent/ # Core agent loop, system prompt, controller & output cap
│ │ ├── agent.controller.ts # HTTP endpoints (/chat, /sessions)
│ │ ├── agent.service.ts # Agent preparation, context injection, invoke/stream
│ │ └── output-cap.ts # Hook & paging tool for oversized tool outputs
│ ├── db/ # Database connection
│ │ └── db.pool.ts # PostgreSQL connection pool with schema auto-init
│ ├── llm/ # LiteLLM client integration
│ │ ├── llm.service.ts # Wraps OpenAIModel from Strands SDK for chat
│ │ └── embedding.service.ts # Generates vectors via LiteLLM /v1/embeddings
│ ├── memory/ # E-commerce personalization memory
│ │ ├── memory.service.ts # Integrates MemoryRepository with Strands MemoryStore
│ │ ├── memory.repository.ts # pgvector + FTS hybrid search & supersede operations
│ │ ├── remember-fact.ts # Tool for saving facts with PII guardrails
│ │ └── types.ts # Memory types, roles (buyer/seller), categories
│ ├── mcp/ # MCP client connection & runtime install
│ ├── session/ # Session lifecycle tracking
│ ├── tools/ # Toolset registry & Tavily web search
│ ├── app.module.ts # Root NestJS module registering all services
│ ├── cli.ts # Terminal interactive chat REPL
│ ├── config.ts # Centralized Zod configuration validation
│ └── main.ts # NestJS application bootstrap
├── package.json
└── tsconfig.json