@botpress/adk 1.15.4 → 1.16.1

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.
Files changed (49) hide show
  1. package/dist/agent-init/agent-project-generator.d.ts +1 -1
  2. package/dist/agent-init/agent-project-generator.d.ts.map +1 -1
  3. package/dist/agent-project/config-writer.d.ts +10 -0
  4. package/dist/agent-project/config-writer.d.ts.map +1 -1
  5. package/dist/agent-project/index.d.ts +1 -0
  6. package/dist/agent-project/index.d.ts.map +1 -1
  7. package/dist/config/coerce-config-value.d.ts +18 -0
  8. package/dist/config/coerce-config-value.d.ts.map +1 -0
  9. package/dist/config/index.d.ts +1 -0
  10. package/dist/config/index.d.ts.map +1 -1
  11. package/dist/config/manager.d.ts +25 -0
  12. package/dist/config/manager.d.ts.map +1 -1
  13. package/dist/eval/client.d.ts +25 -0
  14. package/dist/eval/client.d.ts.map +1 -0
  15. package/dist/eval/graders/index.d.ts +12 -0
  16. package/dist/eval/graders/index.d.ts.map +1 -0
  17. package/dist/eval/graders/llm.d.ts +18 -0
  18. package/dist/eval/graders/llm.d.ts.map +1 -0
  19. package/dist/eval/graders/match.d.ts +11 -0
  20. package/dist/eval/graders/match.d.ts.map +1 -0
  21. package/dist/eval/graders/outcome.d.ts +16 -0
  22. package/dist/eval/graders/outcome.d.ts.map +1 -0
  23. package/dist/eval/graders/response.d.ts +9 -0
  24. package/dist/eval/graders/response.d.ts.map +1 -0
  25. package/dist/eval/graders/state.d.ts +16 -0
  26. package/dist/eval/graders/state.d.ts.map +1 -0
  27. package/dist/eval/graders/tables.d.ts +8 -0
  28. package/dist/eval/graders/tables.d.ts.map +1 -0
  29. package/dist/eval/graders/tools.d.ts +7 -0
  30. package/dist/eval/graders/tools.d.ts.map +1 -0
  31. package/dist/eval/graders/workflow.d.ts +7 -0
  32. package/dist/eval/graders/workflow.d.ts.map +1 -0
  33. package/dist/eval/index.d.ts +10 -0
  34. package/dist/eval/index.d.ts.map +1 -0
  35. package/dist/eval/loader.d.ts +10 -0
  36. package/dist/eval/loader.d.ts.map +1 -0
  37. package/dist/eval/runner.d.ts +15 -0
  38. package/dist/eval/runner.d.ts.map +1 -0
  39. package/dist/eval/store.d.ts +10 -0
  40. package/dist/eval/store.d.ts.map +1 -0
  41. package/dist/eval/traces.d.ts +19 -0
  42. package/dist/eval/traces.d.ts.map +1 -0
  43. package/dist/eval/types.d.ts +202 -0
  44. package/dist/eval/types.d.ts.map +1 -0
  45. package/dist/index.d.ts +7 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1569 -1616
  48. package/dist/index.js.map +23 -8
  49. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -718,7 +718,7 @@ var PRETTIER_CONFIG, formatCode = async (code, filepath) => {
718
718
  `));
719
719
  return code;
720
720
  }
721
- }, ADK_VERSION = "1.15.4", relative2 = (from, to) => {
721
+ }, ADK_VERSION = "1.16.1", relative2 = (from, to) => {
722
722
  const fromDir = path10.dirname(from);
723
723
  const relative3 = path10.relative(fromDir, to);
724
724
  return relative3.startsWith(".") ? relative3 : `./${relative3}`;
@@ -861,7 +861,7 @@ var init_integration_action_types = __esm(() => {
861
861
  var require_package = __commonJS((exports, module) => {
862
862
  module.exports = {
863
863
  name: "@botpress/adk",
864
- version: "1.15.4",
864
+ version: "1.16.1",
865
865
  description: "Core ADK library for building AI agents on Botpress",
866
866
  type: "module",
867
867
  main: "dist/index.js",
@@ -905,10 +905,11 @@ var require_package = __commonJS((exports, module) => {
905
905
  url: "https://github.com/botpress/adk"
906
906
  },
907
907
  dependencies: {
908
+ "@botpress/chat": "^0.5.5",
908
909
  "@botpress/cli": "^5.2.0",
909
910
  "@botpress/client": "^1.35.0",
910
911
  "@botpress/cognitive": "^0.3.14",
911
- "@botpress/runtime": "^1.15.4",
912
+ "@botpress/runtime": "^1.16.1",
912
913
  "@botpress/sdk": "^5.4.3",
913
914
  "@bpinternal/jex": "^1.2.4",
914
915
  "@bpinternal/yargs-extra": "^0.0.21",
@@ -3280,6 +3281,62 @@ class ConfigWriter {
3280
3281
  }
3281
3282
  await this.saveConfig(sourceFile);
3282
3283
  }
3284
+ async updateConfiguration(updates) {
3285
+ const { sourceFile, configObject } = this.loadConfig();
3286
+ const hasAdds = updates.some((u) => u.action === "add");
3287
+ let configProp = configObject.getProperty("configuration");
3288
+ if (!configProp) {
3289
+ if (!hasAdds)
3290
+ return;
3291
+ configProp = configObject.addPropertyAssignment({
3292
+ name: "configuration",
3293
+ initializer: "{ schema: z.object({}) }"
3294
+ });
3295
+ }
3296
+ const configInit = configProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
3297
+ if (!configInit)
3298
+ return;
3299
+ let schemaProp = configInit.getProperty("schema");
3300
+ if (!schemaProp) {
3301
+ if (!hasAdds)
3302
+ return;
3303
+ schemaProp = configInit.addPropertyAssignment({
3304
+ name: "schema",
3305
+ initializer: "z.object({})"
3306
+ });
3307
+ }
3308
+ const schemaCall = schemaProp.getInitializerIfKind(SyntaxKind.CallExpression);
3309
+ if (!schemaCall)
3310
+ return;
3311
+ const schemaArg = schemaCall.getArguments()[0];
3312
+ if (!schemaArg || !schemaArg.isKind(SyntaxKind.ObjectLiteralExpression))
3313
+ return;
3314
+ const schemaObject = schemaArg;
3315
+ for (const update of updates) {
3316
+ const existing = schemaObject.getProperty(update.field);
3317
+ switch (update.action) {
3318
+ case "add":
3319
+ if (!existing && update.definition) {
3320
+ schemaObject.addPropertyAssignment({
3321
+ name: update.field,
3322
+ initializer: update.definition
3323
+ });
3324
+ }
3325
+ break;
3326
+ case "update":
3327
+ if (existing && update.definition) {
3328
+ existing.setInitializer(update.definition);
3329
+ }
3330
+ break;
3331
+ case "remove":
3332
+ if (existing) {
3333
+ existing.remove();
3334
+ }
3335
+ break;
3336
+ }
3337
+ }
3338
+ await this.saveConfig(sourceFile);
3339
+ }
3283
3340
  }
3284
3341
 
3285
3342
  // src/integrations/operations.ts
@@ -5112,6 +5169,45 @@ init_types();
5112
5169
  // src/config/manager.ts
5113
5170
  import { Client as Client12 } from "@botpress/client";
5114
5171
  import { sync as jex } from "@bpinternal/jex";
5172
+
5173
+ // src/config/coerce-config-value.ts
5174
+ function coerceConfigValue(value, fieldSchema) {
5175
+ const typeName = getInnerTypeName(fieldSchema);
5176
+ switch (typeName) {
5177
+ case "ZodNumber": {
5178
+ const num = Number(value);
5179
+ if (Number.isNaN(num)) {
5180
+ return value;
5181
+ }
5182
+ return num;
5183
+ }
5184
+ case "ZodBoolean": {
5185
+ const lower = value.toLowerCase();
5186
+ if (lower === "true" || lower === "1" || lower === "yes") {
5187
+ return true;
5188
+ }
5189
+ if (lower === "false" || lower === "0" || lower === "no") {
5190
+ return false;
5191
+ }
5192
+ return value;
5193
+ }
5194
+ default:
5195
+ return value;
5196
+ }
5197
+ }
5198
+ function getInnerTypeName(schema) {
5199
+ const def = schema?._def;
5200
+ if (!def) {
5201
+ return "unknown";
5202
+ }
5203
+ const typeName = def.typeName ?? "unknown";
5204
+ if ((typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") && def.innerType) {
5205
+ return getInnerTypeName(def.innerType);
5206
+ }
5207
+ return typeName;
5208
+ }
5209
+
5210
+ // src/config/manager.ts
5115
5211
  class ConfigManager {
5116
5212
  botId;
5117
5213
  client;
@@ -5201,1552 +5297,328 @@ class ConfigManager {
5201
5297
  const validation = await this.validate(schema);
5202
5298
  return validation.valid;
5203
5299
  }
5300
+ async describeSchema(schema) {
5301
+ const stored = await this.load();
5302
+ const shape = schema.shape;
5303
+ const fields = [];
5304
+ for (const [key, fieldSchema] of Object.entries(shape)) {
5305
+ const innerType = getInnerTypeName(fieldSchema);
5306
+ const def = fieldSchema?._def;
5307
+ let type = "unknown";
5308
+ if (innerType === "ZodString")
5309
+ type = "string";
5310
+ else if (innerType === "ZodNumber")
5311
+ type = "number";
5312
+ else if (innerType === "ZodBoolean")
5313
+ type = "boolean";
5314
+ const typeName = def?.typeName ?? "";
5315
+ const isOptional = typeName === "ZodOptional" || typeName === "ZodNullable";
5316
+ const hasDefault = typeName === "ZodDefault";
5317
+ const required = !isOptional && !hasDefault;
5318
+ let defaultValue = undefined;
5319
+ if (hasDefault) {
5320
+ defaultValue = def.defaultValue?.();
5321
+ }
5322
+ const description = fieldSchema.description ?? undefined;
5323
+ fields.push({
5324
+ key,
5325
+ type,
5326
+ required,
5327
+ description,
5328
+ defaultValue,
5329
+ currentValue: stored[key]
5330
+ });
5331
+ }
5332
+ return fields;
5333
+ }
5334
+ async setWithValidation(key, value, schema) {
5335
+ const shape = schema.shape;
5336
+ const fieldSchema = shape[key];
5337
+ if (!fieldSchema) {
5338
+ return { success: false, error: `Key "${key}" not found in configuration schema` };
5339
+ }
5340
+ const coerced = typeof value === "string" ? coerceConfigValue(value, fieldSchema) : value;
5341
+ const result = fieldSchema.safeParse(coerced);
5342
+ if (!result.success) {
5343
+ const messages = result.error.issues.map((i) => i.message);
5344
+ return { success: false, error: messages.join("; ") };
5345
+ }
5346
+ await this.set(key, result.data);
5347
+ return { success: true, data: result.data };
5348
+ }
5204
5349
  }
5205
5350
  // src/agent-init/agent-project-generator.ts
5206
5351
  init_utils();
5207
5352
  import * as fs11 from "fs";
5208
5353
  import * as path15 from "path";
5209
5354
 
5210
- // src/agent-init/CLAUDE.template.md
5211
- var CLAUDE_template_default = `# Botpress ADK Project Context
5355
+ // src/agent-init/ai-assistant-instructions.template.md
5356
+ var ai_assistant_instructions_template_default = `# Botpress ADK Agent
5212
5357
 
5213
- This project is built with the **Botpress Agent Development Kit (ADK)** - a TypeScript-first framework for building AI agents.
5358
+ > This project is built with the **Botpress Agent Development Kit (ADK)** a TypeScript-first framework for building AI agents.
5214
5359
 
5215
- ## Table of Contents
5360
+ ## Key Files
5216
5361
 
5217
- - [Quick Reference: Use the Botpress MCP Server](#quick-reference-use-the-botpress-mcp-server)
5218
- - [What is the ADK?](#what-is-the-adk)
5219
- - [ADK CLI](#adk-cli)
5220
- - [Core Concepts](#core-concepts)
5221
- - [1. Agent Configuration](#1-agent-configuration-agentconfigts)
5222
- - [2. Conversations](#2-conversations-srcconversations)
5223
- - [3. Workflows](#3-workflows-srcworkflows)
5224
- - [4. Tools](#4-tools-srctools)
5225
- - [5. Knowledge Bases](#5-knowledge-bases-srcknowledge)
5226
- - [6. Actions](#6-actions-srcactions)
5227
- - [7. Zai Library](#7-zai-library)
5228
- - [Project Structure](#project-structure)
5229
- - [Development Workflow](#development-workflow)
5230
- - [Examples](#examples)
5231
- - [Best Practices](#best-practices)
5232
- - [Common APIs](#common-apis)
5233
- - [Advanced Autonomous Execution](#advanced-autonomous-execution)
5234
- - [State and Metadata Management](#state-and-metadata-management)
5235
- - [Advanced Table Operations](#advanced-table-operations)
5236
- - [Knowledge Base Operations](#knowledge-base-operations)
5237
- - [Advanced Conversation Patterns](#advanced-conversation-patterns)
5238
- - [Citations System](#citations-system)
5239
- - [When Making Changes](#when-making-changes)
5240
- - [Resources](#resources)
5362
+ - \`agent.config.ts\` Agent configuration, models, state schemas, and dependencies
5363
+ - \`src/conversations/\` Message handlers (primary user interaction)
5364
+ - \`src/workflows/\` — Long-running background processes
5365
+ - \`src/tools/\` — AI-callable functions
5366
+ - \`src/actions/\` Reusable business logic
5367
+ - \`src/knowledge/\` — RAG knowledge base sources
5368
+ - \`src/tables/\` — Database table definitions
5369
+ - \`src/triggers/\` — Event-based triggers
5241
5370
 
5242
- ## Quick Reference: Use the Botpress MCP Server
5371
+ ## Development
5243
5372
 
5244
- **IMPORTANT**: When working on this project, always search the Botpress documentation using the \`mcp__botpress-docs__SearchBotpress\` tool before making changes. The ADK has specific patterns and APIs that are well-documented.
5373
+ \`\`\`bash
5374
+ adk dev # Start dev server with hot reload
5375
+ adk build # Build and generate types
5376
+ adk deploy # Deploy to Botpress Cloud
5377
+ adk chat # Chat with your agent in the terminal
5378
+ \`\`\`
5245
5379
 
5246
- ## What is the ADK?
5380
+ ## AI Coding Assistant Skills
5247
5381
 
5248
- The ADK allows developers to build Botpress agents using **code instead of the Studio interface**. It provides:
5382
+ This project uses the Botpress ADK. Before making changes, use the relevant skill:
5249
5383
 
5250
- - Project scaffolding with TypeScript
5251
- - Hot reloading development server (\`adk dev\`)
5252
- - Type-safe APIs and auto-generated types
5253
- - Build and deploy to Botpress Cloud
5384
+ | Skill | Use for |
5385
+ | ------------------ | ------------------------------------------------ |
5386
+ | \`/adk\` | ADK concepts, patterns, and API reference |
5387
+ | \`/adk-integration\` | Finding and using Botpress integrations |
5388
+ | \`/adk-debugger\` | Debugging with traces and test conversations |
5389
+ | \`/adk-frontend\` | Building frontends that connect to ADK bots |
5254
5390
 
5255
- ## ADK CLI
5391
+ If these skills are not installed, install them:
5256
5392
 
5257
- The ADK CLI is installed globally. You can run it using \`adk <command>\`.
5258
- Always use bash to run ADK. (\`Bash(adk)\`)
5259
- To install an integration: \`adk install <integration>\`
5260
- To generate types without running in dev mode: \`adk build\`
5393
+ \`\`\`
5394
+ npx skills add botpress/skills --skill adk
5395
+ \`\`\`
5261
5396
 
5262
- ## Core Concepts
5397
+ ## Project Overview
5263
5398
 
5264
- ### 1. Agent Configuration (\`agent.config.ts\`)
5399
+ <!-- Describe what your agent does -->
5265
5400
 
5266
- The main configuration file defines:
5401
+ ## Architecture & Conventions
5267
5402
 
5268
- - **Agent name and description**
5269
- - **Default models** for autonomous and zai operations
5270
- - **State schemas** (bot-level and user-level state using Zod)
5271
- - **Configuration variables** (encrypted, secure storage for API keys)
5272
- - **Integration dependencies** (webchat, chat, etc.)
5403
+ <!-- Add project-specific patterns, decisions, and conventions -->
5273
5404
 
5274
- \`\`\`typescript
5275
- export default defineConfig({
5276
- name: "my-agent",
5405
+ ## Notes
5406
+
5407
+ <!-- Add anything else relevant to your project -->
5408
+ `;
5409
+
5410
+ // src/agent-init/agent-project-generator.ts
5411
+ class AgentProjectGenerator {
5412
+ projectPath;
5413
+ projectName;
5414
+ packageManager;
5415
+ template;
5416
+ constructor(projectPath, packageManager = "bun", template = "blank") {
5417
+ this.projectPath = path15.resolve(projectPath);
5418
+ this.projectName = path15.basename(this.projectPath);
5419
+ this.packageManager = packageManager;
5420
+ this.template = template;
5421
+ }
5422
+ async generate() {
5423
+ this.ensureEmptyDirectory();
5424
+ this.createPackageJson();
5425
+ await this.createAgentConfig();
5426
+ this.createTsConfig();
5427
+ this.createAgentJson();
5428
+ this.createGitIgnore();
5429
+ await this.createReadme();
5430
+ this.createAIAssistantInstructions();
5431
+ await this.createSourceStructure();
5432
+ }
5433
+ ensureEmptyDirectory() {
5434
+ if (!fs11.existsSync(this.projectPath)) {
5435
+ fs11.mkdirSync(this.projectPath, { recursive: true });
5436
+ }
5437
+ const files = fs11.readdirSync(this.projectPath);
5438
+ if (files.length > 0) {
5439
+ throw new Error(`Directory ${this.projectPath} is not empty. Please use an empty directory.`);
5440
+ }
5441
+ }
5442
+ createPackageJson() {
5443
+ const packageJson = {
5444
+ name: this.projectName,
5445
+ version: "1.0.0",
5446
+ description: `A Botpress Agent built with the ADK`,
5447
+ type: "module",
5448
+ packageManager: this.packageManager === "npm" ? undefined : `${this.packageManager}@latest`,
5449
+ scripts: {
5450
+ dev: "adk dev",
5451
+ build: "adk build",
5452
+ deploy: "adk deploy"
5453
+ },
5454
+ dependencies: {
5455
+ "@botpress/runtime": `^${"1.16.1"}`
5456
+ },
5457
+ devDependencies: {
5458
+ typescript: "^5.9.3"
5459
+ }
5460
+ };
5461
+ if (packageJson.packageManager === undefined) {
5462
+ delete packageJson.packageManager;
5463
+ }
5464
+ this.writeJsonFile("package.json", packageJson);
5465
+ }
5466
+ getDefaultDependencies() {
5467
+ const dependencies = {
5468
+ integrations: {}
5469
+ };
5470
+ if (this.template === "hello-world") {
5471
+ dependencies.integrations = {
5472
+ chat: {
5473
+ version: "chat@latest",
5474
+ enabled: true
5475
+ },
5476
+ webchat: {
5477
+ version: "webchat@latest",
5478
+ enabled: true
5479
+ }
5480
+ };
5481
+ }
5482
+ return dependencies;
5483
+ }
5484
+ async createAgentConfig() {
5485
+ const dependencies = this.getDefaultDependencies();
5486
+ const integrationsJson = JSON.stringify(dependencies.integrations, null, 4).replace(/\n/g, `
5487
+ `);
5488
+ const defaultModels = this.template === "hello-world" ? `
5277
5489
  defaultModels: {
5278
5490
  autonomous: "cerebras:gpt-oss-120b",
5279
5491
  zai: "cerebras:gpt-oss-120b",
5280
5492
  },
5281
- bot: { state: z.object({}) },
5282
- user: { state: z.object({}) },
5283
- dependencies: {
5284
- integrations: {
5285
- webchat: { version: "webchat@0.3.0", enabled: true },
5286
- },
5287
- },
5288
- });
5289
- \`\`\`
5290
-
5291
- ### 2. Conversations (\`src/conversations/\`)
5292
-
5293
- **Primary way agents handle user messages**. Each conversation handler:
5294
-
5295
- - Responds to messages from specific channels
5296
- - Uses \`execute()\` to run autonomous AI logic
5297
- - Can access conversation state, send messages, and call tools
5298
-
5299
- **Key Pattern**: The \`execute()\` function runs the agent's AI loop:
5493
+ ` : "";
5494
+ const agentConfig = `import { z, defineConfig } from '@botpress/runtime';
5300
5495
 
5301
- \`\`\`typescript
5302
- export default new Conversation({
5303
- channel: "webchat.channel",
5304
- handler: async ({ execute, conversation, state }) => {
5305
- await execute({
5306
- instructions: "Your agent's instructions here",
5307
- tools: [myTool1, myTool2],
5308
- knowledge: [myKnowledgeBase],
5309
- });
5496
+ export default defineConfig({
5497
+ name: '${this.projectName}',
5498
+ description: 'An AI agent built with Botpress ADK',
5499
+ ${defaultModels}
5500
+ bot: {
5501
+ state: z.object({}),
5310
5502
  },
5311
- });
5312
- \`\`\`
5313
-
5314
- ### 3. Workflows (\`src/workflows/\`)
5315
-
5316
- **Long-running processes** for complex, multi-step operations:
5317
5503
 
5318
- - Can run on schedules (cron syntax)
5319
- - Run independently or triggered by events
5320
- - NOT the same as Studio Workflows
5321
- - Use \`step()\` for durable execution (survives restarts)
5504
+ user: {
5505
+ state: z.object({}),
5506
+ },
5322
5507
 
5323
- \`\`\`typescript
5324
- export default new Workflow({
5325
- name: "periodic-indexing",
5326
- schedule: "0 */6 * * *",
5327
- handler: async ({ step }) => {
5328
- await step("task-name", async () => {
5329
- // Your logic here
5330
- });
5508
+ dependencies: {
5509
+ integrations: ${integrationsJson},
5331
5510
  },
5332
5511
  });
5333
- \`\`\`
5334
-
5335
- #### Advanced Workflow Step Methods
5336
-
5337
- Beyond basic \`step()\`, workflows have powerful methods for complex orchestration:
5338
-
5339
- **Parallel Processing:**
5340
-
5341
- - \`step.map()\` - Process array items in parallel with concurrency control
5342
- - \`step.forEach()\` - Like map but for side effects (returns void)
5343
- - \`step.batch()\` - Process in sequential batches
5344
-
5345
- \`\`\`typescript
5346
- // Process items in parallel
5347
- const results = await step.map(
5348
- 'process-items',
5349
- items,
5350
- async (item, { i }) => processItem(item),
5351
- { concurrency: 5, maxAttempts: 3 }
5352
- )
5353
-
5354
- // Batch processing
5355
- await step.batch(
5356
- 'bulk-insert',
5357
- records,
5358
- async (batch) => database.bulkInsert(batch),
5359
- { batchSize: 100 }
5360
- )
5361
- \`\`\`
5512
+ `;
5513
+ await this.writeFormattedFile("agent.config.ts", agentConfig);
5514
+ }
5515
+ createTsConfig() {
5516
+ const tsConfig = {
5517
+ compilerOptions: {
5518
+ target: "ES2022",
5519
+ module: "ES2022",
5520
+ moduleResolution: "Bundler",
5521
+ lib: ["ES2022", "DOM"],
5522
+ outDir: "./dist",
5523
+ rootDir: ".",
5524
+ strict: true,
5525
+ esModuleInterop: true,
5526
+ allowSyntheticDefaultImports: true,
5527
+ skipLibCheck: true,
5528
+ forceConsistentCasingInFileNames: true,
5529
+ moduleDetection: "force",
5530
+ resolveJsonModule: true,
5531
+ paths: {
5532
+ "@botpress/runtime/_types/*": ["./.adk/*-types"]
5533
+ }
5534
+ },
5535
+ include: ["src/**/*", ".adk/**/*"],
5536
+ exclude: ["node_modules", "dist"]
5537
+ };
5538
+ this.writeJsonFile("tsconfig.json", tsConfig);
5539
+ }
5540
+ createAgentJson() {
5541
+ const agentJson = {};
5542
+ this.writeJsonFile("agent.json", agentJson);
5543
+ }
5544
+ createGitIgnore() {
5545
+ const gitIgnore = `# Dependencies
5546
+ node_modules/
5547
+ .pnpm-store/
5362
5548
 
5363
- **Workflow Coordination:**
5549
+ # Build outputs
5550
+ dist/
5551
+ .adk/
5364
5552
 
5365
- - \`step.waitForWorkflow()\` - Wait for another workflow to complete
5366
- - \`step.executeWorkflow()\` - Start and wait in one call
5553
+ # Environment files
5554
+ .env
5555
+ .env.local
5556
+ .env.production
5367
5557
 
5368
- \`\`\`typescript
5369
- const result = await step.executeWorkflow('run-child', ChildWorkflow, { input })
5370
- \`\`\`
5558
+ # IDE files
5559
+ .vscode/
5560
+ .idea/
5561
+ *.swp
5562
+ *.swo
5371
5563
 
5372
- **Timing Control:**
5564
+ # OS files
5565
+ .DS_Store
5566
+ Thumbs.db
5373
5567
 
5374
- - \`step.sleep()\` - Pause execution (< 10s in-memory, >= 10s uses listening mode)
5375
- - \`step.sleepUntil()\` - Sleep until specific time
5376
- - \`step.listen()\` - Pause and wait for external event
5568
+ # Logs
5569
+ *.log
5570
+ logs/
5377
5571
 
5378
- \`\`\`typescript
5379
- await step.sleep('wait-5s', 5000)
5380
- await step.sleepUntil('wait-until-noon', new Date('2025-01-15T12:00:00Z'))
5381
- \`\`\`
5572
+ # Runtime files
5573
+ *.pid
5574
+ *.seed
5575
+ *.pid.lock
5576
+ `;
5577
+ this.writeFile(".gitignore", gitIgnore);
5578
+ }
5579
+ async createReadme() {
5580
+ const installCommand = this.packageManager === "npm" ? "npm install" : this.packageManager === "yarn" ? "yarn install" : `${this.packageManager} install`;
5581
+ const readme = `# ${this.projectName}
5382
5582
 
5383
- **Request Data from Conversation:**
5583
+ A Botpress Agent built with the ADK.
5384
5584
 
5385
- \`\`\`typescript
5386
- // In workflow
5387
- const { topic } = await step.request('topic', 'What topic should I research?')
5585
+ ## Getting Started
5388
5586
 
5389
- // In conversation
5390
- if (isWorkflowDataRequest(event)) {
5391
- await workflow.provide(event, { topic: userInput })
5392
- }
5393
- \`\`\`
5587
+ 1. Install dependencies:
5588
+ \`\`\`bash
5589
+ ${installCommand}
5590
+ \`\`\`
5394
5591
 
5395
- **Execution Control:**
5592
+ 2. Start development server:
5593
+ \`\`\`bash
5594
+ adk dev
5595
+ \`\`\`
5396
5596
 
5397
- - \`step.fail()\` - Mark workflow as failed
5398
- - \`step.abort()\` - Abort without failing
5399
- - \`step.progress()\` - Record progress checkpoint
5597
+ 3. Deploy your agent:
5598
+ \`\`\`bash
5599
+ adk deploy
5600
+ \`\`\`
5400
5601
 
5401
- ### 4. Tools (\`src/tools/\`)
5602
+ ## Project Structure
5402
5603
 
5403
- **AI-callable functions** that enable agents to perform actions:
5604
+ - \`src/actions/\` - Define callable functions
5605
+ - \`src/workflows/\` - Define long-running processes
5606
+ - \`src/conversations/\` - Define conversation handlers
5607
+ - \`src/tables/\` - Define data storage schemas
5608
+ - \`src/triggers/\` - Define event subscriptions
5609
+ - \`src/knowledge/\` - Add knowledge base files
5404
5610
 
5405
- - Must have clear name and description
5406
- - Use Zod schemas for input/output
5407
- - Can be passed to \`execute()\`
5408
-
5409
- \`\`\`typescript
5410
- export default new Autonomous.Tool({
5411
- name: "searchDatabase",
5412
- description: "Search the database",
5413
- input: z.object({ query: z.string() }),
5414
- output: z.object({ results: z.array(z.any()) }),
5415
- handler: async ({ query }) => {
5416
- // Tool logic
5417
- return { results: [] };
5418
- },
5419
- });
5420
- \`\`\`
5421
-
5422
- ### 5. Knowledge Bases (\`src/knowledge/\`)
5423
-
5424
- **RAG (Retrieval-Augmented Generation)** for providing context:
5425
-
5426
- - Website scraping
5427
- - Document ingestion
5428
- - Can be passed to \`execute()\` via \`knowledge\` parameter
5429
-
5430
- ### 6. Actions (\`src/actions/\`)
5431
-
5432
- **Reusable business logic** that can:
5433
-
5434
- - Be called from anywhere (import \`actions\` from \`@botpress/runtime\`)
5435
- - Be converted to tools with \`.asTool()\`
5436
- - Encapsulate logic not tied to conversational flow
5437
-
5438
- ### 7. Zai Library
5439
-
5440
- **Zai** is an LLM utility library that provides a clean, type-safe API for common AI operations. It's designed to work seamlessly with the ADK and SDK to process LLM inputs and outputs programmatically.
5441
-
5442
- #### Importing Zai in ADK
5443
-
5444
- In the ADK, Zai is available from \`@botpress/runtime\`:
5445
-
5446
- \`\`\`typescript
5447
- import { adk } from '@botpress/runtime'
5448
- // then adk.zai.<method_name>
5449
- \`\`\`
5450
-
5451
- The default model for Zai operations is configured in \`agent.config.ts\`:
5452
-
5453
- \`\`\`typescript
5454
- export default defineConfig({
5455
- defaultModels: {
5456
- autonomous: "cerebras:gpt-oss-120b",
5457
- zai: "cerebras:gpt-oss-120b", // Model used for Zai operations
5458
- },
5459
- })
5460
- \`\`\`
5461
-
5462
- #### When to Use Zai
5463
-
5464
- Use Zai when you need to:
5465
- - Extract structured data from unstructured text
5466
- - Answer questions from documents with source citations
5467
- - Verify Boolean conditions in content
5468
- - Summarize long text into concise summaries
5469
- - Generate text programmatically based on prompts
5470
-
5471
- **Use Zai instead of \`execute()\` when**: You need deterministic, structured outputs for specific AI tasks (extraction, validation, summarization) rather than conversational interactions.
5472
-
5473
- #### Zai Methods
5474
-
5475
- **1. \`answer()\` - Answer Questions with Citations**
5476
-
5477
- Answers questions from documents with intelligent source citations.
5478
-
5479
- \`\`\`typescript
5480
- const documents = [
5481
- 'Botpress was founded in 2016.',
5482
- 'The company is based in Quebec, Canada.',
5483
- ]
5484
-
5485
- const result = await zai.answer(documents, 'When was Botpress founded?')
5486
-
5487
- if (result.type === 'answer') {
5488
- console.log(result.answer) // "Botpress was founded in 2016."
5489
- console.log(result.citations) // Array of citations with source references
5490
- }
5491
- \`\`\`
5492
-
5493
- **When to use**: When you need to answer questions from a set of documents with traceable sources (e.g., custom RAG implementations, document Q&A).
5494
-
5495
- **2. \`extract()\` - Extract Structured Data**
5496
-
5497
- Extracts structured data from unstructured input using Zod schemas.
5498
-
5499
- \`\`\`typescript
5500
- import { z, adk } from '@botpress/runtime'
5501
-
5502
- const userSchema = z.object({
5503
- name: z.string(),
5504
- email: z.string().email(),
5505
- age: z.number()
5506
- })
5507
-
5508
- const input = "My name is John Doe, I'm 30 years old and my email is john@example.com"
5509
- // zai.extract returns the extracted data DIRECTLY (not wrapped in { output: ... })
5510
- const result = await adk.zai.extract(input, userSchema)
5511
-
5512
- console.log(result)
5513
- // { name: "John Doe", email: "john@example.com", age: 30 }
5514
- \`\`\`
5515
-
5516
- **When to use**: When you need to parse unstructured user input into structured data (e.g., form extraction from natural language, parsing contact information).
5517
-
5518
- **3. \`check()\` - Verify Boolean Conditions**
5519
-
5520
- Verifies a condition against some input and returns a boolean with explanation.
5521
-
5522
- \`\`\`typescript
5523
- const email = "Get rich quick! Click here now!!!"
5524
- const { output } = await zai.check(email, 'is spam').result()
5525
-
5526
- console.log(output.value) // true
5527
- console.log(output.explanation) // "This email contains typical spam indicators..."
5528
- \`\`\`
5529
-
5530
- **When to use**: When you need to validate content or make binary decisions (e.g., content moderation, intent verification, condition checking).
5531
-
5532
- **4. \`summarize()\` - Summarize Text**
5533
-
5534
- Creates concise summaries of lengthy text to a desired length.
5535
-
5536
- \`\`\`typescript
5537
- const longArticle = "..." // Long article content
5538
-
5539
- const summary = await zai.summarize(longArticle, {
5540
- length: 100, // tokens
5541
- prompt: 'key findings and main conclusions'
5542
- })
5543
- \`\`\`
5544
-
5545
- **When to use**: When you need to condense long content (e.g., article summaries, transcript summaries, document overviews).
5546
-
5547
- **5. \`text()\` - Generate Text**
5548
-
5549
- Generates text of the desired length according to a prompt.
5550
-
5551
- \`\`\`typescript
5552
- const generated = await zai.text('Write a welcome message for new users', {
5553
- length: 50 // tokens
5554
- })
5555
- \`\`\`
5556
-
5557
- **When to use**: When you need to generate specific text content programmatically (e.g., dynamic content generation, templated responses).
5558
-
5559
- #### Response Methods
5560
-
5561
- All Zai operations return a Response object with promise-like behavior and additional functionality:
5562
-
5563
- \`\`\`typescript
5564
- // Await the result directly
5565
- const result = await zai.extract(input, schema)
5566
-
5567
- // Or use .result() for explicit promise handling
5568
- const { output } = await zai.check(content, 'is valid').result()
5569
- \`\`\`
5570
-
5571
- ## Project Structure
5572
-
5573
- \`\`\`
5574
- agent.config.ts # Main configuration
5575
- src/
5576
- conversations/ # Message handlers (primary user interaction)
5577
- workflows/ # Long-running processes
5578
- tools/ # AI-callable functions
5579
- actions/ # Reusable business logic
5580
- knowledge/ # Knowledge bases for RAG
5581
- triggers/ # Event-based triggers
5582
- tables/ # Database tables
5583
- .botpress/ # Auto-generated types (DO NOT EDIT)
5584
- \`\`\`
5585
-
5586
- ## Development Workflow
5587
-
5588
- 1. **Start dev server**: \`adk dev\` (http://localhost:3001 for console)
5589
- 2. **Add integrations**: \`adk add webchat@latest\`
5590
- 3. **Build**: \`adk build\`
5591
- 4. **Deploy**: \`adk deploy\`
5592
- 5. **Chat in CLI**: \`adk chat\`
5593
-
5594
- ## Examples
5595
-
5596
- Official examples: https://github.com/botpress/adk/tree/main/examples
5597
-
5598
- ### subagents
5599
-
5600
- **What you'll learn:** How to build a multi-agent system where an orchestrator delegates to specialists.
5601
-
5602
- Shows the \`SubAgent\` pattern where each specialist (HR, IT, Sales, etc.) runs in its own context with \`mode: "worker"\`, returns structured results via custom exits, and reports progress through \`onTrace\` hooks.
5603
-
5604
- ### webchat-rag
5605
-
5606
- **What you'll learn:** How to build a RAG assistant with scheduled indexing, guardrails, and admin features.
5607
-
5608
- Shows \`Autonomous.Object\` for dynamic tool grouping, \`onBeforeTool\` hooks to enforce knowledge search before answering, scheduled workflows for KB refresh, and \`ThinkSignal\` for interrupting execution.
5609
-
5610
- ### deep-research
5611
-
5612
- **What you'll learn:** How to build complex, long-running workflows with progress tracking.
5613
-
5614
- Shows \`step()\` and \`step.map()\` for workflow phases, \`Reference.Workflow\` for conversation-workflow linking, Tables for activity tracking, and extensive Zai usage (\`extract\`, \`answer\`, \`filter\`, \`text\`).
5615
-
5616
- ## Best Practices
5617
-
5618
- 1. **Search Botpress docs first** - Use the MCP tool before implementing
5619
- 2. **Keep tools focused** - Single responsibility per tool
5620
- 3. **Use Zod schemas** with \`.describe()\` for clarity
5621
- 4. **State management** - Minimize large variables in main workflow
5622
- 5. **Type safety** - Run \`adk dev\` or \`adk build\` to regenerate types after config changes
5623
- 6. **Conversations vs Workflows**:
5624
- - Conversations: User interactions, real-time responses
5625
- - Workflows: Background tasks, scheduled jobs, long-running processes
5626
-
5627
- ## Common APIs
5628
-
5629
- ### Conversation Handler
5630
-
5631
- \`\`\`typescript
5632
- handler: async ({
5633
- execute, // Run autonomous AI loop
5634
- conversation, // Send messages, manage conversation
5635
- state, // Conversation state (persisted)
5636
- message, // Incoming message
5637
- client, // Botpress API client
5638
- }) => {};
5639
- \`\`\`
5640
-
5641
- ### Execute Function
5642
-
5643
- \`\`\`typescript
5644
- await execute({
5645
- instructions: "String or function returning instructions",
5646
- tools: [tool1, tool2], // Optional tools
5647
- knowledge: [kb1, kb2], // Optional knowledge bases
5648
- exits: [customExit], // Optional custom exits
5649
- hooks: { onTrace, onBeforeTool }, // Optional hooks
5650
- mode: "worker", // Optional: autonomous until exit
5651
- iterations: 10, // Max loops (default 10)
5652
- });
5653
- \`\`\`
5654
-
5655
- ## Advanced Autonomous Execution
5656
-
5657
- ### Autonomous Namespace
5658
-
5659
- The \`Autonomous\` namespace provides powerful primitives for controlling LLM behavior:
5660
-
5661
- #### Autonomous.Exit - Custom Exit Conditions
5662
-
5663
- Define custom exits for autonomous execution loops:
5664
-
5665
- \`\`\`typescript
5666
- import { Autonomous, z } from '@botpress/runtime'
5667
-
5668
- const AnswerExit = new Autonomous.Exit({
5669
- name: 'answer',
5670
- description: 'Return when you have the final answer',
5671
- schema: z.object({
5672
- answer: z.string(),
5673
- confidence: z.number()
5674
- })
5675
- })
5676
-
5677
- const NoAnswerExit = new Autonomous.Exit({
5678
- name: 'no_answer',
5679
- description: 'No answer could be found'
5680
- })
5681
-
5682
- const result = await execute({
5683
- instructions: 'Research and answer the question',
5684
- exits: [AnswerExit, NoAnswerExit],
5685
- mode: 'worker' // Run until exit triggered
5686
- })
5687
-
5688
- // ✅ CORRECT - Use result.is() and result.output
5689
- if (result.is(AnswerExit)) {
5690
- console.log(result.output.answer) // Type-safe access
5691
- console.log(result.output.confidence)
5692
- } else if (result.is(NoAnswerExit)) {
5693
- console.log('No answer found')
5694
- }
5695
-
5696
- // ❌ WRONG - Don't use result.exit.name or result.exit.value
5697
- // if (result.exit?.name === 'answer') { ... }
5698
- \`\`\`
5699
-
5700
- #### Autonomous.ThinkSignal - Inject Context
5701
-
5702
- Provide context to the LLM without continuing execution:
5703
-
5704
- \`\`\`typescript
5705
- const results = await fetchData()
5706
-
5707
- if (!results.length) {
5708
- throw new ThinkSignal('error', 'No results found')
5709
- }
5710
-
5711
- // Inject formatted results into LLM context
5712
- throw new ThinkSignal('results ready', formatResults(results))
5713
- \`\`\`
5714
-
5715
- #### Autonomous.Object - Dynamic Tool Grouping
5716
-
5717
- Group tools dynamically based on state:
5718
-
5719
- \`\`\`typescript
5720
- const adminTools = new Autonomous.Object({
5721
- name: 'admin',
5722
- description: user.isAdmin ? 'Admin tools available' : 'Login required',
5723
- tools: user.isAdmin ? [refreshKB, manageBots] : [generateLoginCode]
5724
- })
5725
-
5726
- await execute({
5727
- objects: [adminTools]
5728
- })
5729
- \`\`\`
5730
-
5731
- ### Execution Hooks
5732
-
5733
- Full control over the autonomous execution loop:
5734
-
5735
- \`\`\`typescript
5736
- await execute({
5737
- instructions: '...',
5738
- hooks: {
5739
- // Before tool execution - can modify input
5740
- onBeforeTool: async ({ iteration, tool, input, controller }) => {
5741
- console.log(\`About to call \${tool.name}\`)
5742
- return { input: modifiedInput } // Optional: transform input
5743
- },
5744
-
5745
- // After tool execution - can modify output
5746
- onAfterTool: async ({ iteration, tool, input, output, controller }) => {
5747
- console.log(\`\${tool.name} returned:\`, output)
5748
- return { output: modifiedOutput } // Optional: transform output
5749
- },
5750
-
5751
- // Before code execution in iteration
5752
- onBeforeExecution: async (iteration, controller) => {
5753
- return { code: modifiedCode } // Optional: transform generated code
5754
- },
5755
-
5756
- // When exit is triggered
5757
- onExit: async (result) => {
5758
- console.log('Exited with:', result)
5759
- },
5760
-
5761
- // After each iteration completes
5762
- onIterationEnd: async (iteration, controller) => {
5763
- if (iteration > 5) {
5764
- controller.abort() // Stop execution
5765
- }
5766
- },
5767
-
5768
- // On trace events (synchronous, non-blocking)
5769
- onTrace: ({ trace, iteration }) => {
5770
- if (trace.type === 'comment') {
5771
- console.log('LLM thinking:', trace.comment)
5772
- }
5773
- if (trace.type === 'tool_call') {
5774
- console.log('Calling:', trace.tool_name)
5775
- }
5776
- }
5777
- }
5778
- })
5779
- \`\`\`
5780
-
5781
- **Hook use cases:**
5782
- - Logging and debugging
5783
- - Input/output validation and transformation
5784
- - Rate limiting tool calls
5785
- - Custom abort conditions
5786
- - Injecting dynamic context
5787
-
5788
- ## State and Metadata Management
5789
-
5790
- ### Tags - Key-Value Metadata
5791
-
5792
- Track metadata for any entity (bot, user, conversation, workflow):
5793
-
5794
- \`\`\`typescript
5795
- import { TrackedTags } from '@botpress/runtime'
5796
-
5797
- // Create tags instance
5798
- const tags = TrackedTags.create({
5799
- type: 'bot', // or 'user' | 'conversation' | 'workflow'
5800
- id: entityId,
5801
- client: botClient,
5802
- initialTags: { status: 'active' }
5803
- })
5804
-
5805
- // Load from server
5806
- await tags.load()
5807
-
5808
- // Modify tags
5809
- tags.tags = {
5810
- ...tags.tags,
5811
- lastSync: new Date().toISOString()
5812
- }
5813
-
5814
- // Check if modified
5815
- if (tags.isDirty()) {
5816
- await tags.save()
5817
- }
5818
-
5819
- // Batch operations
5820
- await TrackedTags.saveAllDirty()
5821
- await TrackedTags.loadAll()
5822
- \`\`\`
5823
-
5824
- **Access via workflow instance:**
5825
-
5826
- \`\`\`typescript
5827
- workflow.tags = { status: 'processing' }
5828
- await workflow.save()
5829
- \`\`\`
5830
-
5831
- ### Reference.Workflow - Typed Workflow References
5832
-
5833
- Serialize workflow references in state that auto-hydrate on access:
5834
-
5835
- \`\`\`typescript
5836
- import { Reference, z } from '@botpress/runtime'
5837
-
5838
- // In conversation state schema
5839
- state: z.object({
5840
- research: Reference.Workflow('deep_research').optional()
5841
- // or untyped: Reference.Workflow().optional()
5842
- })
5843
-
5844
- // In handler - always a WorkflowInstance
5845
- handler: async ({ state }) => {
5846
- if (state.research) {
5847
- // state.research is typed WorkflowInstance
5848
- console.log(state.research.status) // 'running' | 'completed' | etc
5849
- console.log(state.research.output) // Typed output
5850
-
5851
- if (state.research.status === 'completed') {
5852
- // Access completed workflow data
5853
- }
5854
- }
5855
- }
5856
- \`\`\`
5857
-
5858
- ### Context Object - Runtime Access
5859
-
5860
- Global context for accessing runtime information:
5861
-
5862
- \`\`\`typescript
5863
- import { context } from '@botpress/runtime'
5864
-
5865
- // Get specific context
5866
- const client = context.get('client')
5867
- const citations = context.get('citations')
5868
- const logger = context.get('logger')
5869
-
5870
- // Get all context
5871
- const { client, cognitive, logger, operation } = context.getAll()
5872
- \`\`\`
5873
-
5874
- **Available context properties:**
5875
- - \`client\` - Botpress API client
5876
- - \`cognitive\` - LLM access
5877
- - \`logger\` - Logging
5878
- - \`operation\` - Current operation info
5879
- - \`citations\` - Citation tracking
5880
- - \`chat\` - Chat interface
5881
- - \`bot\` - Bot tags and metadata
5882
- - \`user\` - User information
5883
- - \`conversation\` - Current conversation
5884
- - \`message\` - Incoming message
5885
- - \`event\` - Current event
5886
- - \`workflow\` - Current workflow
5887
- - \`workflowControlContext\` - Workflow control (abort, fail, restart)
5888
-
5889
- ### State Management
5890
-
5891
- Access and modify tracked state:
5892
-
5893
- \`\`\`typescript
5894
- import { bot, user } from '@botpress/runtime'
5895
-
5896
- // Bot state
5897
- bot.state.lastIndexed = new Date().toISOString()
5898
- bot.state.config = { theme: 'dark' }
5899
-
5900
- // User state
5901
- user.state.preferences = { notifications: true }
5902
- user.state.lastActive = Date.now()
5903
- \`\`\`
5904
-
5905
- State persists automatically across executions.
5906
-
5907
- ## Advanced Table Operations
5908
-
5909
- ### Table Naming Rules
5910
-
5911
- **IMPORTANT**: Tables have strict naming requirements:
5912
-
5913
- \`\`\`typescript
5914
- // ✅ CORRECT - Name must end with "Table"
5915
- export const MyDataTable = new Table({
5916
- name: "mydataTable", // Must end with "Table"
5917
- columns: { ... }
5918
- });
5919
-
5920
- // ❌ WRONG - Missing "Table" suffix
5921
- name: "mydata"
5922
- name: "my_data"
5923
- \`\`\`
5924
-
5925
- **Reserved column names** - Cannot use these as column names:
5926
- - \`id\` (auto-generated)
5927
- - \`createdAt\` (auto-generated)
5928
- - \`updatedAt\` (auto-generated)
5929
- - \`computed\`
5930
- - \`stale\`
5931
-
5932
- \`\`\`typescript
5933
- // ❌ WRONG - Using reserved column name
5934
- columns: {
5935
- createdAt: z.string() // Reserved!
5936
- }
5937
-
5938
- // ✅ CORRECT - Use alternative name
5939
- columns: {
5940
- savedAt: z.string()
5941
- }
5942
- \`\`\`
5943
-
5944
- ### Auto-Registration
5945
-
5946
- Files in \`src/tables/\` are **auto-registered** by the ADK. Do NOT re-export from index.ts:
5947
-
5948
- \`\`\`typescript
5949
- // src/tables/index.ts
5950
- // ❌ WRONG - Causes duplicate registration errors
5951
- export { MyTable } from "./myTable";
5952
-
5953
- // ✅ CORRECT - Leave empty or add comment
5954
- // Tables are auto-registered from src/tables/*.ts files
5955
- \`\`\`
5956
-
5957
- Same applies to \`src/conversations/\`, \`src/workflows/\`, \`src/triggers/\`, etc.
5958
-
5959
- Beyond basic CRUD, Tables support powerful query and manipulation features:
5960
-
5961
- ### Complex Filtering
5962
-
5963
- Use logical operators and conditions:
5964
-
5965
- \`\`\`typescript
5966
- await MyTable.findRows({
5967
- filter: {
5968
- $and: [
5969
- { status: 'open' },
5970
- { priority: { $in: ['high', 'urgent'] } }
5971
- ],
5972
- $or: [
5973
- { assignee: userId },
5974
- { reporter: userId }
5975
- ],
5976
- title: { $regex: 'bug|error', $options: 'i' }
5977
- }
5978
- })
5979
- \`\`\`
5980
-
5981
- **Filter operators:**
5982
- - \`$eq\`, \`$ne\` - Equal, not equal
5983
- - \`$gt\`, \`$gte\`, \`$lt\`, \`$lte\` - Comparisons
5984
- - \`$in\`, \`$nin\` - In array, not in array
5985
- - \`$exists\` - Field exists
5986
- - \`$regex\` - Regular expression match
5987
- - \`$options\` - Regex options (e.g., 'i' for case-insensitive)
5988
- - \`$and\`, \`$or\` - Logical operators
5989
-
5990
- ### Full-Text Search
5991
-
5992
- Search across searchable columns:
5993
-
5994
- \`\`\`typescript
5995
- await MyTable.findRows({
5996
- search: 'query string',
5997
- filter: { status: 'active' }
5998
- })
5999
- \`\`\`
6000
-
6001
- Mark columns as searchable in schema:
6002
-
6003
- \`\`\`typescript
6004
- columns: {
6005
- title: z.string().searchable(),
6006
- description: z.string().searchable()
6007
- }
6008
- \`\`\`
6009
-
6010
- ### Aggregation and Grouping
6011
-
6012
- Group and aggregate data:
6013
-
6014
- \`\`\`typescript
6015
- await MyTable.findRows({
6016
- group: {
6017
- status: 'count',
6018
- priority: ['sum', 'avg'],
6019
- complexity: ['max', 'min']
6020
- }
6021
- })
6022
- \`\`\`
6023
-
6024
- **Aggregation operations:** \`key\`, \`count\`, \`sum\`, \`avg\`, \`max\`, \`min\`, \`unique\`
6025
-
6026
- ### Computed Columns
6027
-
6028
- Columns with values computed from row data:
6029
-
6030
- \`\`\`typescript
6031
- columns: {
6032
- fullName: {
6033
- computed: true,
6034
- schema: z.string(),
6035
- dependencies: ['firstName', 'lastName'],
6036
- value: async (row) => \`\${row.firstName} \${row.lastName}\`
6037
- },
6038
- age: {
6039
- computed: true,
6040
- schema: z.number(),
6041
- dependencies: ['birthDate'],
6042
- value: async (row) => {
6043
- const today = new Date()
6044
- const birth = new Date(row.birthDate)
6045
- return today.getFullYear() - birth.getFullYear()
6046
- }
6047
- }
6048
- }
6049
- \`\`\`
6050
-
6051
- ### Upsert Operations
6052
-
6053
- Insert or update based on key column:
6054
-
6055
- \`\`\`typescript
6056
- await MyTable.upsertRows({
6057
- rows: [
6058
- { externalId: '123', name: 'Item 1' },
6059
- { externalId: '456', name: 'Item 2' }
6060
- ],
6061
- keyColumn: 'externalId', // Update if exists, insert if not
6062
- waitComputed: true // Wait for computed columns to update
6063
- })
6064
- \`\`\`
6065
-
6066
- ### Bulk Operations
6067
-
6068
- Efficient batch operations:
6069
-
6070
- \`\`\`typescript
6071
- // Delete by filter
6072
- await MyTable.deleteRows({
6073
- filter: { status: 'archived', createdAt: { $lt: '2024-01-01' } }
6074
- })
6075
-
6076
- // Delete by IDs
6077
- await MyTable.deleteRowIds([1, 2, 3])
6078
-
6079
- // Delete all
6080
- await MyTable.deleteAllRows()
6081
-
6082
- // Update multiple
6083
- await MyTable.updateRows({
6084
- rows: [
6085
- { id: 1, status: 'active' },
6086
- { id: 2, status: 'inactive' }
6087
- ],
6088
- waitComputed: true
6089
- })
6090
- \`\`\`
6091
-
6092
- ### Error Handling
6093
-
6094
- Collect errors and warnings from bulk operations:
6095
-
6096
- \`\`\`typescript
6097
- const { errors, warnings } = await MyTable.createRows({
6098
- rows: data,
6099
- waitComputed: true
6100
- })
6101
-
6102
- if (errors?.length) {
6103
- console.error('Failed rows:', errors)
6104
- }
6105
- if (warnings?.length) {
6106
- console.warn('Warnings:', warnings)
6107
- }
6108
- \`\`\`
6109
-
6110
- ## Knowledge Base Operations
6111
-
6112
- ### Data Sources
6113
-
6114
- Multiple source types for knowledge bases:
6115
-
6116
- #### Directory Source
6117
-
6118
- \`\`\`typescript
6119
- import { DataSource } from '@botpress/runtime'
6120
-
6121
- const docs = DataSource.Directory.fromPath('src/knowledge', {
6122
- id: 'docs',
6123
- filter: (path) => path.endsWith('.md') || path.endsWith('.txt')
6124
- })
6125
- \`\`\`
6126
-
6127
- #### Website Source
6128
-
6129
- \`\`\`typescript
6130
- const siteDocs = DataSource.Website.fromSitemap('https://example.com/sitemap.xml', {
6131
- id: 'website',
6132
- maxPages: 500,
6133
- fetch: 'node:fetch' // or custom fetch implementation
6134
- })
6135
- \`\`\`
6136
-
6137
- ### Knowledge Base Definition
6138
-
6139
- \`\`\`typescript
6140
- import { Knowledge } from '@botpress/runtime'
6141
-
6142
- export default new Knowledge({
6143
- name: 'docs',
6144
- description: 'Product documentation',
6145
- sources: [docsDirectory, websiteSource]
6146
- })
6147
- \`\`\`
6148
-
6149
- ### Refresh Operations
6150
-
6151
- Manually refresh knowledge base content:
6152
-
6153
- \`\`\`typescript
6154
- // Refresh entire knowledge base
6155
- await DocsKB.refresh({ force: true })
6156
-
6157
- // Refresh specific source
6158
- await DocsKB.refreshSource('website', { force: true })
6159
- \`\`\`
6160
-
6161
- **Options:**
6162
- - \`force: true\` - Force refresh even if recently updated
6163
- - Automatic refresh via scheduled workflows recommended
6164
-
6165
- ### Using Knowledge in Execute
6166
-
6167
- \`\`\`typescript
6168
- await execute({
6169
- instructions: 'Answer using the documentation',
6170
- knowledge: [DocsKB, APIKB],
6171
- tools: [searchTool]
6172
- })
6173
- \`\`\`
6174
-
6175
- Knowledge bases are automatically searchable via the \`search_knowledge\` tool.
6176
-
6177
- ## Advanced Conversation Patterns
6178
-
6179
- ### Multiple Channel Support
6180
-
6181
- Handle messages from multiple channels in one handler:
6182
-
6183
- \`\`\`typescript
6184
- export default new Conversation({
6185
- channel: ['chat.channel', 'webchat.channel', 'slack.dm'],
6186
- handler: async ({ channel, execute }) => {
6187
- console.log(\`Message from: \${channel}\`)
6188
- await execute({ instructions: '...' })
6189
- }
6190
- })
6191
- \`\`\`
6192
-
6193
- ### Event Handling
6194
-
6195
- Subscribe to integration events:
6196
-
6197
- \`\`\`typescript
6198
- export default new Conversation({
6199
- channel: 'webchat.channel',
6200
- events: ['webchat:conversationStarted', 'webchat:conversationEnded'],
6201
- handler: async ({ type, event, message }) => {
6202
- if (type === 'event' && event.type === 'webchat:conversationStarted') {
6203
- // Send welcome message
6204
- await conversation.send({
6205
- type: 'text',
6206
- payload: { text: 'Welcome!' }
6207
- })
6208
- }
6209
-
6210
- if (type === 'message' && message?.type === 'text') {
6211
- // Handle regular messages
6212
- await execute({ instructions: '...' })
6213
- }
6214
- }
6215
- })
6216
- \`\`\`
6217
-
6218
- ### Workflow Request Handling
6219
-
6220
- Handle data requests from workflows:
6221
-
6222
- \`\`\`typescript
6223
- import { isWorkflowDataRequest } from '@botpress/runtime'
6224
-
6225
- handler: async ({ type, event, execute }) => {
6226
- // Check if this is a workflow requesting data
6227
- if (type === 'workflow_request' && isWorkflowDataRequest(event)) {
6228
- const userInput = await promptUser(event.payload.message)
6229
-
6230
- // Provide data back to workflow
6231
- await workflow.provide(event, { topic: userInput })
6232
- return
6233
- }
6234
-
6235
- // Regular message handling
6236
- await execute({ instructions: '...' })
6237
- }
6238
- \`\`\`
6239
-
6240
- ### Typed Workflow Interactions
6241
-
6242
- Work with typed workflow instances:
6243
-
6244
- \`\`\`typescript
6245
- import { isWorkflow, ResearchWorkflow } from '@botpress/runtime'
6246
-
6247
- handler: async ({ state }) => {
6248
- if (state.research && isWorkflow(state.research, 'research')) {
6249
- // state.research is now typed as ResearchWorkflow
6250
- console.log(state.research.status)
6251
- console.log(state.research.output) // Typed output
6252
-
6253
- if (state.research.status === 'completed') {
6254
- await conversation.send({
6255
- type: 'text',
6256
- payload: { text: state.research.output.result }
6257
- })
6258
- }
6259
- }
6260
- }
6261
- \`\`\`
6262
-
6263
- ### Dynamic Tools Based on State
6264
-
6265
- Provide different tools based on conversation state:
6266
-
6267
- \`\`\`typescript
6268
- handler: async ({ state, execute }) => {
6269
- const tools = () => {
6270
- if (state.workflowRunning) {
6271
- return [cancelWorkflowTool, checkStatusTool]
6272
- } else {
6273
- return [startWorkflowTool, browseTool, searchTool]
6274
- }
6275
- }
6276
-
6277
- await execute({
6278
- instructions: '...',
6279
- tools: tools()
6280
- })
6281
- }
6282
- \`\`\`
6283
-
6284
- ### Message Sending
6285
-
6286
- Send different message types:
6287
-
6288
- \`\`\`typescript
6289
- // Text message
6290
- await conversation.send({
6291
- type: 'text',
6292
- payload: { text: 'Hello!' }
6293
- })
6294
-
6295
- // Custom message type (integration-specific)
6296
- await conversation.send({
6297
- type: 'custom:messageType',
6298
- payload: { data: 'custom payload' }
6299
- })
6300
- \`\`\`
6301
-
6302
- ## Citations System
6303
-
6304
- Track and manage source citations for LLM responses:
6305
-
6306
- ### CitationsManager
6307
-
6308
- Access via context:
6309
-
6310
- \`\`\`typescript
6311
- import { context } from '@botpress/runtime'
6312
-
6313
- const citations = context.get('citations')
6314
- \`\`\`
6315
-
6316
- ### Registering Sources
6317
-
6318
- Register sources that can be cited:
6319
-
6320
- \`\`\`typescript
6321
- // Register with URL
6322
- const { tag } = citations.registerSource({
6323
- url: 'https://example.com/doc',
6324
- title: 'Documentation Page'
6325
- })
6326
-
6327
- // Register with file reference
6328
- const { tag } = citations.registerSource({
6329
- file: fileKey,
6330
- title: 'Internal Document'
6331
- })
6332
- \`\`\`
6333
-
6334
- ### Using Citation Tags
6335
-
6336
- Inject citation tags into LLM content:
6337
-
6338
- \`\`\`typescript
6339
- const results = await searchKnowledgeBase(query)
6340
-
6341
- for (const result of results) {
6342
- const { tag } = citations.registerSource({
6343
- file: result.file.key,
6344
- title: result.file.name
6345
- })
6346
-
6347
- content += \`\${result.content} \${tag}\\n\`
6348
- }
6349
-
6350
- // Return cited content
6351
- throw new ThinkSignal('results', content)
6352
- \`\`\`
6353
-
6354
- ### Citation Format
6355
-
6356
- Citations are automatically formatted with tags like \`[1]\`, \`[2]\`, etc., and tracked by the system for reference.
6357
-
6358
- ### Example: Tool with Citations
6359
-
6360
- \`\`\`typescript
6361
- export default new Autonomous.Tool({
6362
- name: 'search_docs',
6363
- description: 'Search documentation',
6364
- handler: async ({ query }) => {
6365
- const citations = context.get('citations')
6366
- const results = await searchDocs(query)
6367
-
6368
- let response = ''
6369
- for (const doc of results) {
6370
- const { tag } = citations.registerSource({
6371
- url: doc.url,
6372
- title: doc.title
6373
- })
6374
- response += \`\${doc.content} \${tag}\\n\\n\`
6375
- }
6376
-
6377
- return response
6378
- }
6379
- })
6380
- \`\`\`
6381
-
6382
- ## Common Mistakes to Avoid
6383
-
6384
- ### 1. Wrong Zai Import
6385
- \`\`\`typescript
6386
- // ❌ WRONG
6387
- import { zai } from '@botpress/runtime'
6388
- const result = await zai.extract(...)
6389
-
6390
- // ✅ CORRECT
6391
- import { adk } from '@botpress/runtime'
6392
- const result = await adk.zai.extract(...)
6393
- \`\`\`
6394
-
6395
- ### 2. Expecting \`.output\` from zai.extract
6396
- \`\`\`typescript
6397
- // ❌ WRONG - zai.extract returns data directly
6398
- const result = await adk.zai.extract(input, schema)
6399
- console.log(result.output) // undefined!
6400
-
6401
- // ✅ CORRECT
6402
- const result = await adk.zai.extract(input, schema)
6403
- console.log(result) // { name: "John", age: 30 }
6404
- \`\`\`
6405
-
6406
- ### 3. Wrong Exit Result Handling
6407
- \`\`\`typescript
6408
- // ❌ WRONG
6409
- if (result.exit?.name === 'my_exit') {
6410
- const data = result.exit.value
6411
- }
6412
-
6413
- // ✅ CORRECT
6414
- if (result.is(MyExit)) {
6415
- const data = result.output // Type-safe!
6416
- }
6417
- \`\`\`
6418
-
6419
- ### 4. Reserved Table Column Names
6420
- \`\`\`typescript
6421
- // ❌ WRONG - These are reserved
6422
- columns: {
6423
- id: z.string(),
6424
- createdAt: z.string(),
6425
- updatedAt: z.string()
6426
- }
6427
-
6428
- // ✅ CORRECT - Use alternatives
6429
- columns: {
6430
- visibleId: z.string(),
6431
- savedAt: z.string(),
6432
- modifiedAt: z.string()
6433
- }
6434
- \`\`\`
6435
-
6436
- ### 5. Re-exporting Auto-Registered Files
6437
- \`\`\`typescript
6438
- // ❌ WRONG - src/tables/index.ts
6439
- export { MyTable } from "./myTable" // Causes duplicates!
6440
-
6441
- // ✅ CORRECT - Leave index.ts empty
6442
- // Files in src/tables/, src/conversations/, etc. are auto-registered
6443
- \`\`\`
6444
-
6445
- ### 6. Table Name Missing "Table" Suffix
6446
- \`\`\`typescript
6447
- // ❌ WRONG
6448
- name: "users"
6449
- name: "user_data"
6450
-
6451
- // ✅ CORRECT
6452
- name: "usersTable"
6453
- name: "userdataTable"
6454
- \`\`\`
6455
-
6456
- ## When Making Changes
6457
-
6458
- 1. **Always search Botpress docs** using \`mcp__botpress-docs__SearchBotpress\`
6459
- 2. **Check examples** for patterns
6460
- 3. **Regenerate types** after changing \`agent.config.ts\` (run \`adk dev\`)
6461
- 4. **Test in dev mode** with hot reloading (\`adk dev\`)
6462
- 5. **Follow TypeScript types** - They're auto-generated from integrations
6463
-
6464
- ## Running Tests
6465
-
6466
- The ADK provides \`setupTestRuntime()\` to initialize the full ADK runtime within your test process. This sets up all environment variables, generates types, and imports the runtime so your tests can use actions, tools, workflows, etc.
6467
-
6468
- ### Bun Test
6469
-
6470
- \`\`\`toml
6471
- # bunfig.toml
6472
- [test]
6473
- preload = ["./test-setup.ts"]
6474
- \`\`\`
6475
-
6476
- \`\`\`typescript
6477
- // test-setup.ts
6478
- import { beforeAll } from "bun:test";
6479
- import { setupTestRuntime } from "@botpress/adk";
6480
-
6481
- beforeAll(async () => {
6482
- const runtime = await setupTestRuntime();
6483
- await runtime.initialize();
6484
- });
6485
- \`\`\`
6486
-
6487
- ### Vitest
6488
-
6489
- \`\`\`typescript
6490
- // vitest.setup.ts
6491
- import { beforeAll } from "vitest";
6492
- import { setupTestRuntime } from "@botpress/adk";
6493
-
6494
- beforeAll(async () => {
6495
- const runtime = await setupTestRuntime();
6496
- await runtime.initialize();
6497
- });
6498
- \`\`\`
6499
-
6500
- \`\`\`typescript
6501
- // vitest.config.ts
6502
- import { defineConfig } from "vitest/config";
6503
-
6504
- export default defineConfig({
6505
- test: {
6506
- setupFiles: ["./vitest.setup.ts"],
6507
- },
6508
- });
6509
- \`\`\`
6510
-
6511
- ### Options
6512
-
6513
- \`setupTestRuntime()\` auto-detects project path and credentials, but you can override:
6514
-
6515
- \`\`\`typescript
6516
- const runtime = await setupTestRuntime({
6517
- projectPath: "/path/to/agent", // defaults to auto-detect from CWD
6518
- credentials: { token: "...", apiUrl: "..." }, // defaults to ~/.adk/credentials
6519
- prod: true, // use production bot instead of dev bot
6520
- forceRegenerate: true, // force regenerate bot project
6521
- env: { CUSTOM_VAR: "value" }, // additional env vars
6522
- });
6523
- \`\`\`
6524
-
6525
- ### Prerequisites
6526
-
6527
- - Must have \`@botpress/adk\` installed as a dev dependency (\`bun add -d @botpress/adk\`)
6528
- - Must have run \`adk dev\` at least once (to create the dev bot)
6529
- - Must be logged in (\`adk login\`) or provide credentials explicitly
6530
-
6531
- ## Resources
6532
-
6533
- - [ADK Overview](https://botpress.com/docs/for-developers/adk/overview)
6534
- - [ADK Getting Started](https://botpress.com/docs/for-developers/adk/getting-started)
6535
- - [Project Structure](https://botpress.com/docs/for-developers/adk/project-structure)
6536
- - [Conversations](https://botpress.com/docs/for-developers/adk/concepts/conversations)
6537
- - [Workflows](https://botpress.com/docs/for-developers/adk/concepts/workflows)
6538
- `;
6539
-
6540
- // src/agent-init/agent-project-generator.ts
6541
- class AgentProjectGenerator {
6542
- projectPath;
6543
- projectName;
6544
- packageManager;
6545
- template;
6546
- constructor(projectPath, packageManager = "bun", template = "blank") {
6547
- this.projectPath = path15.resolve(projectPath);
6548
- this.projectName = path15.basename(this.projectPath);
6549
- this.packageManager = packageManager;
6550
- this.template = template;
6551
- }
6552
- async generate() {
6553
- this.ensureEmptyDirectory();
6554
- this.createPackageJson();
6555
- await this.createAgentConfig();
6556
- this.createTsConfig();
6557
- this.createAgentJson();
6558
- this.createGitIgnore();
6559
- await this.createReadme();
6560
- this.createClaudeMd();
6561
- await this.createSourceStructure();
6562
- }
6563
- ensureEmptyDirectory() {
6564
- if (!fs11.existsSync(this.projectPath)) {
6565
- fs11.mkdirSync(this.projectPath, { recursive: true });
6566
- }
6567
- const files = fs11.readdirSync(this.projectPath);
6568
- if (files.length > 0) {
6569
- throw new Error(`Directory ${this.projectPath} is not empty. Please use an empty directory.`);
6570
- }
6571
- }
6572
- createPackageJson() {
6573
- const packageJson = {
6574
- name: this.projectName,
6575
- version: "1.0.0",
6576
- description: `A Botpress Agent built with the ADK`,
6577
- type: "module",
6578
- packageManager: this.packageManager === "npm" ? undefined : `${this.packageManager}@latest`,
6579
- scripts: {
6580
- dev: "adk dev",
6581
- build: "adk build",
6582
- deploy: "adk deploy"
6583
- },
6584
- dependencies: {
6585
- "@botpress/runtime": `^${"1.15.4"}`
6586
- },
6587
- devDependencies: {
6588
- typescript: "^5.9.3"
6589
- }
6590
- };
6591
- if (packageJson.packageManager === undefined) {
6592
- delete packageJson.packageManager;
6593
- }
6594
- this.writeJsonFile("package.json", packageJson);
6595
- }
6596
- getDefaultDependencies() {
6597
- const dependencies = {
6598
- integrations: {}
6599
- };
6600
- if (this.template === "hello-world") {
6601
- dependencies.integrations = {
6602
- chat: {
6603
- version: "chat@latest",
6604
- enabled: true
6605
- },
6606
- webchat: {
6607
- version: "webchat@latest",
6608
- enabled: true
6609
- }
6610
- };
6611
- }
6612
- return dependencies;
6613
- }
6614
- async createAgentConfig() {
6615
- const dependencies = this.getDefaultDependencies();
6616
- const integrationsJson = JSON.stringify(dependencies.integrations, null, 4).replace(/\n/g, `
6617
- `);
6618
- const defaultModels = this.template === "hello-world" ? `
6619
- defaultModels: {
6620
- autonomous: "cerebras:gpt-oss-120b",
6621
- zai: "cerebras:gpt-oss-120b",
6622
- },
6623
- ` : "";
6624
- const agentConfig = `import { z, defineConfig } from '@botpress/runtime';
6625
-
6626
- export default defineConfig({
6627
- name: '${this.projectName}',
6628
- description: 'An AI agent built with Botpress ADK',
6629
- ${defaultModels}
6630
- bot: {
6631
- state: z.object({}),
6632
- },
6633
-
6634
- user: {
6635
- state: z.object({}),
6636
- },
6637
-
6638
- dependencies: {
6639
- integrations: ${integrationsJson},
6640
- },
6641
- });
6642
- `;
6643
- await this.writeFormattedFile("agent.config.ts", agentConfig);
6644
- }
6645
- createTsConfig() {
6646
- const tsConfig = {
6647
- compilerOptions: {
6648
- target: "ES2022",
6649
- module: "ES2022",
6650
- moduleResolution: "Bundler",
6651
- lib: ["ES2022"],
6652
- outDir: "./dist",
6653
- rootDir: ".",
6654
- strict: true,
6655
- esModuleInterop: true,
6656
- allowSyntheticDefaultImports: true,
6657
- skipLibCheck: true,
6658
- forceConsistentCasingInFileNames: true,
6659
- moduleDetection: "force",
6660
- resolveJsonModule: true,
6661
- paths: {
6662
- "@botpress/runtime/_types/*": ["./.adk/*-types"]
6663
- }
6664
- },
6665
- include: ["src/**/*", ".adk/**/*"],
6666
- exclude: ["node_modules", "dist"]
6667
- };
6668
- this.writeJsonFile("tsconfig.json", tsConfig);
6669
- }
6670
- createAgentJson() {
6671
- const agentJson = {};
6672
- this.writeJsonFile("agent.json", agentJson);
6673
- }
6674
- createGitIgnore() {
6675
- const gitIgnore = `# Dependencies
6676
- node_modules/
6677
- .pnpm-store/
6678
-
6679
- # Build outputs
6680
- dist/
6681
- .adk/
6682
-
6683
- # Environment files
6684
- .env
6685
- .env.local
6686
- .env.production
6687
-
6688
- # IDE files
6689
- .vscode/
6690
- .idea/
6691
- *.swp
6692
- *.swo
6693
-
6694
- # OS files
6695
- .DS_Store
6696
- Thumbs.db
6697
-
6698
- # Logs
6699
- *.log
6700
- logs/
6701
-
6702
- # Runtime files
6703
- *.pid
6704
- *.seed
6705
- *.pid.lock
6706
- `;
6707
- this.writeFile(".gitignore", gitIgnore);
6708
- }
6709
- async createReadme() {
6710
- const installCommand = this.packageManager === "npm" ? "npm install" : this.packageManager === "yarn" ? "yarn install" : `${this.packageManager} install`;
6711
- const readme = `# ${this.projectName}
6712
-
6713
- A Botpress Agent built with the ADK.
6714
-
6715
- ## Getting Started
6716
-
6717
- 1. Install dependencies:
6718
- \`\`\`bash
6719
- ${installCommand}
6720
- \`\`\`
6721
-
6722
- 2. Start development server:
6723
- \`\`\`bash
6724
- adk dev
6725
- \`\`\`
6726
-
6727
- 3. Deploy your agent:
6728
- \`\`\`bash
6729
- adk deploy
6730
- \`\`\`
6731
-
6732
- ## Project Structure
6733
-
6734
- - \`src/actions/\` - Define callable functions
6735
- - \`src/workflows/\` - Define long-running processes
6736
- - \`src/conversations/\` - Define conversation handlers
6737
- - \`src/tables/\` - Define data storage schemas
6738
- - \`src/triggers/\` - Define event subscriptions
6739
- - \`src/knowledge/\` - Add knowledge base files
6740
-
6741
- ## Learn More
5611
+ ## Learn More
6742
5612
 
6743
5613
  - [ADK Documentation](https://botpress.com/docs/adk)
6744
5614
  - [Botpress Platform](https://botpress.com)
6745
5615
  `;
6746
5616
  await this.writeFormattedFile("README.md", readme);
6747
5617
  }
6748
- createClaudeMd() {
6749
- this.writeFile("CLAUDE.md", CLAUDE_template_default);
5618
+ createAIAssistantInstructions() {
5619
+ const content = ai_assistant_instructions_template_default;
5620
+ this.writeFile("CLAUDE.md", content);
5621
+ this.writeFile("AGENTS.md", content);
6750
5622
  }
6751
5623
  async createSourceStructure() {
6752
5624
  const srcPath = path15.join(this.projectPath, "src");
@@ -12443,158 +11315,1239 @@ class ScriptRunner {
12443
11315
  for (const [key, value] of Object.entries(envVars)) {
12444
11316
  process.env[key] = value;
12445
11317
  }
12446
- const runtimePath = path41.join(botPath, "src", "index.ts");
12447
- return {
12448
- botPath,
12449
- runtimePath,
12450
- botId,
12451
- workspaceId,
12452
- isProd: this.prod,
12453
- project,
12454
- initialize: async () => {
12455
- const botModule = await import(runtimePath);
12456
- const runtimeModule = await import("@botpress/runtime/runtime");
12457
- const { Autonomous } = await import("@botpress/runtime");
12458
- const { context, agentRegistry } = runtimeModule;
12459
- const { Client: Client18 } = await import("@botpress/client");
12460
- const { BotSpecificClient, BotLogger } = await import("@botpress/sdk");
12461
- const { Cognitive } = await import("@botpress/cognitive");
12462
- const vanillaClient = new Client18({
12463
- token: this.credentials.token,
12464
- apiUrl: this.credentials.apiUrl,
12465
- botId
12466
- });
12467
- const client = new BotSpecificClient(vanillaClient);
12468
- const cognitive = new Cognitive({
12469
- client,
12470
- __experimental_beta: true
12471
- });
12472
- const logger = new BotLogger({});
12473
- context.setDefaultContext({
12474
- executionId: "test-execution",
12475
- executionFinished: false,
12476
- botId,
12477
- client,
12478
- cognitive,
12479
- citations: new Autonomous.CitationsManager,
12480
- logger,
12481
- configuration: configuration ?? {},
12482
- integrations: agentRegistry.integrations,
12483
- interfaces: agentRegistry.interfaces,
12484
- states: [],
12485
- tags: [],
12486
- scheduledHeavyImports: new Set
12487
- });
12488
- return botModule.default;
11318
+ const runtimePath = path41.join(botPath, "src", "index.ts");
11319
+ return {
11320
+ botPath,
11321
+ runtimePath,
11322
+ botId,
11323
+ workspaceId,
11324
+ isProd: this.prod,
11325
+ project,
11326
+ initialize: async () => {
11327
+ const botModule = await import(runtimePath);
11328
+ const runtimeModule = await import("@botpress/runtime/runtime");
11329
+ const { Autonomous } = await import("@botpress/runtime");
11330
+ const { context, agentRegistry } = runtimeModule;
11331
+ const { Client: Client18 } = await import("@botpress/client");
11332
+ const { BotSpecificClient, BotLogger } = await import("@botpress/sdk");
11333
+ const { Cognitive } = await import("@botpress/cognitive");
11334
+ const vanillaClient = new Client18({
11335
+ token: this.credentials.token,
11336
+ apiUrl: this.credentials.apiUrl,
11337
+ botId
11338
+ });
11339
+ const client = new BotSpecificClient(vanillaClient);
11340
+ const cognitive = new Cognitive({
11341
+ client,
11342
+ __experimental_beta: true
11343
+ });
11344
+ const logger = new BotLogger({});
11345
+ context.setDefaultContext({
11346
+ executionId: "test-execution",
11347
+ executionFinished: false,
11348
+ botId,
11349
+ client,
11350
+ cognitive,
11351
+ citations: new Autonomous.CitationsManager,
11352
+ logger,
11353
+ configuration: configuration ?? {},
11354
+ integrations: agentRegistry.integrations,
11355
+ interfaces: agentRegistry.interfaces,
11356
+ states: [],
11357
+ tags: [],
11358
+ scheduledHeavyImports: new Set
11359
+ });
11360
+ return botModule.default;
11361
+ }
11362
+ };
11363
+ }
11364
+ async run(scriptPath, options = {}) {
11365
+ const { botPath, runnerPath, project } = await this.prepare();
11366
+ const absoluteScriptPath = path41.isAbsolute(scriptPath) ? scriptPath : path41.resolve(this.projectPath, scriptPath);
11367
+ if (!existsSync10(absoluteScriptPath)) {
11368
+ throw new Error(`Script not found: ${absoluteScriptPath}`);
11369
+ }
11370
+ const botId = this.prod ? project.agentInfo?.botId : project.agentInfo?.devId || project.agentInfo?.botId;
11371
+ const workspaceId = project.agentInfo?.workspaceId;
11372
+ if (!botId) {
11373
+ const idType = this.prod ? "botId" : "devId";
11374
+ throw new Error(`No ${idType} found in agent.json. ` + (this.prod ? 'Please deploy your agent first with "adk deploy".' : 'Please run "adk dev" first to create a development bot, or use --prod to use the production bot.'));
11375
+ }
11376
+ const args = ["run", runnerPath, absoluteScriptPath, ...options.args || []];
11377
+ let configuration;
11378
+ try {
11379
+ const manager3 = new ConfigManager(botId);
11380
+ configuration = await manager3.getAll();
11381
+ } catch {}
11382
+ const env = {
11383
+ ...process.env,
11384
+ ADK_PROJECT_PATH: this.projectPath,
11385
+ ADK_BOT_PATH: botPath,
11386
+ ADK_BOT_ID: botId,
11387
+ ADK_WORKSPACE_ID: workspaceId || "",
11388
+ ADK_IS_PROD: this.prod ? "true" : "false",
11389
+ BP_DISABLE_WORKER_MODE: "true",
11390
+ ...options.env,
11391
+ ADK_SCRIPT_MODE: "true",
11392
+ ADK_SCRIPT_PATH: absoluteScriptPath,
11393
+ ADK_TOKEN: this.credentials.token,
11394
+ ADK_API_URL: this.credentials.apiUrl,
11395
+ ...configuration && { ADK_CONFIGURATION: JSON.stringify(configuration) }
11396
+ };
11397
+ return new Promise((resolve3, reject) => {
11398
+ const child = spawn("bun", args, {
11399
+ cwd: botPath,
11400
+ env,
11401
+ stdio: options.inheritStdio !== false ? "inherit" : "pipe"
11402
+ });
11403
+ child.on("error", (error) => {
11404
+ reject(error);
11405
+ });
11406
+ child.on("close", (code) => {
11407
+ resolve3(code ?? 0);
11408
+ });
11409
+ });
11410
+ }
11411
+ }
11412
+ async function runScript(options) {
11413
+ const runner = new ScriptRunner({
11414
+ projectPath: options.projectPath,
11415
+ forceRegenerate: options.forceRegenerate,
11416
+ prod: options.prod,
11417
+ credentials: options.credentials
11418
+ });
11419
+ return runner.run(options.scriptPath, {
11420
+ args: options.args,
11421
+ env: options.env,
11422
+ inheritStdio: options.inheritStdio
11423
+ });
11424
+ }
11425
+ async function setupTestRuntime(options = {}) {
11426
+ let projectPath = options.projectPath;
11427
+ if (!projectPath) {
11428
+ const detected = await findAgentRoot(process.cwd());
11429
+ if (!detected) {
11430
+ throw new Error(`Could not find ADK agent project. No agent.config.ts found in current directory or parents.
11431
+ Either run from within an agent project directory, or provide projectPath explicitly.`);
11432
+ }
11433
+ projectPath = detected;
11434
+ }
11435
+ let credentials = options.credentials;
11436
+ if (!credentials) {
11437
+ const credentialsManager = new CredentialsManager;
11438
+ const loadedCredentials = await credentialsManager.getCredentials();
11439
+ if (!loadedCredentials) {
11440
+ throw new Error('No credentials found. Please run "adk login" first, or provide credentials explicitly.');
11441
+ }
11442
+ credentials = {
11443
+ token: loadedCredentials.token,
11444
+ apiUrl: loadedCredentials.apiUrl
11445
+ };
11446
+ }
11447
+ const runner = new ScriptRunner({
11448
+ projectPath,
11449
+ forceRegenerate: options.forceRegenerate,
11450
+ prod: options.prod,
11451
+ credentials
11452
+ });
11453
+ return runner.setupTestRuntime({ env: options.env });
11454
+ }
11455
+ // src/eval/types.ts
11456
+ function defineEval(def) {
11457
+ return def;
11458
+ }
11459
+ // src/eval/loader.ts
11460
+ import { readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
11461
+ import { resolve as resolve3 } from "path";
11462
+ async function loadEvalFile(filePath) {
11463
+ const absPath = resolve3(filePath);
11464
+ const mod = await import(absPath);
11465
+ const def = mod.default;
11466
+ if (!def || typeof def !== "object" || !def.name || !Array.isArray(def.conversation)) {
11467
+ throw new Error(`Invalid eval file ${filePath}: must export default a defineEval({...}) object with name and conversation`);
11468
+ }
11469
+ return def;
11470
+ }
11471
+ async function loadEvalsFromDir(dirPath) {
11472
+ const absDir = resolve3(dirPath);
11473
+ if (!existsSync11(absDir)) {
11474
+ return [];
11475
+ }
11476
+ const files = readdirSync3(absDir).filter((f) => f.endsWith(".eval.ts"));
11477
+ const evals = [];
11478
+ for (const f of files) {
11479
+ evals.push(await loadEvalFile(`${absDir}/${f}`));
11480
+ }
11481
+ return evals;
11482
+ }
11483
+ async function loadEvalByName(dirPath, name) {
11484
+ const absDir = resolve3(dirPath);
11485
+ if (!existsSync11(absDir))
11486
+ return null;
11487
+ const files = readdirSync3(absDir).filter((f) => f.endsWith(".eval.ts"));
11488
+ for (const f of files) {
11489
+ const def = await loadEvalFile(`${absDir}/${f}`);
11490
+ if (def.name === name)
11491
+ return def;
11492
+ }
11493
+ return null;
11494
+ }
11495
+ function filterEvals(evals, filter) {
11496
+ if (!filter)
11497
+ return evals;
11498
+ return evals.filter((e) => {
11499
+ if (filter.names && filter.names.length > 0) {
11500
+ if (!filter.names.includes(e.name))
11501
+ return false;
11502
+ }
11503
+ if (filter.tags && filter.tags.length > 0) {
11504
+ if (!e.tags || !filter.tags.some((t) => e.tags.includes(t)))
11505
+ return false;
11506
+ }
11507
+ if (filter.type) {
11508
+ if (e.type !== filter.type)
11509
+ return false;
11510
+ }
11511
+ return true;
11512
+ });
11513
+ }
11514
+ // src/eval/runner.ts
11515
+ import { Client as BpClient2 } from "@botpress/client";
11516
+
11517
+ // src/eval/client.ts
11518
+ import { Client as BpClient } from "@botpress/client";
11519
+ import { Client as ChatClient } from "@botpress/chat";
11520
+
11521
+ class ChatSession {
11522
+ webhookId;
11523
+ client = null;
11524
+ conversationId = null;
11525
+ constructor(webhookId) {
11526
+ this.webhookId = webhookId;
11527
+ }
11528
+ async connect() {
11529
+ this.client = await ChatClient.connect({ webhookId: this.webhookId });
11530
+ }
11531
+ get userId() {
11532
+ if (!this.client) {
11533
+ throw new Error("ChatSession not connected. Call connect() first.");
11534
+ }
11535
+ return this.client.user.id;
11536
+ }
11537
+ async sendMessage(message, options = {}) {
11538
+ if (!this.client) {
11539
+ throw new Error("ChatSession not connected. Call connect() first.");
11540
+ }
11541
+ const { timeout = 30000, idleTimeout = 3000 } = options;
11542
+ if (!this.conversationId) {
11543
+ const conv = await this.client.createConversation({});
11544
+ this.conversationId = conv.conversation.id;
11545
+ }
11546
+ const conversationId = this.conversationId;
11547
+ const responses = [];
11548
+ const listener = await this.client.listenConversation({
11549
+ id: conversationId
11550
+ });
11551
+ return new Promise((resolve4, reject) => {
11552
+ let idleTimer = null;
11553
+ let resolved = false;
11554
+ const done = () => {
11555
+ if (resolved)
11556
+ return;
11557
+ resolved = true;
11558
+ if (idleTimer)
11559
+ clearTimeout(idleTimer);
11560
+ clearTimeout(overallTimer);
11561
+ resolve4({ conversationId, responses });
11562
+ };
11563
+ const resetIdle = () => {
11564
+ if (idleTimer)
11565
+ clearTimeout(idleTimer);
11566
+ idleTimer = setTimeout(done, idleTimeout);
11567
+ };
11568
+ const overallTimer = setTimeout(() => {
11569
+ if (!resolved) {
11570
+ if (responses.length > 0) {
11571
+ done();
11572
+ } else {
11573
+ resolved = true;
11574
+ reject(new Error(`Timed out after ${timeout}ms with no bot response.`));
11575
+ }
11576
+ }
11577
+ }, timeout);
11578
+ listener.on("message_created", (event) => {
11579
+ if (resolved)
11580
+ return;
11581
+ if (event.isBot) {
11582
+ const payload = event?.payload;
11583
+ const text = typeof payload === "string" ? payload : payload?.text || JSON.stringify(payload);
11584
+ responses.push({ text, raw: payload });
11585
+ resetIdle();
11586
+ }
11587
+ });
11588
+ listener.on("event_created", (event) => {
11589
+ if (event?.payload?.done) {
11590
+ done();
11591
+ }
11592
+ });
11593
+ this.client.createMessage({
11594
+ conversationId,
11595
+ payload: { type: "text", text: message }
11596
+ }).then(() => {
11597
+ resetIdle();
11598
+ }).catch((err) => {
11599
+ clearTimeout(overallTimer);
11600
+ if (!resolved) {
11601
+ resolved = true;
11602
+ reject(err);
11603
+ }
11604
+ });
11605
+ });
11606
+ }
11607
+ }
11608
+ async function discoverWebhookId(botId, token, apiUrl) {
11609
+ const client = new BpClient({ token, botId, apiUrl });
11610
+ const { bot } = await client.getBot({ id: botId });
11611
+ const integrations = bot.integrations || {};
11612
+ const chat = Object.values(integrations).find((int) => int.name === "chat");
11613
+ const webhookId = chat?.webhookId;
11614
+ if (!webhookId) {
11615
+ throw new Error("No chat integration found on bot. Make sure the bot has the chat integration enabled.");
11616
+ }
11617
+ return webhookId;
11618
+ }
11619
+
11620
+ // src/eval/traces.ts
11621
+ async function fetchTraceSpans(conversationId, devServerUrl) {
11622
+ const url = `${devServerUrl}/api/traces/query?attributeName=conversationId&attributeValue=${encodeURIComponent(conversationId)}&count=1000`;
11623
+ const res = await fetch(url);
11624
+ if (!res.ok) {
11625
+ throw new Error(`Failed to fetch traces: ${res.status} ${res.statusText}`);
11626
+ }
11627
+ const data = await res.json();
11628
+ return Array.isArray(data) ? data : data.spans || [];
11629
+ }
11630
+ function extractToolCalls(spans) {
11631
+ const toolEndSpans = spans.filter((span) => span.name === "autonomous.tool" && span.t === "end" && span.attrs?.["autonomous.tool.name"]);
11632
+ const seen = new Set;
11633
+ const unique = toolEndSpans.filter((span) => {
11634
+ if (seen.has(span.spanId))
11635
+ return false;
11636
+ seen.add(span.spanId);
11637
+ return true;
11638
+ });
11639
+ return unique.sort((a, b) => (a.endNs ?? 0) - (b.endNs ?? 0)).map((span) => {
11640
+ const attrs = span.attrs;
11641
+ let input = {};
11642
+ try {
11643
+ input = JSON.parse(attrs["autonomous.tool.input"]);
11644
+ } catch {}
11645
+ return {
11646
+ name: attrs["autonomous.tool.name"],
11647
+ input,
11648
+ output: attrs["autonomous.tool.output"] || "",
11649
+ status: attrs["autonomous.tool.status"] || "unknown"
11650
+ };
11651
+ });
11652
+ }
11653
+ async function getTraceData(conversationId, devServerUrl, options = {}) {
11654
+ const previousCount = options.previousToolCallCount || 0;
11655
+ const expectNew = options.expectNewCalls ?? false;
11656
+ const maxRetries = expectNew ? 5 : 0;
11657
+ const retryDelay = 500;
11658
+ let allToolCalls = [];
11659
+ let spans = [];
11660
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
11661
+ if (attempt > 0) {
11662
+ await new Promise((resolve4) => setTimeout(resolve4, retryDelay));
11663
+ }
11664
+ spans = await fetchTraceSpans(conversationId, devServerUrl);
11665
+ allToolCalls = extractToolCalls(spans);
11666
+ if (!expectNew || allToolCalls.length > previousCount) {
11667
+ break;
11668
+ }
11669
+ }
11670
+ const toolCalls = allToolCalls.slice(previousCount);
11671
+ return { toolCalls, totalToolCallCount: allToolCalls.length, raw: spans };
11672
+ }
11673
+
11674
+ // src/eval/graders/llm.ts
11675
+ import { Cognitive } from "@botpress/cognitive";
11676
+ import { Client as Client18 } from "@botpress/client";
11677
+ var JUDGE_SYSTEM_PROMPT = `You are an evaluation judge for a chatbot. You will be given:
11678
+ - The user's message
11679
+ - The bot's response
11680
+ - Grading criteria
11681
+
11682
+ Your job is to determine if the bot's response meets the criteria.
11683
+
11684
+ Respond with a JSON object:
11685
+ {
11686
+ "pass": true/false,
11687
+ "score": 1-5,
11688
+ "reason": "brief explanation"
11689
+ }
11690
+
11691
+ Scoring guide:
11692
+ - 5: Fully meets criteria, excellent response
11693
+ - 4: Mostly meets criteria, minor issues
11694
+ - 3: Partially meets criteria, notable gaps
11695
+ - 2: Barely meets criteria, significant issues
11696
+ - 1: Does not meet criteria
11697
+
11698
+ A score of 3 or above is a pass.`;
11699
+ var _cognitive = null;
11700
+ function getCognitive() {
11701
+ if (_cognitive)
11702
+ return _cognitive;
11703
+ const token = process.env.BP_TOKEN || process.env.ADK_TOKEN;
11704
+ const botId = process.env.ADK_BOT_ID;
11705
+ const apiUrl = process.env.ADK_API_URL || "https://api.botpress.cloud";
11706
+ if (!token || !botId)
11707
+ return null;
11708
+ const client = new Client18({ token, apiUrl, botId });
11709
+ _cognitive = new Cognitive({ client, __experimental_beta: true });
11710
+ return _cognitive;
11711
+ }
11712
+ function initLLMJudge(credentials) {
11713
+ const client = new Client18({
11714
+ token: credentials.token,
11715
+ apiUrl: credentials.apiUrl,
11716
+ botId: credentials.botId
11717
+ });
11718
+ _cognitive = new Cognitive({ client, __experimental_beta: true });
11719
+ }
11720
+ async function gradeLLMJudge(botResponse, criteria, context) {
11721
+ try {
11722
+ const cognitive = getCognitive();
11723
+ if (!cognitive) {
11724
+ return {
11725
+ assertion: `llm_judge: "${criteria}"`,
11726
+ pass: true,
11727
+ expected: criteria,
11728
+ actual: "SKIPPED — LLM judge unavailable: no credentials configured"
11729
+ };
11730
+ }
11731
+ const { output } = await cognitive.generateContent({
11732
+ model: "fast",
11733
+ temperature: 0,
11734
+ responseFormat: "json_object",
11735
+ systemPrompt: JUDGE_SYSTEM_PROMPT,
11736
+ messages: [
11737
+ {
11738
+ role: "user",
11739
+ content: `User message: ${context.userMessage}
11740
+
11741
+ Bot response: ${botResponse}
11742
+
11743
+ Criteria: ${criteria}`
11744
+ }
11745
+ ]
11746
+ });
11747
+ const rawContent = output?.choices?.[0]?.content;
11748
+ const content = typeof rawContent === "string" ? rawContent : null;
11749
+ if (!content) {
11750
+ return {
11751
+ assertion: `llm_judge: "${criteria}"`,
11752
+ pass: true,
11753
+ expected: criteria,
11754
+ actual: "SKIPPED — LLM judge returned empty response"
11755
+ };
11756
+ }
11757
+ const verdict = JSON.parse(content);
11758
+ return {
11759
+ assertion: `llm_judge: "${criteria}"`,
11760
+ pass: verdict.score >= 3,
11761
+ expected: criteria,
11762
+ actual: `Score ${verdict.score}/5 — ${verdict.reason}`
11763
+ };
11764
+ } catch (err) {
11765
+ return {
11766
+ assertion: `llm_judge: "${criteria}"`,
11767
+ pass: true,
11768
+ expected: criteria,
11769
+ actual: `SKIPPED — LLM judge unavailable: ${err.message}`
11770
+ };
11771
+ }
11772
+ }
11773
+
11774
+ // src/eval/graders/response.ts
11775
+ async function gradeResponse(botResponse, assertions, context) {
11776
+ const results = [];
11777
+ for (const assertion of assertions) {
11778
+ if ("contains" in assertion) {
11779
+ const pass = botResponse.toLowerCase().includes(assertion.contains.toLowerCase());
11780
+ results.push({
11781
+ assertion: `contains "${assertion.contains}"`,
11782
+ pass,
11783
+ expected: `Response contains "${assertion.contains}"`,
11784
+ actual: pass ? `Found in response` : `Not found in response`
11785
+ });
11786
+ continue;
11787
+ }
11788
+ if ("not_contains" in assertion) {
11789
+ const pass = !botResponse.toLowerCase().includes(assertion.not_contains.toLowerCase());
11790
+ results.push({
11791
+ assertion: `not_contains "${assertion.not_contains}"`,
11792
+ pass,
11793
+ expected: `Response does not contain "${assertion.not_contains}"`,
11794
+ actual: pass ? `Not found in response` : `Found in response`
11795
+ });
11796
+ continue;
11797
+ }
11798
+ if ("matches" in assertion) {
11799
+ const regex = new RegExp(assertion.matches, "i");
11800
+ const pass = regex.test(botResponse);
11801
+ results.push({
11802
+ assertion: `matches ${assertion.matches}`,
11803
+ pass,
11804
+ expected: `Response matches /${assertion.matches}/`,
11805
+ actual: pass ? `Matched` : `No match`
11806
+ });
11807
+ continue;
11808
+ }
11809
+ if ("llm_judge" in assertion) {
11810
+ const result = await gradeLLMJudge(botResponse, assertion.llm_judge, context);
11811
+ results.push(result);
11812
+ continue;
11813
+ }
11814
+ if ("similar_to" in assertion) {
11815
+ results.push({
11816
+ assertion: `similar_to: "${assertion.similar_to}"`,
11817
+ pass: true,
11818
+ expected: assertion.similar_to,
11819
+ actual: "SKIPPED — similar_to not yet implemented"
11820
+ });
11821
+ continue;
11822
+ }
11823
+ results.push({
11824
+ assertion: "unknown",
11825
+ pass: false,
11826
+ expected: "known assertion type",
11827
+ actual: `Unknown assertion: ${JSON.stringify(assertion)}`
11828
+ });
11829
+ }
11830
+ return results;
11831
+ }
11832
+
11833
+ // src/eval/graders/match.ts
11834
+ function matchValue(operator, actual) {
11835
+ if (typeof operator === "string") {
11836
+ return String(actual) === operator;
11837
+ }
11838
+ if ("equals" in operator) {
11839
+ return actual === operator.equals;
11840
+ }
11841
+ if ("contains" in operator) {
11842
+ return String(actual).toLowerCase().includes(operator.contains.toLowerCase());
11843
+ }
11844
+ if ("not_contains" in operator) {
11845
+ return !String(actual).toLowerCase().includes(operator.not_contains.toLowerCase());
11846
+ }
11847
+ if ("matches" in operator) {
11848
+ return new RegExp(operator.matches, "i").test(String(actual));
11849
+ }
11850
+ if ("in" in operator) {
11851
+ return operator.in.includes(actual);
11852
+ }
11853
+ if ("exists" in operator) {
11854
+ return operator.exists ? actual !== undefined && actual !== null : actual === undefined || actual === null;
11855
+ }
11856
+ if ("gte" in operator) {
11857
+ return Number(actual) >= operator.gte;
11858
+ }
11859
+ if ("lte" in operator) {
11860
+ return Number(actual) <= operator.lte;
11861
+ }
11862
+ return false;
11863
+ }
11864
+ function operatorToString(operator) {
11865
+ if (typeof operator === "string")
11866
+ return `equals "${operator}"`;
11867
+ if ("equals" in operator)
11868
+ return `equals ${JSON.stringify(operator.equals)}`;
11869
+ if ("contains" in operator)
11870
+ return `contains "${operator.contains}"`;
11871
+ if ("not_contains" in operator)
11872
+ return `not_contains "${operator.not_contains}"`;
11873
+ if ("matches" in operator)
11874
+ return `matches /${operator.matches}/`;
11875
+ if ("in" in operator)
11876
+ return `in [${operator.in.map((v) => JSON.stringify(v)).join(", ")}]`;
11877
+ if ("exists" in operator)
11878
+ return operator.exists ? "exists" : "does not exist";
11879
+ if ("gte" in operator)
11880
+ return `>= ${operator.gte}`;
11881
+ if ("lte" in operator)
11882
+ return `<= ${operator.lte}`;
11883
+ return JSON.stringify(operator);
11884
+ }
11885
+
11886
+ // src/eval/graders/tools.ts
11887
+ function gradeTools(toolCalls, assertions) {
11888
+ return assertions.map((assertion) => {
11889
+ if ("called" in assertion && !("not_called" in assertion) && !("call_order" in assertion)) {
11890
+ const matches = toolCalls.filter((tc) => tc.name === assertion.called);
11891
+ const wasCalled = matches.length > 0;
11892
+ if (!wasCalled) {
11893
+ return {
11894
+ assertion: `tool called: ${assertion.called}`,
11895
+ pass: false,
11896
+ expected: `${assertion.called} was called`,
11897
+ actual: `Not called. Tools called: [${toolCalls.map((tc) => tc.name).join(", ") || "none"}]`
11898
+ };
11899
+ }
11900
+ if (assertion.params) {
11901
+ const paramResults = [];
11902
+ for (const [key, operator] of Object.entries(assertion.params)) {
11903
+ const anyMatch = matches.some((tc) => matchValue(operator, tc.input[key]));
11904
+ paramResults.push({
11905
+ key,
11906
+ pass: anyMatch,
11907
+ detail: anyMatch ? `matched` : `expected ${key} ${operatorToString(operator)}, got ${JSON.stringify(matches.map((tc) => tc.input[key]))}`
11908
+ });
11909
+ }
11910
+ const allParamsPass = paramResults.every((p) => p.pass);
11911
+ const failedParams = paramResults.filter((p) => !p.pass);
11912
+ return {
11913
+ assertion: `tool called: ${assertion.called} with params`,
11914
+ pass: allParamsPass,
11915
+ expected: `${assertion.called} called with ${Object.entries(assertion.params).map(([k, v]) => `${k} ${operatorToString(v)}`).join(", ")}`,
11916
+ actual: allParamsPass ? `Matched` : failedParams.map((p) => p.detail).join("; ")
11917
+ };
11918
+ }
11919
+ return {
11920
+ assertion: `tool called: ${assertion.called}`,
11921
+ pass: true,
11922
+ expected: `${assertion.called} was called`,
11923
+ actual: `Called ${matches.length} time(s)`
11924
+ };
11925
+ }
11926
+ if ("not_called" in assertion) {
11927
+ const wasCalled = toolCalls.some((tc) => tc.name === assertion.not_called);
11928
+ return {
11929
+ assertion: `tool not_called: ${assertion.not_called}`,
11930
+ pass: !wasCalled,
11931
+ expected: `${assertion.not_called} was NOT called`,
11932
+ actual: wasCalled ? `Was called` : `Not called`
11933
+ };
11934
+ }
11935
+ if ("call_order" in assertion) {
11936
+ const calledNames = toolCalls.map((tc) => tc.name);
11937
+ const expectedOrder = assertion.call_order;
11938
+ let cursor = 0;
11939
+ for (const name of calledNames) {
11940
+ if (cursor < expectedOrder.length && name === expectedOrder[cursor]) {
11941
+ cursor++;
11942
+ }
12489
11943
  }
11944
+ const inOrder = cursor === expectedOrder.length;
11945
+ return {
11946
+ assertion: `call_order: [${expectedOrder.join(" → ")}]`,
11947
+ pass: inOrder,
11948
+ expected: `Tools called in order: [${expectedOrder.join(" → ")}]`,
11949
+ actual: `Actual order: [${calledNames.join(" → ") || "none"}]`
11950
+ };
11951
+ }
11952
+ return {
11953
+ assertion: "unknown tool assertion",
11954
+ pass: false,
11955
+ expected: "known assertion type",
11956
+ actual: `Unknown: ${JSON.stringify(assertion)}`
12490
11957
  };
11958
+ });
11959
+ }
11960
+
11961
+ // src/eval/graders/state.ts
11962
+ function parseStatePath(path42) {
11963
+ const dot = path42.indexOf(".");
11964
+ if (dot === -1) {
11965
+ throw new Error(`Invalid state path "${path42}" — expected "type.field" format`);
12491
11966
  }
12492
- async run(scriptPath, options = {}) {
12493
- const { botPath, runnerPath, project } = await this.prepare();
12494
- const absoluteScriptPath = path41.isAbsolute(scriptPath) ? scriptPath : path41.resolve(this.projectPath, scriptPath);
12495
- if (!existsSync10(absoluteScriptPath)) {
12496
- throw new Error(`Script not found: ${absoluteScriptPath}`);
11967
+ const prefix = path42.slice(0, dot);
11968
+ const field = path42.slice(dot + 1);
11969
+ switch (prefix) {
11970
+ case "bot":
11971
+ return { type: "bot", stateName: "botState", stateId: (ctx) => ctx.botId, field };
11972
+ case "user":
11973
+ return { type: "user", stateName: "userState", stateId: (ctx) => ctx.userId, field };
11974
+ case "conversation":
11975
+ return { type: "conversation", stateName: "conversationState", stateId: (ctx) => ctx.conversationId, field };
11976
+ default:
11977
+ throw new Error(`Unknown state type "${prefix}" in path "${path42}" — expected bot, user, or conversation`);
11978
+ }
11979
+ }
11980
+ async function fetchState(client, type, id, name) {
11981
+ try {
11982
+ const result = await client.getState({ type, id, name });
11983
+ const payload = result.state?.payload;
11984
+ return payload?.value ?? payload ?? null;
11985
+ } catch (err) {
11986
+ if (err?.code === 404 || err?.message?.includes("404") || err?.message?.includes("doesn't exist")) {
11987
+ return null;
12497
11988
  }
12498
- const botId = this.prod ? project.agentInfo?.botId : project.agentInfo?.devId || project.agentInfo?.botId;
12499
- const workspaceId = project.agentInfo?.workspaceId;
12500
- if (!botId) {
12501
- const idType = this.prod ? "botId" : "devId";
12502
- throw new Error(`No ${idType} found in agent.json. ` + (this.prod ? 'Please deploy your agent first with "adk deploy".' : 'Please run "adk dev" first to create a development bot, or use --prod to use the production bot.'));
11989
+ throw err;
11990
+ }
11991
+ }
11992
+ async function fetchStateWithRetry(client, type, id, name, field, maxRetries = 3, retryDelay = 1000) {
11993
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
11994
+ const payload = await fetchState(client, type, id, name);
11995
+ if (payload && payload[field] !== undefined) {
11996
+ return payload;
12503
11997
  }
12504
- const args = ["run", runnerPath, absoluteScriptPath, ...options.args || []];
12505
- let configuration;
11998
+ if (attempt < maxRetries) {
11999
+ await new Promise((resolve4) => setTimeout(resolve4, retryDelay));
12000
+ }
12001
+ }
12002
+ return null;
12003
+ }
12004
+ async function snapshotState(client, assertions, ctx) {
12005
+ const snapshots = new Map;
12006
+ for (const assertion of assertions) {
12007
+ if (assertion.changed === undefined)
12008
+ continue;
12009
+ const parsed = parseStatePath(assertion.path);
12010
+ if (parsed.type === "conversation")
12011
+ continue;
12012
+ const payload = await fetchState(client, parsed.type, parsed.stateId(ctx), parsed.stateName);
12013
+ snapshots.set(assertion.path, payload ? payload[parsed.field] : undefined);
12014
+ }
12015
+ return snapshots;
12016
+ }
12017
+ async function gradeState(client, assertions, ctx, preSnapshots) {
12018
+ const results = [];
12019
+ for (const assertion of assertions) {
12020
+ const parsed = parseStatePath(assertion.path);
12021
+ const stateId = parsed.stateId(ctx);
12022
+ let payload;
12506
12023
  try {
12507
- const manager3 = new ConfigManager(botId);
12508
- configuration = await manager3.getAll();
12509
- } catch {}
12510
- const env = {
12511
- ...process.env,
12512
- ADK_PROJECT_PATH: this.projectPath,
12513
- ADK_BOT_PATH: botPath,
12514
- ADK_BOT_ID: botId,
12515
- ADK_WORKSPACE_ID: workspaceId || "",
12516
- ADK_IS_PROD: this.prod ? "true" : "false",
12517
- BP_DISABLE_WORKER_MODE: "true",
12518
- ...options.env,
12519
- ADK_SCRIPT_MODE: "true",
12520
- ADK_SCRIPT_PATH: absoluteScriptPath,
12521
- ADK_TOKEN: this.credentials.token,
12522
- ADK_API_URL: this.credentials.apiUrl,
12523
- ...configuration && { ADK_CONFIGURATION: JSON.stringify(configuration) }
12524
- };
12525
- return new Promise((resolve3, reject) => {
12526
- const child = spawn("bun", args, {
12527
- cwd: botPath,
12528
- env,
12529
- stdio: options.inheritStdio !== false ? "inherit" : "pipe"
12024
+ payload = await fetchStateWithRetry(client, parsed.type, stateId, parsed.stateName, parsed.field);
12025
+ } catch (err) {
12026
+ results.push({
12027
+ assertion: `state: ${assertion.path}`,
12028
+ pass: false,
12029
+ expected: `Fetch state for ${assertion.path}`,
12030
+ actual: `Error fetching state: ${err.message}`
12530
12031
  });
12531
- child.on("error", (error) => {
12532
- reject(error);
12032
+ continue;
12033
+ }
12034
+ const actualValue = payload ? payload[parsed.field] : undefined;
12035
+ if (assertion.equals !== undefined) {
12036
+ const pass = deepEqual(actualValue, assertion.equals);
12037
+ results.push({
12038
+ assertion: `state: ${assertion.path} equals`,
12039
+ pass,
12040
+ expected: JSON.stringify(assertion.equals),
12041
+ actual: JSON.stringify(actualValue)
12533
12042
  });
12534
- child.on("close", (code) => {
12535
- resolve3(code ?? 0);
12043
+ }
12044
+ if (assertion.changed !== undefined) {
12045
+ const preValue = preSnapshots?.get(assertion.path);
12046
+ const didChange = !deepEqual(actualValue, preValue);
12047
+ const pass = assertion.changed ? didChange : !didChange;
12048
+ results.push({
12049
+ assertion: `state: ${assertion.path} ${assertion.changed ? "changed" : "unchanged"}`,
12050
+ pass,
12051
+ expected: assertion.changed ? `Value changed from ${JSON.stringify(preValue)}` : `Value unchanged from ${JSON.stringify(preValue)}`,
12052
+ actual: didChange ? `Changed to ${JSON.stringify(actualValue)}` : `Unchanged: ${JSON.stringify(actualValue)}`
12536
12053
  });
12054
+ }
12055
+ }
12056
+ return results;
12057
+ }
12058
+ function deepEqual(a, b) {
12059
+ if (a === b)
12060
+ return true;
12061
+ if (a === null || b === null)
12062
+ return false;
12063
+ if (typeof a !== typeof b)
12064
+ return false;
12065
+ if (Array.isArray(a) && Array.isArray(b)) {
12066
+ if (a.length !== b.length)
12067
+ return false;
12068
+ return a.every((v, i) => deepEqual(v, b[i]));
12069
+ }
12070
+ if (typeof a === "object" && typeof b === "object") {
12071
+ const keysA = Object.keys(a);
12072
+ const keysB = Object.keys(b);
12073
+ if (keysA.length !== keysB.length)
12074
+ return false;
12075
+ return keysA.every((k) => deepEqual(a[k], b[k]));
12076
+ }
12077
+ return false;
12078
+ }
12079
+
12080
+ // src/eval/graders/tables.ts
12081
+ function buildFilter(conditions) {
12082
+ const filter = {};
12083
+ for (const [column, op] of Object.entries(conditions)) {
12084
+ if (typeof op === "string") {
12085
+ filter[column] = op;
12086
+ } else if ("equals" in op) {
12087
+ filter[column] = op.equals;
12088
+ } else if ("contains" in op) {
12089
+ filter[column] = { $regex: op.contains };
12090
+ } else if ("matches" in op) {
12091
+ filter[column] = { $regex: op.matches };
12092
+ } else if ("gte" in op) {
12093
+ filter[column] = { $gte: op.gte };
12094
+ } else if ("lte" in op) {
12095
+ filter[column] = { $lte: op.lte };
12096
+ }
12097
+ }
12098
+ return filter;
12099
+ }
12100
+ async function gradeRowExists(client, assertion) {
12101
+ const conditionDesc = Object.entries(assertion.row_exists).map(([k, v]) => `${k} ${operatorToString(v)}`).join(", ");
12102
+ try {
12103
+ const filter = buildFilter(assertion.row_exists);
12104
+ const result = await client.findTableRows({
12105
+ table: assertion.table,
12106
+ filter,
12107
+ limit: 10
12537
12108
  });
12109
+ const rows = result.rows || [];
12110
+ const matchingRows = rows.filter((row) => {
12111
+ for (const [column, op] of Object.entries(assertion.row_exists)) {
12112
+ if (!matchValue(op, row[column]))
12113
+ return false;
12114
+ }
12115
+ return true;
12116
+ });
12117
+ const pass = matchingRows.length > 0;
12118
+ return {
12119
+ assertion: `table: ${assertion.table} row_exists`,
12120
+ pass,
12121
+ expected: `Row exists in ${assertion.table} where ${conditionDesc}`,
12122
+ actual: pass ? `Found ${matchingRows.length} matching row(s)` : `No matching rows found`
12123
+ };
12124
+ } catch (err) {
12125
+ return {
12126
+ assertion: `table: ${assertion.table} row_exists`,
12127
+ pass: false,
12128
+ expected: `Row exists in ${assertion.table} where ${conditionDesc}`,
12129
+ actual: `Error querying table: ${err.message}`
12130
+ };
12538
12131
  }
12539
12132
  }
12540
- async function runScript(options) {
12541
- const runner = new ScriptRunner({
12542
- projectPath: options.projectPath,
12543
- forceRegenerate: options.forceRegenerate,
12544
- prod: options.prod,
12545
- credentials: options.credentials
12546
- });
12547
- return runner.run(options.scriptPath, {
12548
- args: options.args,
12549
- env: options.env,
12550
- inheritStdio: options.inheritStdio
12551
- });
12133
+ async function gradeRowCount(client, assertion) {
12134
+ const countDesc = operatorToString(assertion.row_count);
12135
+ const whereDesc = assertion.where ? ` where ${Object.entries(assertion.where).map(([k, v]) => `${k} ${operatorToString(v)}`).join(", ")}` : "";
12136
+ try {
12137
+ const filter = assertion.where ? buildFilter(assertion.where) : {};
12138
+ const result = await client.findTableRows({
12139
+ table: assertion.table,
12140
+ filter,
12141
+ limit: 1000
12142
+ });
12143
+ const rows = result.rows || [];
12144
+ let count;
12145
+ if (assertion.where) {
12146
+ count = rows.filter((row) => {
12147
+ for (const [column, op] of Object.entries(assertion.where)) {
12148
+ if (!matchValue(op, row[column]))
12149
+ return false;
12150
+ }
12151
+ return true;
12152
+ }).length;
12153
+ } else {
12154
+ count = rows.length;
12155
+ }
12156
+ const pass = matchValue(assertion.row_count, count);
12157
+ return {
12158
+ assertion: `table: ${assertion.table} row_count`,
12159
+ pass,
12160
+ expected: `Row count in ${assertion.table}${whereDesc} ${countDesc}`,
12161
+ actual: `Count: ${count}`
12162
+ };
12163
+ } catch (err) {
12164
+ return {
12165
+ assertion: `table: ${assertion.table} row_count`,
12166
+ pass: false,
12167
+ expected: `Row count in ${assertion.table}${whereDesc} ${countDesc}`,
12168
+ actual: `Error querying table: ${err.message}`
12169
+ };
12170
+ }
12552
12171
  }
12553
- async function setupTestRuntime(options = {}) {
12554
- let projectPath = options.projectPath;
12555
- if (!projectPath) {
12556
- const detected = await findAgentRoot(process.cwd());
12557
- if (!detected) {
12558
- throw new Error(`Could not find ADK agent project. No agent.config.ts found in current directory or parents.
12559
- Either run from within an agent project directory, or provide projectPath explicitly.`);
12172
+ async function gradeTables(client, assertions) {
12173
+ const results = [];
12174
+ for (const assertion of assertions) {
12175
+ if ("row_exists" in assertion) {
12176
+ results.push(await gradeRowExists(client, assertion));
12177
+ } else if ("row_count" in assertion) {
12178
+ results.push(await gradeRowCount(client, assertion));
12560
12179
  }
12561
- projectPath = detected;
12562
12180
  }
12563
- let credentials = options.credentials;
12564
- if (!credentials) {
12565
- const credentialsManager = new CredentialsManager;
12566
- const loadedCredentials = await credentialsManager.getCredentials();
12567
- if (!loadedCredentials) {
12568
- throw new Error('No credentials found. Please run "adk login" first, or provide credentials explicitly.');
12181
+ return results;
12182
+ }
12183
+
12184
+ // src/eval/graders/workflow.ts
12185
+ function gradeWorkflows(spans, assertions) {
12186
+ const results = [];
12187
+ for (const assertion of assertions) {
12188
+ const workflowSpans = spans.filter((span) => {
12189
+ const wfName = span.attrs?.["workflow.name"] || span.attrs?.["workflowName"];
12190
+ return wfName === assertion.name;
12191
+ });
12192
+ if (assertion.entered !== undefined) {
12193
+ const wasEntered = workflowSpans.length > 0;
12194
+ const pass = assertion.entered ? wasEntered : !wasEntered;
12195
+ results.push({
12196
+ assertion: `workflow: ${assertion.name} ${assertion.entered ? "entered" : "not entered"}`,
12197
+ pass,
12198
+ expected: assertion.entered ? `Workflow "${assertion.name}" was entered` : `Workflow "${assertion.name}" was not entered`,
12199
+ actual: wasEntered ? `Found ${workflowSpans.length} workflow span(s)` : `No workflow spans found`
12200
+ });
12569
12201
  }
12570
- credentials = {
12571
- token: loadedCredentials.token,
12572
- apiUrl: loadedCredentials.apiUrl
12202
+ if (assertion.completed !== undefined) {
12203
+ const completedSpans = workflowSpans.filter((span) => {
12204
+ const status = span.attrs?.["workflow.status"];
12205
+ return status === "completed" || span.t === "end" && span.name?.includes("workflow");
12206
+ });
12207
+ const didComplete = completedSpans.length > 0;
12208
+ const pass = assertion.completed ? didComplete : !didComplete;
12209
+ results.push({
12210
+ assertion: `workflow: ${assertion.name} ${assertion.completed ? "completed" : "not completed"}`,
12211
+ pass,
12212
+ expected: assertion.completed ? `Workflow "${assertion.name}" completed` : `Workflow "${assertion.name}" did not complete`,
12213
+ actual: didComplete ? `Completed` : `Not completed`
12214
+ });
12215
+ }
12216
+ }
12217
+ return results;
12218
+ }
12219
+
12220
+ // src/eval/graders/outcome.ts
12221
+ async function snapshotOutcomeState(client, evalDef, ctx) {
12222
+ if (!evalDef.outcome?.state) {
12223
+ return new Map;
12224
+ }
12225
+ return snapshotState(client, evalDef.outcome.state, ctx);
12226
+ }
12227
+ async function gradeOutcome(client, evalDef, ctx, traceSpans, preSnapshots) {
12228
+ const outcome = evalDef.outcome;
12229
+ if (!outcome)
12230
+ return [];
12231
+ const results = [];
12232
+ if (outcome.state && outcome.state.length > 0) {
12233
+ const stateResults = await gradeState(client, outcome.state, ctx, preSnapshots);
12234
+ results.push(...stateResults);
12235
+ }
12236
+ if (outcome.tables && outcome.tables.length > 0) {
12237
+ const tableResults = await gradeTables(client, outcome.tables);
12238
+ results.push(...tableResults);
12239
+ }
12240
+ if (outcome.workflow && outcome.workflow.length > 0) {
12241
+ const workflowResults = gradeWorkflows(traceSpans, outcome.workflow);
12242
+ results.push(...workflowResults);
12243
+ }
12244
+ return results;
12245
+ }
12246
+
12247
+ // src/eval/runner.ts
12248
+ import { randomUUID } from "crypto";
12249
+ async function runEval(evalDef, connection, options = {}) {
12250
+ const devServerUrl = options.devServerUrl || "http://localhost:3001";
12251
+ const start = Date.now();
12252
+ const turns = [];
12253
+ let outcomeAssertions = [];
12254
+ try {
12255
+ const session = new ChatSession(connection.webhookId);
12256
+ await session.connect();
12257
+ let bpClient = null;
12258
+ const getBpClient = () => {
12259
+ if (!bpClient) {
12260
+ bpClient = new BpClient2({
12261
+ token: connection.token,
12262
+ botId: connection.botId,
12263
+ apiUrl: connection.apiUrl
12264
+ });
12265
+ }
12266
+ return bpClient;
12267
+ };
12268
+ let preSnapshots = new Map;
12269
+ if (evalDef.outcome?.state) {
12270
+ const ctx = {
12271
+ botId: connection.botId,
12272
+ userId: session.userId,
12273
+ conversationId: ""
12274
+ };
12275
+ preSnapshots = await snapshotOutcomeState(getBpClient(), evalDef, ctx);
12276
+ }
12277
+ let previousToolCallCount = 0;
12278
+ let lastConversationId = "";
12279
+ let allTraceSpans = [];
12280
+ for (let i = 0;i < evalDef.conversation.length; i++) {
12281
+ const turn = evalDef.conversation[i];
12282
+ const turnStart = Date.now();
12283
+ const result = await session.sendMessage(turn.user, {
12284
+ timeout: 30000,
12285
+ idleTimeout: 3000
12286
+ });
12287
+ const botDuration = Date.now() - turnStart;
12288
+ lastConversationId = result.conversationId;
12289
+ const botResponse = result.responses.map((r) => r.text).join(`
12290
+ `);
12291
+ const evalStart = Date.now();
12292
+ let assertions = [];
12293
+ if (turn.assert?.response) {
12294
+ assertions = await gradeResponse(botResponse, turn.assert.response, {
12295
+ userMessage: turn.user
12296
+ });
12297
+ }
12298
+ if (turn.assert?.tools) {
12299
+ try {
12300
+ const expectNewCalls = turn.assert.tools.some((a) => ("called" in a) || ("call_order" in a));
12301
+ const traceData = await getTraceData(result.conversationId, devServerUrl, {
12302
+ previousToolCallCount,
12303
+ expectNewCalls
12304
+ });
12305
+ previousToolCallCount = traceData.totalToolCallCount;
12306
+ allTraceSpans = traceData.raw;
12307
+ const toolResults = gradeTools(traceData.toolCalls, turn.assert.tools);
12308
+ assertions = [...assertions, ...toolResults];
12309
+ } catch (err) {
12310
+ for (const toolAssert of turn.assert.tools) {
12311
+ const name = "called" in toolAssert ? toolAssert.called : ("not_called" in toolAssert) ? toolAssert.not_called : "call_order";
12312
+ assertions.push({
12313
+ assertion: `tool: ${name}`,
12314
+ pass: false,
12315
+ expected: `Tool assertion on ${name}`,
12316
+ actual: `Failed to fetch traces: ${err.message}`
12317
+ });
12318
+ }
12319
+ }
12320
+ }
12321
+ if (turn.assert?.state) {
12322
+ try {
12323
+ const ctx = {
12324
+ botId: connection.botId,
12325
+ userId: session.userId,
12326
+ conversationId: result.conversationId
12327
+ };
12328
+ const stateResults = await gradeState(getBpClient(), turn.assert.state, ctx, preSnapshots);
12329
+ assertions.push(...stateResults);
12330
+ } catch (err) {
12331
+ assertions.push({
12332
+ assertion: "state",
12333
+ pass: false,
12334
+ expected: "State assertions executed",
12335
+ actual: `Error: ${err.message}`
12336
+ });
12337
+ }
12338
+ }
12339
+ if (turn.assert?.tables) {
12340
+ try {
12341
+ const tableResults = await gradeTables(getBpClient(), turn.assert.tables);
12342
+ assertions.push(...tableResults);
12343
+ } catch (err) {
12344
+ assertions.push({
12345
+ assertion: "tables",
12346
+ pass: false,
12347
+ expected: "Table assertions executed",
12348
+ actual: `Error: ${err.message}`
12349
+ });
12350
+ }
12351
+ }
12352
+ if (turn.assert?.workflow) {
12353
+ if (allTraceSpans.length === 0) {
12354
+ try {
12355
+ const traceData = await getTraceData(result.conversationId, devServerUrl);
12356
+ allTraceSpans = traceData.raw;
12357
+ } catch {}
12358
+ }
12359
+ const workflowResults = gradeWorkflows(allTraceSpans, turn.assert.workflow);
12360
+ assertions.push(...workflowResults);
12361
+ }
12362
+ const turnPass = assertions.every((a) => a.pass);
12363
+ const evalDuration = Date.now() - evalStart;
12364
+ turns.push({
12365
+ turnNumber: i + 1,
12366
+ userMessage: turn.user,
12367
+ botResponse,
12368
+ assertions,
12369
+ pass: turnPass,
12370
+ botDuration,
12371
+ evalDuration
12372
+ });
12373
+ }
12374
+ if (evalDef.outcome) {
12375
+ if (allTraceSpans.length === 0 && lastConversationId && evalDef.outcome.workflow) {
12376
+ try {
12377
+ const traceData = await getTraceData(lastConversationId, devServerUrl);
12378
+ allTraceSpans = traceData.raw;
12379
+ } catch {}
12380
+ }
12381
+ const ctx = {
12382
+ botId: connection.botId,
12383
+ userId: session.userId,
12384
+ conversationId: lastConversationId
12385
+ };
12386
+ try {
12387
+ outcomeAssertions = await gradeOutcome(getBpClient(), evalDef, ctx, allTraceSpans, preSnapshots);
12388
+ } catch (err) {
12389
+ outcomeAssertions = [
12390
+ {
12391
+ assertion: "outcome",
12392
+ pass: false,
12393
+ expected: "Outcome assertions executed",
12394
+ actual: `Error: ${err.message}`
12395
+ }
12396
+ ];
12397
+ }
12398
+ }
12399
+ const turnsPass = turns.every((t) => t.pass);
12400
+ const outcomePass = outcomeAssertions.every((a) => a.pass);
12401
+ return {
12402
+ name: evalDef.name,
12403
+ description: evalDef.description,
12404
+ type: evalDef.type,
12405
+ tags: evalDef.tags,
12406
+ turns,
12407
+ outcomeAssertions,
12408
+ pass: turnsPass && outcomePass,
12409
+ duration: Date.now() - start
12410
+ };
12411
+ } catch (err) {
12412
+ return {
12413
+ name: evalDef.name,
12414
+ description: evalDef.description,
12415
+ type: evalDef.type,
12416
+ tags: evalDef.tags,
12417
+ turns,
12418
+ outcomeAssertions,
12419
+ pass: false,
12420
+ duration: Date.now() - start,
12421
+ error: err.message
12573
12422
  };
12574
12423
  }
12575
- const runner = new ScriptRunner({
12576
- projectPath,
12577
- forceRegenerate: options.forceRegenerate,
12578
- prod: options.prod,
12579
- credentials
12424
+ }
12425
+ async function runEvalSuite(config, filter) {
12426
+ const start = Date.now();
12427
+ const runId = randomUUID().replace(/-/g, "").slice(0, 26);
12428
+ initLLMJudge({
12429
+ token: config.credentials.token,
12430
+ apiUrl: config.credentials.apiUrl,
12431
+ botId: config.credentials.botId
12580
12432
  });
12581
- return runner.setupTestRuntime({ env: options.env });
12433
+ const evalsDir = `${config.agentPath}/evals`;
12434
+ const allEvals = await loadEvalsFromDir(evalsDir);
12435
+ const evals = filterEvals(allEvals, filter);
12436
+ if (evals.length === 0) {
12437
+ return {
12438
+ id: runId,
12439
+ timestamp: new Date().toISOString(),
12440
+ evals: [],
12441
+ passed: 0,
12442
+ failed: 0,
12443
+ total: 0,
12444
+ duration: 0,
12445
+ filter
12446
+ };
12447
+ }
12448
+ let webhookId = config.credentials.webhookId;
12449
+ if (!webhookId) {
12450
+ webhookId = await discoverWebhookId(config.credentials.botId, config.credentials.token, config.credentials.apiUrl);
12451
+ }
12452
+ const connection = {
12453
+ webhookId,
12454
+ botId: config.credentials.botId,
12455
+ token: config.credentials.token,
12456
+ apiUrl: config.credentials.apiUrl
12457
+ };
12458
+ const devServerUrl = config.devServerUrl || "http://localhost:3001";
12459
+ const reports = [];
12460
+ config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
12461
+ for (let i = 0;i < evals.length; i++) {
12462
+ const evalDef = evals[i];
12463
+ config.onProgress?.({ type: "eval_start", evalName: evalDef.name, index: i });
12464
+ const report = await runEval(evalDef, connection, { devServerUrl });
12465
+ reports.push(report);
12466
+ config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
12467
+ }
12468
+ const runReport = {
12469
+ id: runId,
12470
+ timestamp: new Date().toISOString(),
12471
+ evals: reports,
12472
+ passed: reports.filter((r) => r.pass).length,
12473
+ failed: reports.filter((r) => !r.pass).length,
12474
+ total: reports.length,
12475
+ duration: Date.now() - start,
12476
+ filter
12477
+ };
12478
+ config.onProgress?.({ type: "suite_complete", report: runReport });
12479
+ return runReport;
12480
+ }
12481
+ // src/eval/store.ts
12482
+ import { existsSync as existsSync12, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync4 } from "fs";
12483
+ import { join as join10 } from "path";
12484
+ function getRunsDir(agentPath) {
12485
+ const dir = join10(agentPath, ".adk", "evals", "runs");
12486
+ if (!existsSync12(dir)) {
12487
+ mkdirSync2(dir, { recursive: true });
12488
+ }
12489
+ return dir;
12490
+ }
12491
+ function saveRunResult(agentPath, report) {
12492
+ const dir = getRunsDir(agentPath);
12493
+ const filename = `${report.timestamp.replace(/[:.]/g, "-")}-${report.id}.json`;
12494
+ const filepath = join10(dir, filename);
12495
+ writeFileSync2(filepath, JSON.stringify(report, null, 2));
12496
+ return filepath;
12497
+ }
12498
+ function loadRunResult(agentPath, runId) {
12499
+ const dir = getRunsDir(agentPath);
12500
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
12501
+ for (const file of files) {
12502
+ if (file.includes(runId)) {
12503
+ const filepath = join10(dir, file);
12504
+ return JSON.parse(readFileSync3(filepath, "utf-8"));
12505
+ }
12506
+ }
12507
+ return null;
12508
+ }
12509
+ function listRunResults(agentPath, limit = 50) {
12510
+ const dir = getRunsDir(agentPath);
12511
+ if (!existsSync12(dir))
12512
+ return [];
12513
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json")).sort().reverse().slice(0, limit);
12514
+ return files.map((file) => {
12515
+ const filepath = join10(dir, file);
12516
+ return JSON.parse(readFileSync3(filepath, "utf-8"));
12517
+ });
12518
+ }
12519
+ function getLatestRun(agentPath) {
12520
+ const runs = listRunResults(agentPath, 1);
12521
+ return runs[0] || null;
12582
12522
  }
12583
12523
  export {
12584
12524
  workspaceCache,
12585
12525
  stringifyWithOrder,
12586
12526
  setupTestRuntime,
12527
+ saveRunResult,
12587
12528
  runScript,
12529
+ runEvalSuite,
12530
+ runEval,
12588
12531
  orderKeys,
12532
+ loadRunResult,
12533
+ loadEvalsFromDir,
12534
+ loadEvalFile,
12535
+ loadEvalByName,
12536
+ listRunResults,
12589
12537
  integrationKeyOrder,
12590
12538
  initAssets,
12591
12539
  getRelativeTime,
12540
+ getLatestRun,
12541
+ getInnerTypeName,
12592
12542
  generateIntegrationTypes,
12593
12543
  generateClientWrapper,
12594
12544
  generateBotProject,
12595
12545
  generateAssetsTypes,
12596
12546
  generateAssetsRuntime,
12547
+ filterEvals,
12597
12548
  dependenciesKeyOrder,
12549
+ defineEval,
12550
+ coerceConfigValue,
12598
12551
  bpCliImporter,
12599
12552
  auth,
12600
12553
  agentInfoKeyOrder,
@@ -12642,4 +12595,4 @@ export {
12642
12595
  AgentProject
12643
12596
  };
12644
12597
 
12645
- //# debugId=A068F5974643B24F64756E2164756E21
12598
+ //# debugId=D97F506CD3BBA90E64756E2164756E21