llm-agent-runtime 0.1.6 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/USAGE.md +528 -283
- package/dist/agent.d.ts +2 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +36 -4
- package/dist/create-runtime.d.ts +14 -4
- package/dist/create-runtime.d.ts.map +1 -1
- package/dist/create-runtime.js +108 -20
- package/dist/embeddings/xenova.d.ts +19 -0
- package/dist/embeddings/xenova.d.ts.map +1 -0
- package/dist/embeddings/xenova.js +69 -0
- package/dist/env-llm.d.ts +17 -4
- package/dist/env-llm.d.ts.map +1 -1
- package/dist/env-llm.js +66 -16
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/intents/define.d.ts +35 -0
- package/dist/intents/define.d.ts.map +1 -0
- package/dist/intents/define.js +52 -0
- package/dist/intents/general.d.ts +37 -0
- package/dist/intents/general.d.ts.map +1 -0
- package/dist/intents/general.js +67 -0
- package/dist/llm.d.ts +2 -2
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +45 -15
- package/dist/rag/index.d.ts +67 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/index.js +140 -0
- package/dist/rag/loaders.d.ts +11 -0
- package/dist/rag/loaders.d.ts.map +1 -0
- package/dist/rag/loaders.js +80 -0
- package/dist/rag/sources.d.ts +29 -0
- package/dist/rag/sources.d.ts.map +1 -0
- package/dist/rag/sources.js +101 -0
- package/dist/services/intent-embeddings.d.ts.map +1 -1
- package/dist/services/intent-embeddings.js +5 -6
- package/dist/services/token-usage.d.ts +26 -0
- package/dist/services/token-usage.d.ts.map +1 -0
- package/dist/services/token-usage.js +63 -0
- package/dist/services/usage-store.d.ts +24 -0
- package/dist/services/usage-store.d.ts.map +1 -0
- package/dist/services/usage-store.js +92 -0
- package/dist/types.d.ts +47 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -3
package/USAGE.md
CHANGED
|
@@ -4,28 +4,411 @@ Reusable LangGraph agent engine. Your app supplies **intents, prompts, and tools
|
|
|
4
4
|
|
|
5
5
|
`resolveFlow → runAgent → extract tool outputs → finalize`
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
RAG stays in **your app**: index docs, retrieve chunks, inject via `context` / `buildSystemPrompt`.
|
|
8
|
+
|
|
9
|
+
**Where to add RAG docs:** put paths/URLs in `.env.local` as `RAG_SOURCES` (see [Example B](#example-b--rag-chatbot-matches-plain-agent-demo)). Prefer `.md` / `.docx` (some PDFs fail with `bad XRef entry`).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
RAG_SOURCES=doc/faq.md,doc/handbook.docx,https://cdn.example.com/policy.docx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Index **once** with `getRagStore()`; each message only runs `retrieve()` (query embed + top-K). Example B matches `plain-agent-demo`.
|
|
8
16
|
|
|
9
17
|
---
|
|
10
18
|
|
|
11
19
|
## Install
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
pnpm add llm-agent-runtime @langchain/core @langchain/langgraph @langchain/langgraph-checkpoint zod
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Optional (recommended for production without Ollama):**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @xenova/transformers # free in-process embeddings
|
|
29
|
+
pnpm add langsmith # tracing
|
|
17
30
|
```
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
If `@xenova/transformers` fails on `sharp` (pnpm ignored build scripts):
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- `zod`
|
|
34
|
+
```bash
|
|
35
|
+
cd node_modules/sharp && npm run install
|
|
36
|
+
```
|
|
25
37
|
|
|
26
38
|
---
|
|
27
39
|
|
|
28
|
-
##
|
|
40
|
+
## Configure a project (`.env`)
|
|
41
|
+
|
|
42
|
+
Minimal **chatbot** on OpenRouter (free embeddings via Xenova):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Chat
|
|
46
|
+
LLM_PROVIDER=openrouter
|
|
47
|
+
OPENROUTER_API_KEY=sk-or-v1-...
|
|
48
|
+
OPENROUTER_MODEL=meta-llama/llama-3.1-70b-instruct
|
|
49
|
+
OPENROUTER_INTENT_ROUTER_MODEL=meta-llama/llama-3.1-70b-instruct
|
|
50
|
+
|
|
51
|
+
# Embeddings — local default for openrouter is xenova (free)
|
|
52
|
+
EMBEDDINGS_PROVIDER=xenova
|
|
53
|
+
# XENOVA_EMBEDDINGS_MODEL=Xenova/all-MiniLM-L6-v2
|
|
54
|
+
|
|
55
|
+
# Vercel / Lambda / Netlify: xenova will NOT load. Set:
|
|
56
|
+
# EMBEDDINGS_PROVIDER=openai
|
|
57
|
+
# OPENAI_API_KEY=sk-...
|
|
58
|
+
# (If OPENAI_API_KEY is set on those hosts, the package auto-selects openai
|
|
59
|
+
# even when EMBEDDINGS_PROVIDER is unset or xenova.)
|
|
60
|
+
|
|
61
|
+
# Optional Redis (flow state / cache). Plain Redis works for cache;
|
|
62
|
+
# Redis Stack (RedisJSON) needed for durable LangGraph checkpoints.
|
|
63
|
+
REDIS_URL=redis://localhost:6379
|
|
64
|
+
|
|
65
|
+
# Optional LangSmith
|
|
66
|
+
LANGSMITH_TRACING=true
|
|
67
|
+
LANGSMITH_API_KEY=lsv2_pt_...
|
|
68
|
+
LANGSMITH_PROJECT=my-agent
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Embeddings options**
|
|
72
|
+
|
|
73
|
+
| `EMBEDDINGS_PROVIDER` | Cost | When to use |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `xenova` | Free | Local default for OpenRouter/OpenAI chat when unset |
|
|
76
|
+
| `ollama` | Free (self-host) | Default when `LLM_PROVIDER=ollama` |
|
|
77
|
+
| `openai` | Paid | **Required on Vercel/serverless**; strongest cloud embeddings |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Suggested app layout
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
your-app/
|
|
85
|
+
.env / .env.local
|
|
86
|
+
agent/
|
|
87
|
+
runtime.ts # createAgentRuntime({ ... })
|
|
88
|
+
intents.ts # intents + menuMap + aliases
|
|
89
|
+
prompts/ # system / intent prompts
|
|
90
|
+
tools/ # LangChain tools (APIs, DB, …)
|
|
91
|
+
rag/ # optional — or skip and use getRagStore() from the package
|
|
92
|
+
controllers/ # HTTP / WhatsApp → retrieve then invoke
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Put document paths/URLs in `.env` as `RAG_SOURCES=...` (see Example B). No custom `rag/store.ts` required — import `getRagStore` from `llm-agent-runtime`.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Example A — Normal chatbot (no RAG)
|
|
100
|
+
|
|
101
|
+
Intent routing + tools + sticky flows. No vector store.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// agent/runtime.ts
|
|
105
|
+
import {
|
|
106
|
+
createAgentRuntime,
|
|
107
|
+
llmConfigFromEnv,
|
|
108
|
+
} from "llm-agent-runtime";
|
|
109
|
+
import { tool } from "@langchain/core/tools";
|
|
110
|
+
import { z } from "zod";
|
|
111
|
+
|
|
112
|
+
const greet = tool(async () => "Welcome! Reply 1 to recharge or 2 to login.", {
|
|
113
|
+
name: "show_menu",
|
|
114
|
+
description: "Show the main menu",
|
|
115
|
+
schema: z.object({}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const recharge = tool(
|
|
119
|
+
async ({ amount, phone }) => ({ ok: true, amount, phone }),
|
|
120
|
+
{
|
|
121
|
+
name: "buy_airtime",
|
|
122
|
+
description: "Buy airtime for a phone number",
|
|
123
|
+
schema: z.object({
|
|
124
|
+
amount: z.number(),
|
|
125
|
+
phone: z.string(),
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
export async function getRuntime() {
|
|
131
|
+
const llm = llmConfigFromEnv(); // reads LLM_PROVIDER, EMBEDDINGS_PROVIDER, …
|
|
132
|
+
|
|
133
|
+
const runtime = await createAgentRuntime({
|
|
134
|
+
redisUrl: process.env.REDIS_URL,
|
|
135
|
+
llm,
|
|
136
|
+
tracing: {
|
|
137
|
+
enabled: Boolean(process.env.LANGSMITH_API_KEY),
|
|
138
|
+
projectName: process.env.LANGSMITH_PROJECT ?? "my-agent",
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
intents: [
|
|
142
|
+
{
|
|
143
|
+
name: "greetings",
|
|
144
|
+
examples: ["hi", "hello", "menu", "help"],
|
|
145
|
+
description: "greetings or main menu",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "recharge",
|
|
149
|
+
examples: ["buy airtime", "recharge my phone", "buy data"],
|
|
150
|
+
sticky: true,
|
|
151
|
+
description: "buy airtime or data",
|
|
152
|
+
regex: /(buy|recharge).*(airtime|data)/i,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
menuMap: { "1": "recharge" },
|
|
157
|
+
|
|
158
|
+
intentRegistry: {
|
|
159
|
+
greetings: {
|
|
160
|
+
prompt: () =>
|
|
161
|
+
"You are a helpful recharge assistant. Greet the user and show menu options.",
|
|
162
|
+
tools: () => [greet],
|
|
163
|
+
},
|
|
164
|
+
recharge: {
|
|
165
|
+
prompt: () =>
|
|
166
|
+
"Collect amount and phone, then call buy_airtime when ready.",
|
|
167
|
+
tools: () => [recharge],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
toolOutputMap: { buy_airtime: "transaction" },
|
|
172
|
+
completeOn: ["buy_airtime"],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await runtime.connect();
|
|
176
|
+
return runtime;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// controllers/chat.ts
|
|
182
|
+
const runtime = await getRuntime();
|
|
183
|
+
|
|
184
|
+
const result = await runtime.invoke({
|
|
185
|
+
message: req.body.message,
|
|
186
|
+
threadId: req.body.phoneNumber, // conversation id
|
|
187
|
+
phoneKey: req.body.phoneNumber,
|
|
188
|
+
context: { userInfo: req.body.user ?? {} },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
res.json({
|
|
192
|
+
message: result.answer,
|
|
193
|
+
intent: result.intent,
|
|
194
|
+
flow: result.flow,
|
|
195
|
+
toolOutputs: result.toolOutputs,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Example B — RAG chatbot (matches `plain-agent-demo`)
|
|
202
|
+
|
|
203
|
+
Point the app at **local files and/or http(s) URLs**, **index once**, then **retrieve per message**.
|
|
204
|
+
Document chunking + embeddings are **not** rebuilt on every chat — only the user query is embedded each time.
|
|
205
|
+
|
|
206
|
+
### Layout (what each file does)
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
your-app/
|
|
210
|
+
.env.local # RAG_SOURCES + LLM + EMBEDDINGS_PROVIDER
|
|
211
|
+
index.ts # load .env.local FIRST, mount POST /chat-bot/rag-chatbot
|
|
212
|
+
src/
|
|
213
|
+
routers/index.ts # route → ragChatBotController
|
|
214
|
+
controllers/index.ts # retrieve snippets → invoke RAG runtime
|
|
215
|
+
rag/
|
|
216
|
+
store.ts # optional warm-up (getRagStore lives in the package)
|
|
217
|
+
runtime.ts # createAgentRuntime + buildSystemPrompt (RAG Q&A)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Piece | What it does |
|
|
221
|
+
|-------|----------------|
|
|
222
|
+
| `RAG_SOURCES` | Comma-separated paths/URLs to index |
|
|
223
|
+
| `getRagStore()` | Singleton: load → chunk → embed docs **once** per process |
|
|
224
|
+
| `store.retrieve(msg, 4)` | Embed the **query only**, return top-K text snippets |
|
|
225
|
+
| `buildSystemPrompt` | Puts snippets into the system prompt as `Knowledge:` |
|
|
226
|
+
| `getRagRuntime()` | Separate agent (no tools); answers from Knowledge only |
|
|
227
|
+
| `explicitIntent: "qa"` | Skip intent-router LLM (faster, deterministic) |
|
|
228
|
+
|
|
229
|
+
### 1) Env — documents + embeddings
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# .env.local — `import "dotenv/config"` alone only loads `.env`, not `.env.local`
|
|
233
|
+
EMBEDDINGS_PROVIDER=xenova
|
|
234
|
+
|
|
235
|
+
# Prefer .md / .docx (some PDFs fail pdf.js with "bad XRef entry")
|
|
236
|
+
RAG_SOURCES=doc/faq.md,doc/handbook.docx,https://cdn.example.com/policy.docx
|
|
237
|
+
|
|
238
|
+
# Multiple files = comma-separated
|
|
239
|
+
# RAG_SOURCES=/abs/a.docx,/abs/b.md
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
pnpm add pdf-parse mammoth @xenova/transformers
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// index.ts — load env BEFORE getRagStore / createAgentRuntime
|
|
248
|
+
import dotenv from "dotenv";
|
|
249
|
+
dotenv.config({ path: ".env.local" });
|
|
250
|
+
dotenv.config(); // optional `.env` fallback (won't override keys already set)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 2) Optional warm-up (`src/rag/store.ts`)
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { getRagStore, getRagDocumentCount } from "llm-agent-runtime";
|
|
257
|
+
|
|
258
|
+
/** Optional: index at startup so the first HTTP request is faster. */
|
|
259
|
+
export async function warmRagStore() {
|
|
260
|
+
const store = await getRagStore(); // reads RAG_SOURCES; chunks + embeds once
|
|
261
|
+
console.log(`[rag] ready — ${getRagDocumentCount()} document(s) indexed`);
|
|
262
|
+
return store;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { getRagStore, getRagDocumentCount };
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- **`getRagStore()`** — lazy singleton. First await indexes; later awaits reuse memory.
|
|
269
|
+
- **`resetRagStore()`** — clear singleton (tests / re-index after changing sources).
|
|
270
|
+
- Overrides: `getRagStore({ embeddings, env, chunkSize, overlap, log: false })`.
|
|
271
|
+
- Unreadable sources are **skipped with a warning**; if none load, it throws (next call can retry).
|
|
272
|
+
|
|
273
|
+
### 3) RAG agent runtime (`src/rag/runtime.ts`)
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import {
|
|
277
|
+
createAgentRuntime,
|
|
278
|
+
llmConfigFromEnv,
|
|
279
|
+
withGeneralIntents, // or defineIntents([...]) without the general catch-all
|
|
280
|
+
type AgentRuntime,
|
|
281
|
+
} from "llm-agent-runtime";
|
|
282
|
+
|
|
283
|
+
export type RagContext = {
|
|
284
|
+
ragContext?: string;
|
|
285
|
+
userInfo?: Record<string, unknown>;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const buildSystemPrompt = (ctx: { context?: RagContext }) => {
|
|
289
|
+
const rag = String(ctx.context?.ragContext ?? "").trim();
|
|
290
|
+
const base =
|
|
291
|
+
"Answer using ONLY the knowledge snippets below. If missing, say you don't know.";
|
|
292
|
+
return rag ? `${base}\n\nKnowledge:\n${rag}` : base;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
let ragPromise: Promise<AgentRuntime<RagContext>> | null = null;
|
|
296
|
+
|
|
297
|
+
export async function getRagRuntime(): Promise<AgentRuntime<RagContext>> {
|
|
298
|
+
if (!ragPromise) {
|
|
299
|
+
ragPromise = (async () => {
|
|
300
|
+
// Unified intent: name/examples/description + prompt/tools in ONE object
|
|
301
|
+
const { intents, intentRegistry } = withGeneralIntents([
|
|
302
|
+
{
|
|
303
|
+
name: "qa",
|
|
304
|
+
examples: ["what is", "how do I", "explain", "tell me about", "question"],
|
|
305
|
+
description: "answer questions from indexed documents",
|
|
306
|
+
prompt: () =>
|
|
307
|
+
"Answer the user question from the knowledge provided. Be concise.",
|
|
308
|
+
tools: () => [],
|
|
309
|
+
},
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const runtime = await createAgentRuntime<RagContext>({
|
|
313
|
+
// omit redisUrl for RAG Q&A
|
|
314
|
+
llm: llmConfigFromEnv(),
|
|
315
|
+
intents,
|
|
316
|
+
intentRegistry,
|
|
317
|
+
defaultIntent: "qa",
|
|
318
|
+
buildSystemPrompt,
|
|
319
|
+
recursionLimit: 4,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await runtime.connect();
|
|
323
|
+
return runtime;
|
|
324
|
+
})();
|
|
325
|
+
}
|
|
326
|
+
return ragPromise;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`withGeneralIntents` / `defineIntents` split that one object into the two lists the runtime still uses internally (`intents` for routing, `intentRegistry` for prompt/tools).
|
|
331
|
+
|
|
332
|
+
### 4) Controller — retrieve then invoke
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { getRagStore } from "llm-agent-runtime";
|
|
336
|
+
import { getRagRuntime } from "../rag/runtime.js";
|
|
337
|
+
|
|
338
|
+
export const ragChatBotController = async (req, res) => {
|
|
339
|
+
const { message, phoneNumber } = req.body;
|
|
340
|
+
if (!message || !phoneNumber) {
|
|
341
|
+
return res.status(400).json({ error: "message and phoneNumber are required" });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const threadId = String(req.body.sessionId ?? phoneNumber);
|
|
346
|
+
|
|
347
|
+
// Singleton store (indexes once). retrieve() embeds ONLY the user message.
|
|
348
|
+
const store = await getRagStore();
|
|
349
|
+
const ragContext = await store.retrieve(String(message), 4);
|
|
350
|
+
|
|
351
|
+
const runtime = await getRagRuntime();
|
|
352
|
+
const result = await runtime.invoke({
|
|
353
|
+
message: String(message),
|
|
354
|
+
threadId,
|
|
355
|
+
phoneKey: threadId,
|
|
356
|
+
explicitIntent: "qa", // skip intent-router LLM
|
|
357
|
+
context: {
|
|
358
|
+
ragContext, // consumed by buildSystemPrompt
|
|
359
|
+
userInfo: req.body.user ?? {},
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
res.json({
|
|
364
|
+
message: result.answer,
|
|
365
|
+
intent: result.intent,
|
|
366
|
+
ragChars: ragContext.length,
|
|
367
|
+
});
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.error("[rag] chat failed:", err);
|
|
370
|
+
res.status(500).json({
|
|
371
|
+
error: err instanceof Error ? err.message : "RAG chat failed",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
chatBotRouter.post("/rag-chatbot", ragChatBotController);
|
|
379
|
+
// POST /chat-bot/rag-chatbot { "message": "...", "phoneNumber": "..." }
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Flow (once vs per message)
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
Startup / first getRagStore():
|
|
386
|
+
RAG_SOURCES=doc/a.md,doc/b.docx
|
|
387
|
+
→ read/download files
|
|
388
|
+
→ extract text (.md utf8, .docx mammoth, .pdf pdf-parse)
|
|
389
|
+
→ chunk (default 800 / overlap 120) + embed each chunk
|
|
390
|
+
→ keep in memory (singleton)
|
|
391
|
+
|
|
392
|
+
Each POST /chat-bot/rag-chatbot:
|
|
393
|
+
store.retrieve(userMessage, 4) // embed query only + cosine top-K
|
|
394
|
+
→ invoke({ explicitIntent: "qa", context: { ragContext } })
|
|
395
|
+
→ buildSystemPrompt injects Knowledge
|
|
396
|
+
→ LLM answers from snippets only
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Tips / pitfalls
|
|
400
|
+
|
|
401
|
+
| Issue | Fix |
|
|
402
|
+
|-------|-----|
|
|
403
|
+
| `No RAG sources configured` | Set `RAG_SOURCES` in `.env.local` and load it |
|
|
404
|
+
| Request hangs forever | Omit `redisUrl` on the RAG runtime |
|
|
405
|
+
| `bad XRef entry` on PDF | Prefer `.docx` / `.md`; bad PDFs are skipped with a warning |
|
|
406
|
+
| First chat slow | Call `warmRagStore()` at startup |
|
|
407
|
+
| Re-index after changing files | `resetRagStore()` then `getRagStore()` (or restart) |
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Quick start (shortest)
|
|
29
412
|
|
|
30
413
|
```ts
|
|
31
414
|
import { createAgentRuntime } from "llm-agent-runtime";
|
|
@@ -39,37 +422,20 @@ const greet = tool(async () => "ok", {
|
|
|
39
422
|
});
|
|
40
423
|
|
|
41
424
|
const runtime = await createAgentRuntime({
|
|
42
|
-
redisUrl: process.env.REDIS_URL,
|
|
43
|
-
// llm omitted → reads process.env (see Env vars below)
|
|
44
|
-
|
|
425
|
+
redisUrl: process.env.REDIS_URL,
|
|
45
426
|
intents: [
|
|
46
427
|
{
|
|
47
428
|
name: "greetings",
|
|
48
|
-
examples: ["hi", "hello"
|
|
49
|
-
description: "hello
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "create_campaign",
|
|
53
|
-
examples: ["create a campaign", "start fundraising"],
|
|
54
|
-
sticky: true,
|
|
55
|
-
description: "new campaign wizard",
|
|
429
|
+
examples: ["hi", "hello"],
|
|
430
|
+
description: "hello",
|
|
56
431
|
},
|
|
57
432
|
],
|
|
58
|
-
|
|
59
433
|
intentRegistry: {
|
|
60
434
|
greetings: {
|
|
61
435
|
prompt: (ctx) =>
|
|
62
|
-
`Greet
|
|
436
|
+
`Greet ${ctx.context?.contactName ?? "friend"}.`,
|
|
63
437
|
tools: () => [greet],
|
|
64
438
|
},
|
|
65
|
-
create_campaign: {
|
|
66
|
-
prompt: () => "Help create a campaign step by step. Call create_campaign when ready.",
|
|
67
|
-
tools: (phoneKey) => [/* your tools scoped by phoneKey */],
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
toolOutputMap: {
|
|
72
|
-
create_campaign: "campaign",
|
|
73
439
|
},
|
|
74
440
|
});
|
|
75
441
|
|
|
@@ -81,7 +447,7 @@ const result = await runtime.invoke({
|
|
|
81
447
|
context: { contactName: "Ada" },
|
|
82
448
|
});
|
|
83
449
|
|
|
84
|
-
console.log(result.answer, result.intent
|
|
450
|
+
console.log(result.answer, result.intent);
|
|
85
451
|
```
|
|
86
452
|
|
|
87
453
|
---
|
|
@@ -91,178 +457,121 @@ console.log(result.answer, result.intent, result.flow, result.toolOutputs);
|
|
|
91
457
|
| You configure | Library handles |
|
|
92
458
|
| --- | --- |
|
|
93
459
|
| `intents` (examples + `sticky` + `regex`) | Hybrid regex + LLM + embedding router |
|
|
94
|
-
| `intentRegistry` (prompt + tools) | LangGraph agent +
|
|
95
|
-
| `toolOutputMap` |
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
460
|
+
| `intentRegistry` (prompt + tools) | LangGraph agent + checkpointer |
|
|
461
|
+
| `toolOutputMap` / `completeOn` | Tool output parse + sticky-flow end |
|
|
462
|
+
| `menuMap` | Digit → intent |
|
|
463
|
+
| RAG retrieve + inject via `context` | App calls `getRagStore()` / `store.retrieve` |
|
|
98
464
|
| Prompts, API tools, webhooks | — stay in your app |
|
|
99
465
|
|
|
100
466
|
---
|
|
101
467
|
|
|
102
468
|
## Config reference
|
|
103
469
|
|
|
104
|
-
###
|
|
105
|
-
|
|
106
|
-
#### Required
|
|
470
|
+
### Required
|
|
107
471
|
|
|
108
472
|
```ts
|
|
109
473
|
intents: IntentDefinition[]
|
|
110
474
|
intentRegistry: Record<string, { prompt: (ctx) => string; tools: (phoneKey) => Tool[] }>
|
|
111
475
|
```
|
|
112
476
|
|
|
113
|
-
|
|
477
|
+
### Common optional
|
|
114
478
|
|
|
115
479
|
| Option | Default | Purpose |
|
|
116
480
|
| --- | --- | --- |
|
|
117
|
-
| `redisUrl` |
|
|
118
|
-
| `llm` | `llmConfigFromEnv()` | Provider / models |
|
|
481
|
+
| `redisUrl` | MemorySaver | Checkpointer + flow state + optional answer cache |
|
|
482
|
+
| `llm` | `llmConfigFromEnv()` | Provider / models / embeddings |
|
|
119
483
|
| `cache.enabled` | `false` | Exact-match answer cache (needs Redis) |
|
|
120
484
|
| `toolOutputMap` | raw tool args | Map tool name → output key |
|
|
121
|
-
| `completeOn` | keys of `toolOutputMap` |
|
|
122
|
-
| `menuMap` | `{}` | `{ "1": "
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `resolvePhoneKey` | `phoneKey ?? threadId` | Key passed to `tools(phoneKey)` |
|
|
127
|
-
| `tracing` | auto-detect | LangSmith tracing config |
|
|
485
|
+
| `completeOn` | keys of `toolOutputMap` | Tools that finish a sticky flow |
|
|
486
|
+
| `menuMap` | `{}` | `{ "1": "recharge" }` |
|
|
487
|
+
| `buildSystemPrompt` | registry `prompt` (+ `sharedPrompt`) | One builder (use this for RAG) |
|
|
488
|
+
| `sharedPrompt` | none | Prepended to **every** intent prompt (emojis, tone, …) |
|
|
489
|
+
| `tracing` | auto if `LANGSMITH_API_KEY` | LangSmith |
|
|
128
490
|
|
|
129
491
|
---
|
|
130
492
|
|
|
131
493
|
## Intents
|
|
132
494
|
|
|
133
|
-
```ts
|
|
134
|
-
intents: [
|
|
135
|
-
{
|
|
136
|
-
name: "greetings",
|
|
137
|
-
examples: ["hi", "hello"],
|
|
138
|
-
description: "Shown to the LLM router",
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "create_campaign",
|
|
142
|
-
examples: ["create a campaign"],
|
|
143
|
-
sticky: true, // keep this flow across turns until completeOn fires
|
|
144
|
-
regex: /create.*(campaign|fundrais)/i, // fast-path: skip LLM if matched
|
|
145
|
-
},
|
|
146
|
-
]
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
- **examples** — embedding routing
|
|
150
|
-
- **sticky** — multi-step wizards
|
|
151
|
-
- **description** — LLM intent router
|
|
152
|
-
- **regex** — fast-path: if message matches, skip LLM/embeddings entirely (zero tokens)
|
|
153
|
-
|
|
154
|
-
### Intent detection flow
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
1. explicitIntent passed? → Use it directly (score 1.0)
|
|
158
|
-
2. Wizard input + sticky? → Reuse cached intent (skip LLM)
|
|
159
|
-
3. Menu digit match? → Use mapped intent (score 1.0)
|
|
160
|
-
4. Regex matches? → Use matched intent (score 1.0, 0 tokens)
|
|
161
|
-
5. Embeddings + LLM (hybrid) → Existing flow
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
The regex fast-path is a token-saving optimization. When a user message matches an intent's regex pattern, the library returns that intent immediately without calling the LLM or embeddings. Define regexes for high-confidence, pattern-based intents (e.g. "generate an image of...") and leave regex undefined for ambiguous intents that need LLM classification.
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## Prompts & tools (`intentRegistry`)
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
intentRegistry: {
|
|
172
|
-
greetings: {
|
|
173
|
-
prompt: (ctx) => string,
|
|
174
|
-
tools: (phoneKey) => DynamicStructuredTool[],
|
|
175
|
-
},
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
`prompt` receives:
|
|
180
|
-
|
|
181
495
|
```ts
|
|
182
496
|
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
497
|
+
name: "recharge",
|
|
498
|
+
examples: ["buy airtime"], // embedding routing
|
|
499
|
+
sticky: true, // multi-step wizard
|
|
500
|
+
description: "buy airtime", // LLM router
|
|
501
|
+
regex: /buy.*airtime/i, // fast-path, 0 tokens
|
|
188
502
|
}
|
|
189
503
|
```
|
|
190
504
|
|
|
191
|
-
|
|
505
|
+
Detection order: `explicitIntent` → sticky wizard skip → `menuMap` → `regex` → embeddings + LLM.
|
|
192
506
|
|
|
193
|
-
|
|
194
|
-
buildSystemPrompt: (ctx) => combineGeneralAndIntent(ctx)
|
|
195
|
-
```
|
|
507
|
+
**Fallback:** `defaultIntent` (optional) — defaults to the first intent in your list, else `"greetings"`.
|
|
196
508
|
|
|
197
|
-
|
|
509
|
+
### Optional general intent (Xel-style catch-all)
|
|
198
510
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
Replaces hand-written extractors:
|
|
511
|
+
Not required. If you want a first catch-all (hello / help / unclear → general chat):
|
|
202
512
|
|
|
203
513
|
```ts
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
propose_tickets: { as: "tickets", from: "tickets", initial: [] },
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Result lands on `result.toolOutputs` and completed keys on `result.extras`.
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
## Flow completion (`completeOn`)
|
|
514
|
+
import {
|
|
515
|
+
createAgentRuntime,
|
|
516
|
+
withGeneralIntent,
|
|
517
|
+
} from "llm-agent-runtime";
|
|
217
518
|
|
|
218
|
-
|
|
519
|
+
const { intents, intentRegistry } = withGeneralIntent(
|
|
520
|
+
[{ name: "recharge", examples: ["buy airtime"], sticky: true }],
|
|
521
|
+
{
|
|
522
|
+
recharge: {
|
|
523
|
+
prompt: () => "Collect amount and phone…",
|
|
524
|
+
tools: () => [buyAirtime],
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
// Optional: match Xel naming
|
|
528
|
+
// { intent: { name: "greetings" } },
|
|
529
|
+
);
|
|
219
530
|
|
|
220
|
-
|
|
221
|
-
|
|
531
|
+
const runtime = await createAgentRuntime({
|
|
532
|
+
intents,
|
|
533
|
+
intentRegistry,
|
|
534
|
+
defaultIntent: intents[0]!.name, // "general" (or "greetings")
|
|
535
|
+
});
|
|
222
536
|
```
|
|
223
537
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
This is tuned to Xel-style wizards: when a sticky flow is already active and the user message looks like wizard input (digits / yes-no), the router LLM call is skipped for that turn to reduce tokens.
|
|
227
|
-
|
|
228
|
-
---
|
|
538
|
+
Helpers: `createGeneralIntent`, `createGeneralIntentBinding`, `GENERAL_INTENT_NAME`.
|
|
539
|
+
If you already define `greetings` / `general` yourself, skip these — `withGeneralIntent` is a no-op when that name exists.
|
|
229
540
|
|
|
230
|
-
|
|
541
|
+
### Shared prompt (rules for every intent)
|
|
231
542
|
|
|
232
|
-
|
|
543
|
+
`withGeneralIntent` is only a **catch-all routing intent**. It does **not** inject style rules into recharge / other flows.
|
|
233
544
|
|
|
234
|
-
|
|
545
|
+
For instructions that should apply to **all** intents (emojis, tone, language), use optional `sharedPrompt` — the package prepends it to whichever intent prompt is active:
|
|
235
546
|
|
|
236
547
|
```ts
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
548
|
+
const runtime = await createAgentRuntime({
|
|
549
|
+
intents,
|
|
550
|
+
intentRegistry,
|
|
551
|
+
sharedPrompt:
|
|
552
|
+
"Always use friendly emojis. Keep replies concise and warm.",
|
|
553
|
+
// or: sharedPrompt: (ctx) => `User: ${ctx.context?.userInfo?.name ?? "friend"}`,
|
|
554
|
+
});
|
|
242
555
|
```
|
|
243
556
|
|
|
557
|
+
You do **not** need to repeat those rules in every `intentRegistry.*.prompt`.
|
|
558
|
+
|
|
244
559
|
---
|
|
245
560
|
|
|
246
|
-
## Invoke
|
|
561
|
+
## Invoke & result
|
|
247
562
|
|
|
248
563
|
```ts
|
|
249
|
-
|
|
564
|
+
await runtime.invoke({
|
|
250
565
|
message: "hi",
|
|
251
|
-
threadId: "whatsapp:234...",
|
|
252
|
-
phoneKey: "234...",
|
|
253
|
-
explicitIntent: "greetings",
|
|
254
|
-
context: {
|
|
255
|
-
contactName: "Ada",
|
|
256
|
-
userInfo: { email: "ada@example.com" },
|
|
257
|
-
},
|
|
566
|
+
threadId: "whatsapp:234...",
|
|
567
|
+
phoneKey: "234...",
|
|
568
|
+
explicitIntent: "greetings", // optional
|
|
569
|
+
context: { contactName: "Ada", ragContext: "..." },
|
|
258
570
|
});
|
|
259
571
|
```
|
|
260
572
|
|
|
261
|
-
**Result**
|
|
262
|
-
|
|
263
573
|
```ts
|
|
264
574
|
{
|
|
265
|
-
messages: unknown[]
|
|
266
575
|
answer?: string
|
|
267
576
|
intent: { intent: string, score: number }
|
|
268
577
|
flow: { active: boolean, intent: string | null }
|
|
@@ -270,167 +579,85 @@ const result = await runtime.invoke({
|
|
|
270
579
|
flowCompleted: boolean
|
|
271
580
|
extras?: Record<string, unknown>
|
|
272
581
|
router?: { action: "continue" | "switch" | "cancel", reason?: string }
|
|
582
|
+
usage?: {
|
|
583
|
+
turn: { inputTokens, outputTokens, totalTokens } // this invoke
|
|
584
|
+
flow: { …, intent } // sticky flow bucket
|
|
585
|
+
total: { inputTokens, outputTokens, totalTokens } // whole thread
|
|
586
|
+
}
|
|
273
587
|
}
|
|
274
588
|
```
|
|
275
589
|
|
|
276
|
-
|
|
590
|
+
Token totals are stored in **Redis** when `redisUrl` is set (`usage:thread:{id}`, `usage:flow:{id}:{intent}`), otherwise in memory.
|
|
277
591
|
|
|
278
|
-
|
|
592
|
+
### Clear session (frontend)
|
|
279
593
|
|
|
280
594
|
```ts
|
|
281
|
-
|
|
282
|
-
await runtime.
|
|
283
|
-
|
|
284
|
-
await runtime.
|
|
595
|
+
// Clears checkpoint history + sticky flow + token usage for this threadId
|
|
596
|
+
await runtime.clearSession(threadId);
|
|
597
|
+
// aliases: clearThread(threadId) | clearFlow(threadId) only sticky state
|
|
598
|
+
const total = await runtime.getUsage(threadId);
|
|
285
599
|
```
|
|
286
600
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
## LLM Providers
|
|
601
|
+
Example HTTP for the UI “New chat / Clear session” button:
|
|
290
602
|
|
|
291
|
-
|
|
603
|
+
```ts
|
|
604
|
+
// DELETE /session body: { sessionId }
|
|
605
|
+
await runtime.clearSession(req.body.sessionId);
|
|
606
|
+
res.json({ ok: true });
|
|
607
|
+
```
|
|
292
608
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
609
|
+
```ts
|
|
610
|
+
await runtime.connect()
|
|
611
|
+
await runtime.clearFlow(threadId)
|
|
612
|
+
await runtime.clearSession(threadId)
|
|
613
|
+
await runtime.disconnect()
|
|
614
|
+
```
|
|
298
615
|
|
|
299
|
-
|
|
616
|
+
---
|
|
300
617
|
|
|
301
|
-
|
|
302
|
-
| --- | --- |
|
|
303
|
-
| `LLM_PROVIDER` | `ollama` (default), `openai`, or `openrouter` |
|
|
304
|
-
| `EMBEDDINGS_PROVIDER` | defaults to `LLM_PROVIDER` |
|
|
305
|
-
| `OLLAMA_MODEL` | default `qwen2.5:7b` |
|
|
306
|
-
| `OLLAMA_BASE_URL` | e.g. `http://localhost:11434` |
|
|
307
|
-
| `OLLAMA_EMBEDDINGS_MODEL` | default `mxbai-embed-large:latest` |
|
|
308
|
-
| `INTENT_ROUTER_MODEL` | optional smaller router model (ollama) |
|
|
309
|
-
| `OPENAI_API_KEY` | required for openai |
|
|
310
|
-
| `OPENAI_MODEL` | default `gpt-4o-mini` |
|
|
311
|
-
| `OPENAI_EMBEDDINGS_MODEL` | default `text-embedding-3-small` |
|
|
312
|
-
| `OPENROUTER_API_KEY` | required for openrouter |
|
|
313
|
-
| `OPENROUTER_MODEL` | default `meta-llama/llama-4-scout` |
|
|
314
|
-
| `OPENROUTER_BASE_URL` | default `https://openrouter.ai/api/v1` |
|
|
315
|
-
| `OPENROUTER_INTENT_ROUTER_MODEL` | optional separate router model |
|
|
316
|
-
| `REDIS_URL` | your app usually passes this as `redisUrl` |
|
|
317
|
-
| `ANSWER_CACHE` | app convention: `"1"` to enable cache |
|
|
318
|
-
|
|
319
|
-
### OpenRouter example
|
|
618
|
+
## LLM & embeddings
|
|
320
619
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
620
|
+
| Chat `LLM_PROVIDER` | Key | Default model |
|
|
621
|
+
| --- | --- | --- |
|
|
622
|
+
| `ollama` | — | `qwen2.5:7b` |
|
|
623
|
+
| `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
|
624
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `meta-llama/llama-4-scout` |
|
|
324
625
|
|
|
325
|
-
|
|
626
|
+
| `EMBEDDINGS_PROVIDER` | Default model |
|
|
627
|
+
| --- | --- |
|
|
628
|
+
| `xenova` (default for openrouter/openai when unset) | `Xenova/all-MiniLM-L6-v2` |
|
|
629
|
+
| `ollama` (default for ollama chat when unset) | `mxbai-embed-large:latest` |
|
|
630
|
+
| `openai` | `text-embedding-3-small` |
|
|
326
631
|
|
|
327
632
|
```ts
|
|
328
633
|
createAgentRuntime({
|
|
329
634
|
llm: {
|
|
330
635
|
provider: "openrouter",
|
|
331
|
-
model: "meta-llama/llama-
|
|
332
|
-
apiKey:
|
|
636
|
+
model: "meta-llama/llama-3.1-70b-instruct",
|
|
637
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
638
|
+
embeddingsProvider: "xenova",
|
|
639
|
+
embeddingsModel: "Xenova/all-MiniLM-L6-v2",
|
|
333
640
|
},
|
|
334
641
|
// ...
|
|
335
642
|
});
|
|
336
643
|
```
|
|
337
644
|
|
|
338
|
-
### LangSmith
|
|
339
|
-
|
|
340
|
-
Tracing auto-enables when `LANGSMITH_API_KEY` is set in the environment. No code changes needed.
|
|
645
|
+
### LangSmith
|
|
341
646
|
|
|
342
647
|
```bash
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
Or enable explicitly:
|
|
347
|
-
|
|
348
|
-
```ts
|
|
349
|
-
createAgentRuntime({
|
|
350
|
-
tracing: { enabled: true, projectName: "my-project" },
|
|
351
|
-
// ...
|
|
352
|
-
});
|
|
648
|
+
LANGSMITH_TRACING=true
|
|
649
|
+
LANGSMITH_API_KEY=lsv2_pt_...
|
|
650
|
+
LANGSMITH_PROJECT=my-agent
|
|
353
651
|
```
|
|
354
652
|
|
|
355
|
-
|
|
653
|
+
Or `tracing: { enabled: true, projectName: "my-agent" }`.
|
|
356
654
|
|
|
357
655
|
---
|
|
358
656
|
|
|
359
|
-
##
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
your-app/
|
|
363
|
-
agent/
|
|
364
|
-
runtime.ts # createAgentRuntime({ ... })
|
|
365
|
-
intents.ts # intents + menuMap + aliases
|
|
366
|
-
prompts/ # prompt strings
|
|
367
|
-
tools/ # LangChain tools / API clients
|
|
368
|
-
controllers/ # HTTP / WhatsApp — call runtime.invoke()
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
Keep channel adapters and product APIs **out** of this package.
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## Full example (Xel-like)
|
|
376
|
-
|
|
377
|
-
```ts
|
|
378
|
-
import { createAgentRuntime } from "llm-agent-runtime";
|
|
379
|
-
|
|
380
|
-
const runtime = await createAgentRuntime({
|
|
381
|
-
redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
|
|
382
|
-
cache: { enabled: process.env.ANSWER_CACHE === "1" },
|
|
383
|
-
|
|
384
|
-
intents: [
|
|
385
|
-
{ name: "greetings", examples: ["hi", "hello"] },
|
|
386
|
-
{
|
|
387
|
-
name: "create_campaign",
|
|
388
|
-
examples: ["create a campaign"],
|
|
389
|
-
sticky: true,
|
|
390
|
-
regex: /create.*(campaign|fundrais)/i,
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
name: "buy_airtime_and_data",
|
|
394
|
-
examples: ["buy airtime", "vend airtime"],
|
|
395
|
-
sticky: true,
|
|
396
|
-
regex: /(buy|vend|purchase).*(airtime|data|credit)/i,
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
|
|
400
|
-
menuMap: {
|
|
401
|
-
"1": "create_campaign",
|
|
402
|
-
"4": "buy_airtime_and_data",
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
intentAliases: {
|
|
406
|
-
buy_airtime: "buy_airtime_and_data",
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
intentRegistry: {
|
|
410
|
-
greetings: {
|
|
411
|
-
prompt: (ctx) => `Greet ${ctx.context?.contactName ?? "there"}. Show menu 1–7.`,
|
|
412
|
-
tools: () => [],
|
|
413
|
-
},
|
|
414
|
-
create_campaign: {
|
|
415
|
-
prompt: () => "Walk through campaign creation.",
|
|
416
|
-
tools: (phoneKey) => createCampaignTools(phoneKey),
|
|
417
|
-
},
|
|
418
|
-
buy_airtime_and_data: {
|
|
419
|
-
prompt: () => "Help buy airtime/data.",
|
|
420
|
-
tools: (phoneKey) => airtimeTools(phoneKey),
|
|
421
|
-
},
|
|
422
|
-
},
|
|
657
|
+
## Redis note
|
|
423
658
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
buy_airtime: "buyAirtime",
|
|
427
|
-
add_airtime: "addAirtime",
|
|
428
|
-
},
|
|
429
|
-
completeOn: ["create_campaign", "buy_airtime", "add_airtime"],
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
await runtime.connect();
|
|
433
|
-
```
|
|
659
|
+
- **Flow cache / answer cache / token usage:** plain Redis is fine.
|
|
660
|
+
- **Durable LangGraph checkpoints:** need **Redis Stack** (RedisJSON). Without it the package falls back to an in-memory checkpointer and logs a warning.
|
|
434
661
|
|
|
435
662
|
---
|
|
436
663
|
|
|
@@ -440,9 +667,27 @@ await runtime.connect();
|
|
|
440
667
|
import {
|
|
441
668
|
createAgentRuntime,
|
|
442
669
|
llmConfigFromEnv,
|
|
670
|
+
resolveEmbeddingsProvider,
|
|
671
|
+
XenovaEmbeddings,
|
|
672
|
+
// Optional catch-all intent
|
|
673
|
+
withGeneralIntent,
|
|
674
|
+
withGeneralIntents,
|
|
675
|
+
defineIntents,
|
|
676
|
+
createGeneralIntent,
|
|
677
|
+
createGeneralIntentBinding,
|
|
678
|
+
// RAG (path + URL)
|
|
679
|
+
getRagStore,
|
|
680
|
+
getRagDocumentCount,
|
|
681
|
+
resetRagStore,
|
|
682
|
+
ragSourcesFromEnv,
|
|
683
|
+
createSimpleRagFromEnv,
|
|
684
|
+
createSimpleRagFromSources,
|
|
443
685
|
getLastMessageContent,
|
|
444
|
-
// advanced (usually not needed)
|
|
445
|
-
createToolOutputExtractor,
|
|
446
|
-
toolOutputMapToExtractors,
|
|
447
686
|
} from "llm-agent-runtime";
|
|
448
687
|
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Version
|
|
692
|
+
|
|
693
|
+
Current local package: **0.1.16** (`defineIntents` / `withGeneralIntents` unified intent objects, `getRagStore` singleton, Example B aligned with `plain-agent-demo`, Xenova embeddings, token usage + `clearSession`).
|