@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.
- package/README.md +9 -0
- package/docs/guides/add-channel.md +25 -0
- package/docs/guides/add-knowledge.md +134 -0
- package/docs/guides/agentkit-skills-architecture.md +471 -0
- package/docs/guides/channels-production-handoff.md +2 -0
- package/docs/guides/connect-telegram.md +17 -0
- package/docs/guides/connect-whatsapp-zapster.md +16 -0
- package/docs/guides/create-agent.md +10 -1
- package/docs/guides/run-evals.md +36 -1
- package/docs/llms-full.txt +90 -1
- package/docs/llms.txt +9 -2
- package/package.json +2 -1
- package/src/cli/cloud-client.ts +10 -2
- package/src/cli/commands/channels.ts +67 -3
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/deploy-chat-ui.ts +7 -0
- package/src/cli/deploy-readiness.ts +19 -0
- package/src/cli/help.ts +26 -2
- package/src/cli/index.ts +140 -8
- package/src/cloud/artifact.ts +92 -1
- package/src/cloud/contracts.ts +16 -0
- package/src/create-project.ts +38 -6
- package/src/index.ts +142 -1
- package/src/providers/pi.ts +1 -1
- package/src/providers/test.ts +1 -1
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channels.ts +1 -0
- package/src/runtime/chat.ts +21 -2
- package/src/runtime/config.ts +175 -0
- package/src/runtime/core/manifest.ts +37 -0
- package/src/runtime/database.ts +93 -2
- package/src/runtime/db-commands.ts +9 -0
- package/src/runtime/deploy-readiness.ts +12 -0
- package/src/runtime/dev-server.ts +201 -11
- package/src/runtime/evals.ts +210 -20
- package/src/runtime/inspect.ts +39 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +283 -0
- package/src/runtime/knowledge/schema.ts +56 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +514 -4
- package/src/runtime/tools.ts +121 -1
- package/src/runtime/traces.ts +41 -0
- package/src/storage/sqlite.ts +141 -0
- package/src/templates/blank.ts +16 -5
- package/src/templates/dentista.ts +17 -2
- package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +37 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +37 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +37 -0
- package/src/templates/skills/agentkit-database/SKILL.md +45 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
- package/src/templates/skills/agentkit-security/SKILL.md +55 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
- 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 (
|
|
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 =
|
|
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
|
|
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,
|