@andreprado/agentkit 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (87) hide show
  1. package/README.md +9 -0
  2. package/docs/guides/add-channel.md +25 -0
  3. package/docs/guides/add-knowledge.md +134 -0
  4. package/docs/guides/agentkit-skills-architecture.md +471 -0
  5. package/docs/guides/channels-production-handoff.md +2 -0
  6. package/docs/guides/connect-telegram.md +17 -0
  7. package/docs/guides/connect-whatsapp-zapster.md +16 -0
  8. package/docs/guides/create-agent.md +10 -1
  9. package/docs/guides/run-evals.md +36 -1
  10. package/docs/llms-full.txt +90 -1
  11. package/docs/llms.txt +9 -2
  12. package/package.json +2 -1
  13. package/src/cli/cloud-client.ts +10 -2
  14. package/src/cli/commands/channels.ts +67 -3
  15. package/src/cli/commands/knowledge.ts +136 -0
  16. package/src/cli/deploy-chat-ui.ts +7 -0
  17. package/src/cli/deploy-readiness.ts +19 -0
  18. package/src/cli/help.ts +26 -2
  19. package/src/cli/index.ts +140 -8
  20. package/src/cloud/artifact.ts +92 -1
  21. package/src/cloud/contracts.ts +16 -0
  22. package/src/create-project.ts +38 -6
  23. package/src/index.ts +142 -1
  24. package/src/providers/pi.ts +1 -1
  25. package/src/providers/test.ts +1 -1
  26. package/src/runtime/channel-buffer.ts +30 -0
  27. package/src/runtime/channels.ts +1 -0
  28. package/src/runtime/chat.ts +21 -2
  29. package/src/runtime/config.ts +175 -0
  30. package/src/runtime/core/manifest.ts +37 -0
  31. package/src/runtime/database.ts +93 -2
  32. package/src/runtime/db-commands.ts +9 -0
  33. package/src/runtime/deploy-readiness.ts +12 -0
  34. package/src/runtime/dev-server.ts +201 -11
  35. package/src/runtime/evals.ts +210 -20
  36. package/src/runtime/inspect.ts +39 -0
  37. package/src/runtime/knowledge/chunk.ts +333 -0
  38. package/src/runtime/knowledge/config.ts +135 -0
  39. package/src/runtime/knowledge/embeddings.ts +133 -0
  40. package/src/runtime/knowledge/ingest.ts +521 -0
  41. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  42. package/src/runtime/knowledge/retrieve.ts +283 -0
  43. package/src/runtime/knowledge/schema.ts +56 -0
  44. package/src/runtime/knowledge/tool.ts +64 -0
  45. package/src/runtime/knowledge/vector.ts +258 -0
  46. package/src/runtime/spec.ts +152 -0
  47. package/src/runtime/sync.ts +144 -0
  48. package/src/runtime/targets/cloudflare/build.ts +514 -4
  49. package/src/runtime/tools.ts +121 -1
  50. package/src/runtime/traces.ts +41 -0
  51. package/src/storage/sqlite.ts +141 -0
  52. package/src/templates/blank.ts +16 -5
  53. package/src/templates/dentista.ts +17 -2
  54. package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
  55. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
  56. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  57. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  58. package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
  59. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  60. package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
  61. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
  62. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +37 -0
  63. package/src/templates/skills/agentkit-channels/references/telegram.md +37 -0
  64. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +37 -0
  65. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  66. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  67. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  68. package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
  69. package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
  70. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
  71. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
  72. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
  73. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
  74. package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
  75. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  76. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  77. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  78. package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
  79. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  80. package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
  81. package/src/templates/skills/agentkit-security/SKILL.md +55 -0
  82. package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
  83. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  84. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  85. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  86. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
  87. package/src/templates/support.ts +15 -4
@@ -8,6 +8,8 @@ import { build as esbuild, type Plugin } from "esbuild";
8
8
  import { loadAgentCapsule, type LoadedAgentCapsule } from "../../config";
9
9
  import { buildSharedAgentManifest, type ManifestSchema, type SharedAgentManifest } from "../../core/manifest";
10
10
  import { AgentKitError } from "../../errors";
11
+ import { resolveKnowledgeConfig } from "../../knowledge/config";
12
+ import { KNOWLEDGE_SCHEMA_SQL } from "../../knowledge/schema";
11
13
 
12
14
  export type BuildTarget = "cloudflare";
13
15
 
@@ -34,6 +36,7 @@ export type AgentManifest = {
34
36
  secrets: SharedAgentManifest["secrets"];
35
37
  tools: SharedAgentManifest["tools"];
36
38
  channels: SharedAgentManifest["channels"];
39
+ knowledge: SharedAgentManifest["knowledge"];
37
40
  access: SharedAgentManifest["access"];
38
41
  storage: {
39
42
  driver: "agentkit";
@@ -145,6 +148,14 @@ function validateCloudflareBuild(capsule: LoadedAgentCapsule): void {
145
148
  );
146
149
  }
147
150
 
151
+ const knowledge = resolveKnowledgeConfig(capsule.config);
152
+
153
+ if (knowledge.enabled && capsule.config.storage.database?.driver !== "turso") {
154
+ throw new AgentKitError(
155
+ "build_knowledge_database",
156
+ 'Cloudflare Knowledge deploy requires storage.database.driver: "turso" so chunks and retrieval state are available to the hosted runtime.',
157
+ );
158
+ }
148
159
  }
149
160
 
150
161
  function buildWarnings(capsule: LoadedAgentCapsule): string[] {
@@ -185,6 +196,7 @@ function buildManifest(
185
196
  secrets: shared.secrets,
186
197
  tools: shared.tools,
187
198
  channels: shared.channels,
199
+ knowledge: shared.knowledge,
188
200
  access: shared.access,
189
201
  storage: {
190
202
  driver: "agentkit",
@@ -210,12 +222,26 @@ function buildCloudflareFilesManifest(files: SharedAgentManifest["storage"]["fil
210
222
 
211
223
  async function loadTursoSchema(capsule: LoadedAgentCapsule): Promise<ManifestSchema | null> {
212
224
  const database = capsule.config.storage.database;
225
+ const knowledge = resolveKnowledgeConfig(capsule.config);
226
+ const sources: string[] = [];
227
+
228
+ if (!database || database.driver !== "turso") {
229
+ return null;
230
+ }
231
+
232
+ if (database.schema) {
233
+ sources.push(await readFile(resolve(capsule.root, database.schema), "utf8"));
234
+ }
213
235
 
214
- if (!database || database.driver !== "turso" || !database.schema) {
236
+ if (knowledge.enabled) {
237
+ sources.push(KNOWLEDGE_SCHEMA_SQL);
238
+ }
239
+
240
+ if (sources.length === 0) {
215
241
  return null;
216
242
  }
217
243
 
218
- const source = await readFile(resolve(capsule.root, database.schema), "utf8");
244
+ const source = sources.map((part) => part.trim()).filter(Boolean).join("\n\n") + "\n";
219
245
  const digest = sha256(source);
220
246
  return { source, sha256: digest };
221
247
  }
@@ -361,6 +387,7 @@ export function websiteChannel(input) {
361
387
  access: input.access ?? { mode: "token" },
362
388
  secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBSITE_CHANNEL_TOKEN"], "website channel"),
363
389
  ...(input.limits ? { limits: input.limits } : {}),
390
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "website channel") } : {}),
364
391
  };
365
392
  }
366
393
 
@@ -372,6 +399,7 @@ export function telegramChannel(input) {
372
399
  secrets: normalizeChannelSecrets(input.secrets ?? ["TELEGRAM_BOT_TOKEN", "TELEGRAM_WEBHOOK_SECRET"], "telegram channel"),
373
400
  allowedUpdates: input.allowedUpdates ?? ["message"],
374
401
  ...(input.limits ? { limits: input.limits } : {}),
402
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "telegram channel") } : {}),
375
403
  };
376
404
  }
377
405
 
@@ -382,6 +410,7 @@ export function whatsappChannel(input) {
382
410
  name: input.name,
383
411
  secrets: normalizeChannelSecrets(input.secrets ?? defaultWhatsappChannelSecrets(input.provider), \`\${input.provider} whatsapp channel\`),
384
412
  ...(input.limits ? { limits: input.limits } : {}),
413
+ ...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, \`\${input.provider} whatsapp channel\`) } : {}),
385
414
  };
386
415
  }
387
416
 
@@ -406,6 +435,36 @@ function normalizeChannelSecrets(secrets, context) {
406
435
 
407
436
  return Array.from(uniqueSecrets).sort();
408
437
  }
438
+
439
+ function normalizeChannelBuffer(input, context) {
440
+ if (input.mode === "off") {
441
+ return { mode: "off" };
442
+ }
443
+
444
+ if (input.mode !== undefined && input.mode !== "debounce") {
445
+ throw new Error(\`\${context} buffer.mode must be "off" or "debounce".\`);
446
+ }
447
+
448
+ const buffer = {
449
+ mode: "debounce",
450
+ quietWindowMs: input.quietWindowMs ?? 2500,
451
+ maxWaitMs: input.maxWaitMs ?? 12000,
452
+ maxMessages: input.maxMessages ?? 20,
453
+ maxChars: input.maxChars ?? 8000,
454
+ };
455
+
456
+ for (const field of ["quietWindowMs", "maxWaitMs", "maxMessages", "maxChars"]) {
457
+ if (!Number.isInteger(buffer[field]) || buffer[field] <= 0) {
458
+ throw new Error(\`\${context} buffer.\${field} must be a positive integer.\`);
459
+ }
460
+ }
461
+
462
+ if (buffer.maxWaitMs < buffer.quietWindowMs) {
463
+ throw new Error(\`\${context} buffer.maxWaitMs must be greater than or equal to buffer.quietWindowMs.\`);
464
+ }
465
+
466
+ return buffer;
467
+ }
409
468
  `;
410
469
  }
411
470
 
@@ -486,7 +545,8 @@ import { getProviderAdapter } from ${providerImportPath};
486
545
 
487
546
  const manifest = ${manifestJson};
488
547
  const instructions = ${instructionsJson};
489
- const hostedTools = agentConfig.tools ?? [];
548
+ const hostedKnowledgeTool = manifest.knowledge.enabled ? createHostedKnowledgeSearchTool() : null;
549
+ const hostedTools = hostedKnowledgeTool ? [hostedKnowledgeTool, ...(agentConfig.tools ?? [])] : (agentConfig.tools ?? []);
490
550
  const DEFAULT_TOOL_TIMEOUT_MS = 30000;
491
551
 
492
552
  export class AgentKitConversationStore extends DurableObject {
@@ -668,6 +728,7 @@ export default {
668
728
  provider: channel.provider,
669
729
  secrets: channel.secrets,
670
730
  })),
731
+ knowledge: manifest.knowledge,
671
732
  access: manifest.access,
672
733
  storage: manifest.storage,
673
734
  database: inspectHostedDatabase(),
@@ -690,6 +751,10 @@ export default {
690
751
  return Response.json({ status: "ok", agent: manifest.name });
691
752
  }
692
753
 
754
+ if (isHostedChannelWebhookPath(url.pathname)) {
755
+ return forwardHostedChannelWebhook(request, env, url);
756
+ }
757
+
693
758
  if (request.method === "POST" && (url.pathname === "/chat" || url.pathname === "/v1/chat")) {
694
759
  return handleChat(request, env);
695
760
  }
@@ -714,6 +779,47 @@ export default {
714
779
  },
715
780
  };
716
781
 
782
+ function isHostedChannelWebhookPath(pathname) {
783
+ return (
784
+ /^\\/channels\\/[^/]+\\/(?:website|telegram)\\/webhook$/.test(pathname) ||
785
+ /^\\/channels\\/[^/]+\\/whatsapp\\/(?:zapster|meta)\\/webhook$/.test(pathname)
786
+ );
787
+ }
788
+
789
+ async function forwardHostedChannelWebhook(request, env, url) {
790
+ if (!env.AGENTKIT_CLOUD_API_URL) {
791
+ return Response.json(
792
+ {
793
+ error: {
794
+ code: "channel_webhook_proxy_not_configured",
795
+ message: "Hosted channel webhooks require AGENTKIT_CLOUD_API_URL on the deployed Worker.",
796
+ },
797
+ },
798
+ { status: 501 },
799
+ );
800
+ }
801
+
802
+ const target = new URL(\`\${url.pathname}\${url.search}\`, env.AGENTKIT_CLOUD_API_URL);
803
+ const headers = new Headers(request.headers);
804
+ headers.delete("host");
805
+ headers.delete("content-length");
806
+
807
+ if (env.AGENTKIT_DEPLOY_ID) {
808
+ headers.set("X-AgentKit-Deploy-ID", env.AGENTKIT_DEPLOY_ID);
809
+ }
810
+
811
+ if (env.AGENTKIT_RUNTIME_GATE_TOKEN) {
812
+ headers.set("X-AgentKit-Runtime-Gate", env.AGENTKIT_RUNTIME_GATE_TOKEN);
813
+ }
814
+
815
+ return fetch(target.href, {
816
+ method: request.method,
817
+ headers,
818
+ body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
819
+ redirect: "manual",
820
+ });
821
+ }
822
+
717
823
  async function handleTursoHealth(request, env) {
718
824
  const auth = authorizeAdmin(request, env);
719
825
 
@@ -1022,7 +1128,7 @@ async function callHostedProvider(env, messages, signal) {
1022
1128
  runtime: manifest.runtime,
1023
1129
  provider: manifest.provider,
1024
1130
  },
1025
- instructions: withToolVisibilityInstructions(instructions, hostedTools),
1131
+ instructions: withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1026
1132
  messages,
1027
1133
  tools: hostedTools,
1028
1134
  toolRuntime: createHostedProviderToolRuntime(env, signal),
@@ -1033,6 +1139,20 @@ async function callHostedProvider(env, messages, signal) {
1033
1139
  return result.message.content;
1034
1140
  }
1035
1141
 
1142
+ function appendHostedKnowledgePromptPolicy(baseInstructions) {
1143
+ if (!manifest.knowledge.enabled) {
1144
+ return baseInstructions;
1145
+ }
1146
+
1147
+ return \`\${baseInstructions.trimEnd()}
1148
+
1149
+ Knowledge rules:
1150
+ - Use agentkit_search_knowledge before answering business-specific factual questions about the user's documents, policies, prices, availability, services, procedures, or source-specific claims.
1151
+ - Do not invent prices, policies, legal/clinical claims, availability, or source-specific facts when Knowledge has not returned support for the answer.
1152
+ - Use retrieved Knowledge as context for the final answer, but do not expose raw retrieval JSON, scores, chunk IDs, or internal tool output objects to the user.
1153
+ \`;
1154
+ }
1155
+
1036
1156
  function withToolVisibilityInstructions(baseInstructions, tools) {
1037
1157
  const internalTools = tools
1038
1158
  .filter((tool) => tool.visibility === "internal")
@@ -1052,6 +1172,396 @@ Tool visibility rules:
1052
1172
  \`;
1053
1173
  }
1054
1174
 
1175
+ function createHostedKnowledgeSearchTool() {
1176
+ return {
1177
+ name: manifest.knowledge.internalTool || "agentkit_search_knowledge",
1178
+ description: "Search the Agent Capsule's configured Knowledge sources for business-specific facts.",
1179
+ visibility: "internal",
1180
+ inputSchema: {
1181
+ type: "object",
1182
+ properties: {
1183
+ query: { type: "string" },
1184
+ topK: { type: "integer" },
1185
+ },
1186
+ required: ["query"],
1187
+ additionalProperties: false,
1188
+ },
1189
+ outputSchema: {
1190
+ type: "object",
1191
+ properties: {
1192
+ results: {
1193
+ type: "array",
1194
+ items: {
1195
+ type: "object",
1196
+ properties: {
1197
+ content: { type: "string" },
1198
+ score: { type: "number" },
1199
+ source: { type: "object" },
1200
+ },
1201
+ required: ["content", "score", "source"],
1202
+ },
1203
+ },
1204
+ },
1205
+ required: ["results"],
1206
+ additionalProperties: false,
1207
+ },
1208
+ secrets: manifest.knowledge.embedding.secret ? [manifest.knowledge.embedding.secret] : [],
1209
+ async execute(input, ctx) {
1210
+ const query = typeof input?.query === "string" ? input.query : "";
1211
+ const topK = normalizeKnowledgeTopK(input?.topK ?? manifest.knowledge.retrieval.topK);
1212
+ const results = await searchHostedKnowledge(ctx.database, query, topK, ctx.secrets ?? {});
1213
+
1214
+ return {
1215
+ results: results.map((result) => ({
1216
+ content: result.content,
1217
+ score: result.score,
1218
+ source: result.source,
1219
+ })),
1220
+ };
1221
+ },
1222
+ };
1223
+ }
1224
+
1225
+ async function searchHostedKnowledge(database, query, topK, secrets) {
1226
+ const normalizedQuery = String(query || "").trim();
1227
+
1228
+ if (!normalizedQuery) {
1229
+ throw agentKitError("knowledge_query_invalid", "Knowledge search query must be a non-empty string.");
1230
+ }
1231
+
1232
+ const hasEmbeddingProvider = manifest.knowledge.embedding.provider !== "none";
1233
+ const lexical =
1234
+ manifest.knowledge.retrieval.hybrid || !hasEmbeddingProvider
1235
+ ? await searchHostedKnowledgeLexical(database, normalizedQuery, Math.max(topK * 4, topK))
1236
+ : [];
1237
+ const semantic = hasEmbeddingProvider
1238
+ ? await searchHostedKnowledgeSemantic(database, normalizedQuery, topK * 4, secrets)
1239
+ : [];
1240
+
1241
+ return combineHostedKnowledgeResults(lexical, semantic, topK);
1242
+ }
1243
+
1244
+ async function searchHostedKnowledgeLexical(database, query, limit) {
1245
+ const tokens = hostedKnowledgeQueryTokens(query);
1246
+
1247
+ if (tokens.length === 0) {
1248
+ return [];
1249
+ }
1250
+
1251
+ try {
1252
+ const result = await database.query(
1253
+ \`
1254
+ SELECT
1255
+ c.id AS chunk_id,
1256
+ c.source_id AS source_id,
1257
+ c.content AS content,
1258
+ c.title AS title,
1259
+ c.section AS section,
1260
+ c.locator AS locator,
1261
+ s.source_key AS source_key,
1262
+ rank
1263
+ FROM agentkit_knowledge_fts
1264
+ JOIN agentkit_knowledge_chunks c ON c.id = agentkit_knowledge_fts.chunk_id
1265
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1266
+ WHERE agentkit_knowledge_fts MATCH ?
1267
+ ORDER BY rank ASC
1268
+ LIMIT ?
1269
+ \`,
1270
+ [tokens.map((token) => \`"\${token.replace(/"/g, '""')}"\`).join(" OR "), limit],
1271
+ );
1272
+
1273
+ return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
1274
+ } catch {
1275
+ return searchHostedKnowledgeLexicalFallback(database, query, limit);
1276
+ }
1277
+ }
1278
+
1279
+ async function searchHostedKnowledgeLexicalFallback(database, query, limit) {
1280
+ const like = \`%\${query.toLowerCase()}%\`;
1281
+ const result = await database.query(
1282
+ \`
1283
+ SELECT
1284
+ c.id AS chunk_id,
1285
+ c.source_id AS source_id,
1286
+ c.content AS content,
1287
+ c.title AS title,
1288
+ c.section AS section,
1289
+ c.locator AS locator,
1290
+ s.source_key AS source_key
1291
+ FROM agentkit_knowledge_chunks c
1292
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1293
+ WHERE lower(c.content) LIKE ?
1294
+ OR lower(COALESCE(c.title, '')) LIKE ?
1295
+ OR lower(COALESCE(c.section, '')) LIKE ?
1296
+ ORDER BY c.created_at DESC, c.ordinal ASC
1297
+ LIMIT ?
1298
+ \`,
1299
+ [like, like, like, limit],
1300
+ );
1301
+
1302
+ return result.rows.map((row, index) => hostedKnowledgeResultFromRow(row, hostedLexicalScore(index), null));
1303
+ }
1304
+
1305
+ async function searchHostedKnowledgeSemantic(database, query, limit, secrets) {
1306
+ const queryEmbedding = await createHostedKnowledgeEmbedding(query, secrets);
1307
+
1308
+ if (!queryEmbedding) {
1309
+ return [];
1310
+ }
1311
+
1312
+ try {
1313
+ return await searchHostedKnowledgeNativeVector(database, queryEmbedding, limit);
1314
+ } catch {
1315
+ return searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit);
1316
+ }
1317
+ }
1318
+
1319
+ async function searchHostedKnowledgeNativeVector(database, queryEmbedding, limit) {
1320
+ const vector = JSON.stringify(queryEmbedding);
1321
+ const result = await database.query(
1322
+ \`
1323
+ SELECT
1324
+ c.id AS chunk_id,
1325
+ c.source_id AS source_id,
1326
+ c.content AS content,
1327
+ c.title AS title,
1328
+ c.section AS section,
1329
+ c.locator AS locator,
1330
+ s.source_key AS source_key,
1331
+ vector_distance_cos(v.embedding, vector32(?)) AS vector_distance
1332
+ FROM vector_top_k('agentkit_knowledge_vectors_embedding_idx', vector32(?), ?) nearest
1333
+ JOIN agentkit_knowledge_vectors v ON v.id = nearest.id
1334
+ JOIN agentkit_knowledge_chunks c ON c.id = v.chunk_id
1335
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1336
+ ORDER BY vector_distance ASC
1337
+ \`,
1338
+ [vector, vector, limit],
1339
+ );
1340
+
1341
+ return result.rows
1342
+ .map((row) => {
1343
+ const distance = Number(row.vector_distance);
1344
+ const semantic = Number.isFinite(distance) ? 1 - distance : 0;
1345
+ return hostedKnowledgeResultFromRow(row, 0, semantic);
1346
+ })
1347
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
1348
+ .slice(0, limit);
1349
+ }
1350
+
1351
+ async function searchHostedKnowledgeSemanticFallback(database, queryEmbedding, limit) {
1352
+ const result = await database.query(
1353
+ \`
1354
+ SELECT
1355
+ c.id AS chunk_id,
1356
+ c.source_id AS source_id,
1357
+ c.content AS content,
1358
+ c.title AS title,
1359
+ c.section AS section,
1360
+ c.locator AS locator,
1361
+ c.embedding_json AS embedding_json,
1362
+ s.source_key AS source_key
1363
+ FROM agentkit_knowledge_chunks c
1364
+ JOIN agentkit_knowledge_sources s ON s.id = c.source_id
1365
+ WHERE c.embedding_json IS NOT NULL
1366
+ ORDER BY c.created_at DESC, c.ordinal ASC
1367
+ \`,
1368
+ );
1369
+
1370
+ return result.rows
1371
+ .map((row) => {
1372
+ const embedding = parseHostedKnowledgeEmbedding(row.embedding_json);
1373
+ const semantic = embedding ? hostedCosineSimilarity(queryEmbedding, embedding) : 0;
1374
+ return hostedKnowledgeResultFromRow(row, 0, semantic);
1375
+ })
1376
+ .filter((result) => result.match.semantic !== null && result.match.semantic > 0)
1377
+ .sort((left, right) => (right.match.semantic ?? 0) - (left.match.semantic ?? 0))
1378
+ .slice(0, limit);
1379
+ }
1380
+
1381
+ async function createHostedKnowledgeEmbedding(input, secrets) {
1382
+ const provider = manifest.knowledge.embedding.provider;
1383
+
1384
+ if (provider === "none") {
1385
+ return null;
1386
+ }
1387
+
1388
+ if (provider === "test") {
1389
+ return hostedFakeEmbedding(input, manifest.knowledge.embedding.dimensions || 32);
1390
+ }
1391
+
1392
+ if (provider !== "openai") {
1393
+ throw agentKitError("knowledge_embedding_provider_unsupported", "Unsupported knowledge embedding provider.");
1394
+ }
1395
+
1396
+ const secretName = manifest.knowledge.embedding.secret || "OPENAI_API_KEY";
1397
+ const apiKey = secrets[secretName];
1398
+
1399
+ if (!apiKey) {
1400
+ throw agentKitError("knowledge_embedding_secret_missing", \`Knowledge embeddings require missing secret "\${secretName}".\`);
1401
+ }
1402
+
1403
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
1404
+ method: "POST",
1405
+ headers: {
1406
+ "content-type": "application/json",
1407
+ authorization: \`Bearer \${apiKey}\`,
1408
+ },
1409
+ body: JSON.stringify({
1410
+ model: manifest.knowledge.embedding.model || "text-embedding-3-small",
1411
+ input,
1412
+ encoding_format: "float",
1413
+ ...(manifest.knowledge.embedding.dimensions ? { dimensions: manifest.knowledge.embedding.dimensions } : {}),
1414
+ }),
1415
+ });
1416
+
1417
+ if (!response.ok) {
1418
+ const text = await response.text().catch(() => "");
1419
+ throw agentKitError(
1420
+ "knowledge_embedding_failed",
1421
+ \`OpenAI embedding request failed with HTTP \${response.status}.\${text ? " " + text.slice(0, 240) : ""}\`,
1422
+ );
1423
+ }
1424
+
1425
+ const payload = await response.json();
1426
+ const embedding = payload?.data?.[0]?.embedding;
1427
+
1428
+ if (!Array.isArray(embedding) || embedding.some((value) => typeof value !== "number")) {
1429
+ throw agentKitError("knowledge_embedding_failed", "OpenAI embedding response did not contain a numeric vector.");
1430
+ }
1431
+
1432
+ return embedding;
1433
+ }
1434
+
1435
+ function combineHostedKnowledgeResults(lexical, semantic, topK) {
1436
+ const byChunk = new Map();
1437
+
1438
+ for (const result of lexical) {
1439
+ byChunk.set(result.chunkId, result);
1440
+ }
1441
+
1442
+ for (const result of semantic) {
1443
+ const existing = byChunk.get(result.chunkId);
1444
+
1445
+ if (!existing) {
1446
+ byChunk.set(result.chunkId, {
1447
+ ...result,
1448
+ score: hostedCombinedScore(0, result.match.semantic),
1449
+ });
1450
+ continue;
1451
+ }
1452
+
1453
+ byChunk.set(result.chunkId, {
1454
+ ...existing,
1455
+ score: hostedCombinedScore(existing.match.lexical, result.match.semantic),
1456
+ match: {
1457
+ lexical: existing.match.lexical,
1458
+ semantic: result.match.semantic,
1459
+ },
1460
+ });
1461
+ }
1462
+
1463
+ return Array.from(byChunk.values())
1464
+ .map((result) => ({
1465
+ ...result,
1466
+ score: result.score || hostedCombinedScore(result.match.lexical, result.match.semantic),
1467
+ }))
1468
+ .sort((left, right) => right.score - left.score || left.source.path.localeCompare(right.source.path))
1469
+ .slice(0, topK);
1470
+ }
1471
+
1472
+ function hostedKnowledgeResultFromRow(row, lexical, semantic) {
1473
+ return {
1474
+ chunkId: String(row.chunk_id),
1475
+ content: String(row.content),
1476
+ score: hostedCombinedScore(lexical, semantic),
1477
+ source: {
1478
+ id: String(row.source_id),
1479
+ path: String(row.source_key),
1480
+ title: typeof row.title === "string" ? row.title : null,
1481
+ section: typeof row.section === "string" ? row.section : null,
1482
+ locator: typeof row.locator === "string" ? row.locator : null,
1483
+ },
1484
+ match: {
1485
+ lexical,
1486
+ semantic,
1487
+ },
1488
+ };
1489
+ }
1490
+
1491
+ function normalizeKnowledgeTopK(value) {
1492
+ const topK = Number(value);
1493
+
1494
+ if (!Number.isInteger(topK) || topK <= 0 || topK > 50) {
1495
+ throw agentKitError("knowledge_query_invalid", "Knowledge search topK must be a positive integer no larger than 50.");
1496
+ }
1497
+
1498
+ return topK;
1499
+ }
1500
+
1501
+ function hostedKnowledgeQueryTokens(query) {
1502
+ return Array.from(new Set(String(query).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [])).slice(0, 12);
1503
+ }
1504
+
1505
+ function parseHostedKnowledgeEmbedding(value) {
1506
+ if (typeof value !== "string") {
1507
+ return null;
1508
+ }
1509
+
1510
+ try {
1511
+ const parsed = JSON.parse(value);
1512
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === "number") ? parsed : null;
1513
+ } catch {
1514
+ return null;
1515
+ }
1516
+ }
1517
+
1518
+ function hostedCombinedScore(lexical, semantic) {
1519
+ const semanticScore = semantic === null ? 0 : Math.max(0, (semantic + 1) / 2);
1520
+ return Number((lexical * 0.6 + semanticScore * 0.4).toFixed(6));
1521
+ }
1522
+
1523
+ function hostedLexicalScore(index) {
1524
+ return Number((1 / (index + 1)).toFixed(6));
1525
+ }
1526
+
1527
+ function hostedCosineSimilarity(left, right) {
1528
+ if (left.length !== right.length || left.length === 0) {
1529
+ return 0;
1530
+ }
1531
+
1532
+ let dot = 0;
1533
+ let leftNorm = 0;
1534
+ let rightNorm = 0;
1535
+
1536
+ for (let index = 0; index < left.length; index += 1) {
1537
+ dot += left[index] * right[index];
1538
+ leftNorm += left[index] * left[index];
1539
+ rightNorm += right[index] * right[index];
1540
+ }
1541
+
1542
+ if (leftNorm === 0 || rightNorm === 0) {
1543
+ return 0;
1544
+ }
1545
+
1546
+ return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
1547
+ }
1548
+
1549
+ async function hostedFakeEmbedding(input, dimensions) {
1550
+ const values = Array.from({ length: dimensions }, () => 0);
1551
+ const tokens = String(input).toLowerCase().match(/[\\p{L}\\p{N}_-]+/gu) ?? [];
1552
+ const encoder = new TextEncoder();
1553
+
1554
+ for (const token of tokens) {
1555
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token)));
1556
+ const slot = hash[0] % dimensions;
1557
+ const sign = hash[1] % 2 === 0 ? 1 : -1;
1558
+ values[slot] += sign * (1 + (hash[2] % 7) / 10);
1559
+ }
1560
+
1561
+ const norm = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
1562
+ return norm === 0 ? values : values.map((value) => Number((value / norm).toFixed(8)));
1563
+ }
1564
+
1055
1565
  async function persistFailure(env, conversationId, userMessage, error) {
1056
1566
  await persist(env, {
1057
1567
  conversationId,