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