llm-agent-runtime 0.1.5 → 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 +530 -234
- 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 +112 -14
- 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 -7
- package/dist/env-llm.d.ts.map +1 -1
- package/dist/env-llm.js +67 -13
- 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 +60 -13
- 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/flow-router.d.ts +1 -0
- package/dist/services/flow-router.d.ts.map +1 -1
- package/dist/services/flow-router.js +12 -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 +50 -16
- 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
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `@xenova/transformers` fails on `sharp` (pnpm ignored build scripts):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd node_modules/sharp && npm run install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
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
|
+
};
|
|
17
369
|
```
|
|
18
370
|
|
|
19
|
-
|
|
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
|
|
20
394
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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) |
|
|
25
402
|
|
|
26
403
|
---
|
|
27
404
|
|
|
28
|
-
## Quick start
|
|
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
|
---
|
|
@@ -90,164 +450,122 @@ console.log(result.answer, result.intent, result.flow, result.toolOutputs);
|
|
|
90
450
|
|
|
91
451
|
| You configure | Library handles |
|
|
92
452
|
| --- | --- |
|
|
93
|
-
| `intents` (examples + `sticky`) | Hybrid LLM + embedding router |
|
|
94
|
-
| `intentRegistry` (prompt + tools) | LangGraph agent +
|
|
95
|
-
| `toolOutputMap` |
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
453
|
+
| `intents` (examples + `sticky` + `regex`) | Hybrid regex + LLM + embedding router |
|
|
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)` |
|
|
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 |
|
|
127
484
|
|
|
128
485
|
---
|
|
129
486
|
|
|
130
487
|
## Intents
|
|
131
488
|
|
|
132
|
-
```ts
|
|
133
|
-
intents: [
|
|
134
|
-
{
|
|
135
|
-
name: "greetings",
|
|
136
|
-
examples: ["hi", "hello"],
|
|
137
|
-
description: "Shown to the LLM router",
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: "create_campaign",
|
|
141
|
-
examples: ["create a campaign"],
|
|
142
|
-
sticky: true, // keep this flow across turns until completeOn fires
|
|
143
|
-
},
|
|
144
|
-
]
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
- **examples** — embedding routing
|
|
148
|
-
- **sticky** — multi-step wizards
|
|
149
|
-
- **description** — LLM intent router
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Prompts & tools (`intentRegistry`)
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
intentRegistry: {
|
|
157
|
-
greetings: {
|
|
158
|
-
prompt: (ctx) => string,
|
|
159
|
-
tools: (phoneKey) => DynamicStructuredTool[],
|
|
160
|
-
},
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`prompt` receives:
|
|
165
|
-
|
|
166
489
|
```ts
|
|
167
490
|
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
173
496
|
}
|
|
174
497
|
```
|
|
175
498
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
buildSystemPrompt: (ctx) => combineGeneralAndIntent(ctx)
|
|
180
|
-
```
|
|
499
|
+
Detection order: `explicitIntent` → sticky wizard skip → `menuMap` → `regex` → embeddings + LLM.
|
|
181
500
|
|
|
182
|
-
|
|
501
|
+
**Fallback:** `defaultIntent` (optional) — defaults to the first intent in your list, else `"greetings"`.
|
|
183
502
|
|
|
184
|
-
|
|
503
|
+
### Optional general intent (Xel-style catch-all)
|
|
185
504
|
|
|
186
|
-
|
|
505
|
+
Not required. If you want a first catch-all (hello / help / unclear → general chat):
|
|
187
506
|
|
|
188
507
|
```ts
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
propose_tickets: { as: "tickets", from: "tickets", initial: [] },
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Result lands on `result.toolOutputs` and completed keys on `result.extras`.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## Flow completion (`completeOn`)
|
|
508
|
+
import {
|
|
509
|
+
createAgentRuntime,
|
|
510
|
+
withGeneralIntent,
|
|
511
|
+
} from "llm-agent-runtime";
|
|
202
512
|
|
|
203
|
-
|
|
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
|
+
);
|
|
204
524
|
|
|
205
|
-
|
|
206
|
-
|
|
525
|
+
const runtime = await createAgentRuntime({
|
|
526
|
+
intents,
|
|
527
|
+
intentRegistry,
|
|
528
|
+
defaultIntent: intents[0]!.name, // "general" (or "greetings")
|
|
529
|
+
});
|
|
207
530
|
```
|
|
208
531
|
|
|
209
|
-
|
|
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.
|
|
210
534
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
---
|
|
535
|
+
### Shared prompt (rules for every intent)
|
|
214
536
|
|
|
215
|
-
|
|
537
|
+
`withGeneralIntent` is only a **catch-all routing intent**. It does **not** inject style rules into recharge / other flows.
|
|
216
538
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
The **prompt** shows the menu text. **`menuMap`** tells the **router** that `"1"` means `create_campaign`. Without it, a bare `"1"` is not reliably routed.
|
|
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:
|
|
220
540
|
|
|
221
541
|
```ts
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
});
|
|
227
549
|
```
|
|
228
550
|
|
|
551
|
+
You do **not** need to repeat those rules in every `intentRegistry.*.prompt`.
|
|
552
|
+
|
|
229
553
|
---
|
|
230
554
|
|
|
231
|
-
## Invoke
|
|
555
|
+
## Invoke & result
|
|
232
556
|
|
|
233
557
|
```ts
|
|
234
|
-
|
|
558
|
+
await runtime.invoke({
|
|
235
559
|
message: "hi",
|
|
236
|
-
threadId: "whatsapp:234...",
|
|
237
|
-
phoneKey: "234...",
|
|
238
|
-
explicitIntent: "greetings",
|
|
239
|
-
context: {
|
|
240
|
-
contactName: "Ada",
|
|
241
|
-
userInfo: { email: "ada@example.com" },
|
|
242
|
-
},
|
|
560
|
+
threadId: "whatsapp:234...",
|
|
561
|
+
phoneKey: "234...",
|
|
562
|
+
explicitIntent: "greetings", // optional
|
|
563
|
+
context: { contactName: "Ada", ragContext: "..." },
|
|
243
564
|
});
|
|
244
565
|
```
|
|
245
566
|
|
|
246
|
-
**Result**
|
|
247
|
-
|
|
248
567
|
```ts
|
|
249
568
|
{
|
|
250
|
-
messages: unknown[]
|
|
251
569
|
answer?: string
|
|
252
570
|
intent: { intent: string, score: number }
|
|
253
571
|
flow: { active: boolean, intent: string | null }
|
|
@@ -255,125 +573,85 @@ const result = await runtime.invoke({
|
|
|
255
573
|
flowCompleted: boolean
|
|
256
574
|
extras?: Record<string, unknown>
|
|
257
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
|
+
}
|
|
258
581
|
}
|
|
259
582
|
```
|
|
260
583
|
|
|
261
|
-
|
|
584
|
+
Token totals are stored in **Redis** when `redisUrl` is set (`usage:thread:{id}`, `usage:flow:{id}:{intent}`), otherwise in memory.
|
|
262
585
|
|
|
263
|
-
|
|
586
|
+
### Clear session (frontend)
|
|
264
587
|
|
|
265
588
|
```ts
|
|
266
|
-
|
|
267
|
-
await runtime.
|
|
268
|
-
|
|
269
|
-
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);
|
|
270
593
|
```
|
|
271
594
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
## Env vars (when `llm` is omitted)
|
|
275
|
-
|
|
276
|
-
| Variable | Notes |
|
|
277
|
-
| --- | --- |
|
|
278
|
-
| `LLM_PROVIDER` | `ollama` (default) or `openai` |
|
|
279
|
-
| `EMBEDDINGS_PROVIDER` | defaults to `LLM_PROVIDER` |
|
|
280
|
-
| `OLLAMA_MODEL` | default `qwen2.5:7b` |
|
|
281
|
-
| `OLLAMA_BASE_URL` | e.g. `http://localhost:11434` |
|
|
282
|
-
| `INTENT_ROUTER_MODEL` | optional smaller router model |
|
|
283
|
-
| `OLLAMA_EMBEDDINGS_MODEL` | default `mxbai-embed-large:latest` |
|
|
284
|
-
| `OPENAI_API_KEY` | required for openai |
|
|
285
|
-
| `OPENAI_MODEL` | default `gpt-4o-mini` |
|
|
286
|
-
| `OPENAI_EMBEDDINGS_MODEL` | default `text-embedding-3-small` |
|
|
287
|
-
| `REDIS_URL` | your app usually passes this as `redisUrl` |
|
|
288
|
-
| `ANSWER_CACHE` | app convention: `"1"` to enable cache |
|
|
289
|
-
|
|
290
|
-
Or pass LLM explicitly:
|
|
595
|
+
Example HTTP for the UI “New chat / Clear session” button:
|
|
291
596
|
|
|
292
597
|
```ts
|
|
293
|
-
|
|
598
|
+
// DELETE /session body: { sessionId }
|
|
599
|
+
await runtime.clearSession(req.body.sessionId);
|
|
600
|
+
res.json({ ok: true });
|
|
601
|
+
```
|
|
294
602
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
603
|
+
```ts
|
|
604
|
+
await runtime.connect()
|
|
605
|
+
await runtime.clearFlow(threadId)
|
|
606
|
+
await runtime.clearSession(threadId)
|
|
607
|
+
await runtime.disconnect()
|
|
300
608
|
```
|
|
301
609
|
|
|
302
610
|
---
|
|
303
611
|
|
|
304
|
-
##
|
|
305
|
-
|
|
306
|
-
```
|
|
307
|
-
your-app/
|
|
308
|
-
agent/
|
|
309
|
-
runtime.ts # createAgentRuntime({ ... })
|
|
310
|
-
intents.ts # intents + menuMap + aliases
|
|
311
|
-
prompts/ # prompt strings
|
|
312
|
-
tools/ # LangChain tools / API clients
|
|
313
|
-
controllers/ # HTTP / WhatsApp — call runtime.invoke()
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Keep channel adapters and product APIs **out** of this package.
|
|
612
|
+
## LLM & embeddings
|
|
317
613
|
|
|
318
|
-
|
|
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` |
|
|
319
619
|
|
|
320
|
-
|
|
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` |
|
|
321
625
|
|
|
322
626
|
```ts
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
627
|
+
createAgentRuntime({
|
|
628
|
+
llm: {
|
|
629
|
+
provider: "openrouter",
|
|
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",
|
|
634
|
+
},
|
|
635
|
+
// ...
|
|
636
|
+
});
|
|
637
|
+
```
|
|
328
638
|
|
|
329
|
-
|
|
330
|
-
{ name: "greetings", examples: ["hi", "hello"] },
|
|
331
|
-
{
|
|
332
|
-
name: "create_campaign",
|
|
333
|
-
examples: ["create a campaign"],
|
|
334
|
-
sticky: true,
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
name: "buy_airtime_and_data",
|
|
338
|
-
examples: ["buy airtime", "vend airtime"],
|
|
339
|
-
sticky: true,
|
|
340
|
-
},
|
|
341
|
-
],
|
|
639
|
+
### LangSmith
|
|
342
640
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
641
|
+
```bash
|
|
642
|
+
LANGSMITH_TRACING=true
|
|
643
|
+
LANGSMITH_API_KEY=lsv2_pt_...
|
|
644
|
+
LANGSMITH_PROJECT=my-agent
|
|
645
|
+
```
|
|
347
646
|
|
|
348
|
-
|
|
349
|
-
buy_airtime: "buy_airtime_and_data",
|
|
350
|
-
},
|
|
647
|
+
Or `tracing: { enabled: true, projectName: "my-agent" }`.
|
|
351
648
|
|
|
352
|
-
|
|
353
|
-
greetings: {
|
|
354
|
-
prompt: (ctx) => `Greet ${ctx.context?.contactName ?? "there"}. Show menu 1–7.`,
|
|
355
|
-
tools: () => [],
|
|
356
|
-
},
|
|
357
|
-
create_campaign: {
|
|
358
|
-
prompt: () => "Walk through campaign creation.",
|
|
359
|
-
tools: (phoneKey) => createCampaignTools(phoneKey),
|
|
360
|
-
},
|
|
361
|
-
buy_airtime_and_data: {
|
|
362
|
-
prompt: () => "Help buy airtime/data.",
|
|
363
|
-
tools: (phoneKey) => airtimeTools(phoneKey),
|
|
364
|
-
},
|
|
365
|
-
},
|
|
649
|
+
---
|
|
366
650
|
|
|
367
|
-
|
|
368
|
-
create_campaign: "campaign",
|
|
369
|
-
buy_airtime: "buyAirtime",
|
|
370
|
-
add_airtime: "addAirtime",
|
|
371
|
-
},
|
|
372
|
-
completeOn: ["create_campaign", "buy_airtime", "add_airtime"],
|
|
373
|
-
});
|
|
651
|
+
## Redis note
|
|
374
652
|
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
|
377
655
|
|
|
378
656
|
---
|
|
379
657
|
|
|
@@ -383,9 +661,27 @@ await runtime.connect();
|
|
|
383
661
|
import {
|
|
384
662
|
createAgentRuntime,
|
|
385
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,
|
|
386
679
|
getLastMessageContent,
|
|
387
|
-
// advanced (usually not needed)
|
|
388
|
-
createToolOutputExtractor,
|
|
389
|
-
toolOutputMapToExtractors,
|
|
390
680
|
} from "llm-agent-runtime";
|
|
391
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`).
|