@andreprado/agentkit 0.1.0-alpha.9 → 0.1.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.
- package/README.md +18 -1
- package/docs/guides/add-channel.md +251 -7
- package/docs/guides/add-knowledge.md +10 -0
- package/docs/guides/add-managed-composio.md +165 -0
- package/docs/guides/add-tool.md +10 -3
- package/docs/guides/channel-security.md +162 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +61 -1
- package/docs/guides/connect-whatsapp-evolution.md +121 -0
- package/docs/guides/connect-whatsapp-uazapi.md +139 -0
- package/docs/guides/connect-whatsapp-zapster.md +119 -16
- package/docs/guides/create-agent.md +31 -4
- package/docs/guides/debug-channel.md +159 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +32 -14
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +95 -25
- package/docs/guides/security-rules.md +9 -5
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-jev.md +67 -0
- package/docs/guides/use-provider.md +70 -3
- package/docs/llms-full.txt +295 -25
- package/docs/llms.txt +54 -7
- package/package.json +3 -7
- package/src/cli/args.ts +23 -2
- package/src/cli/cloud-client.ts +121 -9
- package/src/cli/commands/channels.ts +856 -36
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/provider.ts +47 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/deploy-chat-ui.ts +232 -18
- package/src/cli/deploy-readiness.ts +227 -14
- package/src/cli/help.ts +67 -9
- package/src/cli/index.ts +740 -35
- package/src/cli/new-command.ts +41 -0
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +18 -35
- package/src/index.ts +565 -11
- package/src/providers/codex-auth.ts +111 -0
- package/src/providers/pi.ts +88 -19
- package/src/providers/test.ts +36 -0
- package/src/providers/types.ts +8 -0
- package/src/runtime/channel-test-harness.ts +21 -1
- package/src/runtime/channels/discord.ts +904 -0
- package/src/runtime/channels/generic-webhook.ts +682 -0
- package/src/runtime/channels/net-guard.ts +480 -0
- package/src/runtime/channels/provider-fetch.ts +54 -0
- package/src/runtime/channels/slack.ts +652 -0
- package/src/runtime/channels/telegram.ts +379 -15
- package/src/runtime/channels/whatsapp-evolution.ts +1330 -0
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1192 -0
- package/src/runtime/channels/whatsapp-zapster.ts +702 -40
- package/src/runtime/channels.ts +83 -3
- package/src/runtime/chat.ts +70 -44
- package/src/runtime/config.ts +512 -20
- package/src/runtime/core/manifest.ts +75 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/deploy-readiness.ts +34 -4
- package/src/runtime/dev-server.ts +639 -39
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +445 -74
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +173 -4
- package/src/runtime/integrations/composio.ts +425 -0
- package/src/runtime/knowledge/embeddings.ts +45 -7
- package/src/runtime/knowledge/ingest.ts +69 -6
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/knowledge/vector.ts +30 -30
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +71 -7
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/targets/cloudflare/build.ts +1010 -208
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +26 -2
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +7 -2
- package/src/templates/blank.ts +37 -9
- package/src/templates/dentista.ts +40 -14
- package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
- package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
- package/src/templates/skills/agentkit-database/SKILL.md +11 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
- package/src/templates/skills/agentkit-security/SKILL.md +5 -2
- package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
- package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
- package/src/templates/support.ts +42 -12
- package/docs/guides/agentkit-skills-architecture.md +0 -471
- package/docs/guides/channels-implementation-map.md +0 -243
- package/docs/guides/channels-production-handoff.md +0 -101
- package/docs/portable-deploy-release-checklist.md +0 -41
|
@@ -5,8 +5,15 @@ import { AgentKitError } from "../errors";
|
|
|
5
5
|
|
|
6
6
|
export type EmbeddingProvider = {
|
|
7
7
|
embed(input: string): Promise<number[]>;
|
|
8
|
+
/** Embeds inputs in as few provider requests as possible; results keep input order. */
|
|
9
|
+
embedMany(inputs: string[]): Promise<number[][]>;
|
|
8
10
|
};
|
|
9
11
|
|
|
12
|
+
// Each valid input is at most 8,192 tokens; 36 inputs cannot exceed the
|
|
13
|
+
// 300,000-token request limit, even for long CSV rows or dense text.
|
|
14
|
+
// ponytail: conservative batches; add token-aware packing if throughput needs it.
|
|
15
|
+
const OPENAI_EMBEDDING_BATCH_SIZE = Math.floor(300_000 / 8_192);
|
|
16
|
+
|
|
10
17
|
export type CreateEmbeddingProviderOptions = {
|
|
11
18
|
config: ResolvedKnowledgeConfig["embedding"];
|
|
12
19
|
env?: Record<string, string | undefined>;
|
|
@@ -23,13 +30,26 @@ export function createEmbeddingProvider(options: CreateEmbeddingProviderOptions)
|
|
|
23
30
|
async embed(input) {
|
|
24
31
|
return fakeEmbedding(input, options.config.dimensions ?? 32);
|
|
25
32
|
},
|
|
33
|
+
async embedMany(inputs) {
|
|
34
|
+
return inputs.map((input) => fakeEmbedding(input, options.config.dimensions ?? 32));
|
|
35
|
+
},
|
|
26
36
|
};
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
if (options.config.provider === "openai") {
|
|
30
40
|
return {
|
|
31
41
|
async embed(input) {
|
|
32
|
-
|
|
42
|
+
const [embedding] = await openAiEmbeddings([input], options);
|
|
43
|
+
return embedding;
|
|
44
|
+
},
|
|
45
|
+
async embedMany(inputs) {
|
|
46
|
+
const embeddings: number[][] = [];
|
|
47
|
+
|
|
48
|
+
for (let start = 0; start < inputs.length; start += OPENAI_EMBEDDING_BATCH_SIZE) {
|
|
49
|
+
embeddings.push(...(await openAiEmbeddings(inputs.slice(start, start + OPENAI_EMBEDDING_BATCH_SIZE), options)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return embeddings;
|
|
33
53
|
},
|
|
34
54
|
};
|
|
35
55
|
}
|
|
@@ -74,7 +94,7 @@ export function cosineSimilarity(left: number[], right: number[]): number {
|
|
|
74
94
|
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
async function
|
|
97
|
+
async function openAiEmbeddings(inputs: string[], options: CreateEmbeddingProviderOptions): Promise<number[][]> {
|
|
78
98
|
const secretName = options.config.secret ?? "OPENAI_API_KEY";
|
|
79
99
|
const apiKey = options.env?.[secretName] ?? process.env[secretName];
|
|
80
100
|
|
|
@@ -94,7 +114,7 @@ async function openAiEmbedding(input: string, options: CreateEmbeddingProviderOp
|
|
|
94
114
|
},
|
|
95
115
|
body: JSON.stringify({
|
|
96
116
|
model: options.config.model ?? "text-embedding-3-small",
|
|
97
|
-
input,
|
|
117
|
+
input: inputs,
|
|
98
118
|
encoding_format: "float",
|
|
99
119
|
...(options.config.dimensions ? { dimensions: options.config.dimensions } : {}),
|
|
100
120
|
}),
|
|
@@ -109,13 +129,31 @@ async function openAiEmbedding(input: string, options: CreateEmbeddingProviderOp
|
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
const payload = await response.json();
|
|
112
|
-
const
|
|
132
|
+
const data: unknown[] = Array.isArray(payload?.data) ? payload.data : [];
|
|
133
|
+
const embeddings: number[][] = new Array(inputs.length);
|
|
134
|
+
|
|
135
|
+
for (const [position, item] of data.entries()) {
|
|
136
|
+
const record = item as { index?: unknown; embedding?: unknown };
|
|
137
|
+
const index = typeof record.index === "number" ? record.index : position;
|
|
138
|
+
const embedding = record.embedding;
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
index >= 0 &&
|
|
142
|
+
index < inputs.length &&
|
|
143
|
+
Array.isArray(embedding) &&
|
|
144
|
+
embedding.every((value) => typeof value === "number")
|
|
145
|
+
) {
|
|
146
|
+
embeddings[index] = embedding;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
113
149
|
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
for (let index = 0; index < inputs.length; index += 1) {
|
|
151
|
+
if (!embeddings[index]) {
|
|
152
|
+
throw new AgentKitError("knowledge_embedding_failed", "OpenAI embedding response did not contain a numeric vector.");
|
|
153
|
+
}
|
|
116
154
|
}
|
|
117
155
|
|
|
118
|
-
return
|
|
156
|
+
return embeddings;
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
function normalizeVector(values: number[]): number[] {
|
|
@@ -151,7 +151,9 @@ async function ingestKnowledgeSources(
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
if (results.some((result) => result.status === "indexed")) {
|
|
155
|
+
await checkpointLocalKnowledgeDatabase(db);
|
|
156
|
+
}
|
|
155
157
|
} finally {
|
|
156
158
|
store.close();
|
|
157
159
|
}
|
|
@@ -304,13 +306,28 @@ async function ingestKnowledgeSource(
|
|
|
304
306
|
id: string;
|
|
305
307
|
}> = [];
|
|
306
308
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
+
const chunkHashes = chunks.map((chunk) => sha256(chunk.content));
|
|
310
|
+
const embeddingsByHash = embeddingProvider
|
|
311
|
+
? await loadReusableChunkEmbeddings(db, sourceId, resolvedKnowledge)
|
|
312
|
+
: new Map<string, number[]>();
|
|
313
|
+
|
|
314
|
+
if (embeddingProvider) {
|
|
315
|
+
const missingHashes = [...new Set(chunkHashes.filter((hash) => !embeddingsByHash.has(hash)))];
|
|
316
|
+
|
|
317
|
+
if (missingHashes.length > 0) {
|
|
318
|
+
const contentByHash = new Map(chunks.map((chunk, index) => [chunkHashes[index], chunk.content]));
|
|
319
|
+
const embeddings = await embeddingProvider.embedMany(missingHashes.map((hash) => contentByHash.get(hash)!));
|
|
320
|
+
missingHashes.forEach((hash, index) => embeddingsByHash.set(hash, embeddings[index]));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
325
|
+
const contentSha256 = chunkHashes[index];
|
|
309
326
|
embeddedChunks.push({
|
|
310
327
|
chunk,
|
|
311
|
-
embedding,
|
|
312
|
-
contentSha256
|
|
313
|
-
id: stableId("kchk", `${sourceId}:${chunk.ordinal}:${
|
|
328
|
+
embedding: embeddingProvider ? embeddingsByHash.get(contentSha256) ?? null : null,
|
|
329
|
+
contentSha256,
|
|
330
|
+
id: stableId("kchk", `${sourceId}:${chunk.ordinal}:${contentSha256}`),
|
|
314
331
|
});
|
|
315
332
|
}
|
|
316
333
|
|
|
@@ -491,6 +508,52 @@ function capsuleRelativePath(root: string, absolutePath: string): string {
|
|
|
491
508
|
return relativePath.split("\\").join("/");
|
|
492
509
|
}
|
|
493
510
|
|
|
511
|
+
// Chunks whose text is unchanged keep their stored vector, so editing one part
|
|
512
|
+
// of a file only pays for embedding the chunks that actually changed.
|
|
513
|
+
async function loadReusableChunkEmbeddings(
|
|
514
|
+
db: DatabaseRunner,
|
|
515
|
+
sourceId: string,
|
|
516
|
+
knowledge: ReturnType<typeof resolveKnowledgeConfig>,
|
|
517
|
+
): Promise<Map<string, number[]>> {
|
|
518
|
+
const result = await db.query(
|
|
519
|
+
`
|
|
520
|
+
SELECT content_sha256, embedding_json
|
|
521
|
+
FROM agentkit_knowledge_chunks
|
|
522
|
+
WHERE source_id = ?
|
|
523
|
+
AND embedding_json IS NOT NULL
|
|
524
|
+
AND embedding_provider = ?
|
|
525
|
+
AND embedding_model = ?
|
|
526
|
+
AND (? IS NULL OR embedding_dimensions = ?)
|
|
527
|
+
`,
|
|
528
|
+
[
|
|
529
|
+
sourceId,
|
|
530
|
+
knowledge.embedding.provider,
|
|
531
|
+
knowledge.embedding.model,
|
|
532
|
+
knowledge.embedding.dimensions,
|
|
533
|
+
knowledge.embedding.dimensions,
|
|
534
|
+
],
|
|
535
|
+
);
|
|
536
|
+
const embeddings = new Map<string, number[]>();
|
|
537
|
+
|
|
538
|
+
for (const row of result.rows) {
|
|
539
|
+
if (typeof row.content_sha256 !== "string" || typeof row.embedding_json !== "string") {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const embedding = JSON.parse(row.embedding_json);
|
|
545
|
+
|
|
546
|
+
if (Array.isArray(embedding) && embedding.every((value) => typeof value === "number")) {
|
|
547
|
+
embeddings.set(row.content_sha256, embedding);
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
// A corrupt stored vector is simply re-embedded.
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return embeddings;
|
|
555
|
+
}
|
|
556
|
+
|
|
494
557
|
async function findExistingSource(db: DatabaseRunner, sourceKey: string): Promise<DatabaseRow | undefined> {
|
|
495
558
|
const result = await db.query(
|
|
496
559
|
"SELECT id, content_sha256, status FROM agentkit_knowledge_sources WHERE source_key = ?",
|
|
@@ -108,7 +108,25 @@ async function searchLexical(db: DatabaseRunner, query: string, limit: number):
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
async function searchLexicalFallback(db: DatabaseRunner, query: string, limit: number): Promise<KnowledgeSearchResult[]> {
|
|
111
|
-
const
|
|
111
|
+
const tokens = queryTokens(query);
|
|
112
|
+
|
|
113
|
+
if (tokens.length === 0) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const conditions = tokens
|
|
118
|
+
.map(
|
|
119
|
+
() => `(
|
|
120
|
+
lower(c.content) LIKE ? ESCAPE '\\'
|
|
121
|
+
OR lower(COALESCE(c.title, '')) LIKE ? ESCAPE '\\'
|
|
122
|
+
OR lower(COALESCE(c.section, '')) LIKE ? ESCAPE '\\'
|
|
123
|
+
)`,
|
|
124
|
+
)
|
|
125
|
+
.join(" AND ");
|
|
126
|
+
const args = tokens.flatMap((token) => {
|
|
127
|
+
const like = likePattern(token);
|
|
128
|
+
return [like, like, like];
|
|
129
|
+
});
|
|
112
130
|
const result = await db.query(
|
|
113
131
|
`
|
|
114
132
|
SELECT
|
|
@@ -121,13 +139,11 @@ async function searchLexicalFallback(db: DatabaseRunner, query: string, limit: n
|
|
|
121
139
|
s.source_key AS source_key
|
|
122
140
|
FROM agentkit_knowledge_chunks c
|
|
123
141
|
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
124
|
-
WHERE
|
|
125
|
-
OR lower(COALESCE(c.title, '')) LIKE ?
|
|
126
|
-
OR lower(COALESCE(c.section, '')) LIKE ?
|
|
142
|
+
WHERE ${conditions}
|
|
127
143
|
ORDER BY c.created_at DESC, c.ordinal ASC
|
|
128
144
|
LIMIT ?
|
|
129
145
|
`,
|
|
130
|
-
[
|
|
146
|
+
[...args, limit],
|
|
131
147
|
);
|
|
132
148
|
|
|
133
149
|
return result.rows.map((row, index) => knowledgeSearchResultFromRow(row, lexicalScore(index), null));
|
|
@@ -269,6 +285,10 @@ function queryTokens(query: string): string[] {
|
|
|
269
285
|
return Array.from(new Set(query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [])).slice(0, 12);
|
|
270
286
|
}
|
|
271
287
|
|
|
288
|
+
function likePattern(token: string): string {
|
|
289
|
+
return `%${token.replace(/[\\%_]/g, (value) => `\\${value}`)}%`;
|
|
290
|
+
}
|
|
291
|
+
|
|
272
292
|
function parseEmbedding(value: unknown): number[] | null {
|
|
273
293
|
if (typeof value !== "string") {
|
|
274
294
|
return null;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export const KNOWLEDGE_MIGRATION_ID = "0006_create_knowledge_tables";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type KnowledgeSchemaDatabase = {
|
|
4
|
+
exec(sql: string): unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type KnowledgeLexicalIndexMode = "fts5" | "fallback";
|
|
8
|
+
|
|
9
|
+
export const KNOWLEDGE_BASE_SCHEMA_SQL = `
|
|
4
10
|
CREATE TABLE IF NOT EXISTS agentkit_knowledge_sources (
|
|
5
11
|
id TEXT PRIMARY KEY,
|
|
6
12
|
source_key TEXT NOT NULL UNIQUE,
|
|
@@ -43,7 +49,9 @@ export const KNOWLEDGE_SCHEMA_SQL = `
|
|
|
43
49
|
|
|
44
50
|
CREATE INDEX IF NOT EXISTS agentkit_knowledge_chunks_source_ordinal_idx
|
|
45
51
|
ON agentkit_knowledge_chunks(source_id, ordinal);
|
|
52
|
+
`;
|
|
46
53
|
|
|
54
|
+
export const KNOWLEDGE_FTS5_SCHEMA_SQL = `
|
|
47
55
|
CREATE VIRTUAL TABLE IF NOT EXISTS agentkit_knowledge_fts
|
|
48
56
|
USING fts5(
|
|
49
57
|
chunk_id UNINDEXED,
|
|
@@ -54,3 +62,39 @@ export const KNOWLEDGE_SCHEMA_SQL = `
|
|
|
54
62
|
tokenize='unicode61'
|
|
55
63
|
);
|
|
56
64
|
`;
|
|
65
|
+
|
|
66
|
+
export const KNOWLEDGE_FTS_FALLBACK_SCHEMA_SQL = `
|
|
67
|
+
CREATE TABLE IF NOT EXISTS agentkit_knowledge_fts (
|
|
68
|
+
chunk_id TEXT PRIMARY KEY,
|
|
69
|
+
source_id TEXT NOT NULL,
|
|
70
|
+
title TEXT,
|
|
71
|
+
section TEXT,
|
|
72
|
+
content TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS agentkit_knowledge_fts_source_idx
|
|
76
|
+
ON agentkit_knowledge_fts(source_id);
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
export const KNOWLEDGE_SCHEMA_SQL = `${KNOWLEDGE_BASE_SCHEMA_SQL}\n${KNOWLEDGE_FTS5_SCHEMA_SQL}`;
|
|
80
|
+
|
|
81
|
+
export function applyKnowledgeSchema(db: KnowledgeSchemaDatabase): KnowledgeLexicalIndexMode {
|
|
82
|
+
db.exec(KNOWLEDGE_BASE_SCHEMA_SQL);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
db.exec(KNOWLEDGE_FTS5_SCHEMA_SQL);
|
|
86
|
+
return "fts5";
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!isMissingFts5ModuleError(error)) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
db.exec(KNOWLEDGE_FTS_FALLBACK_SCHEMA_SQL);
|
|
94
|
+
return "fallback";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isMissingFts5ModuleError(error: unknown): boolean {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return /no such module:\s*fts5/i.test(message) || /unknown module:\s*fts5/i.test(message);
|
|
100
|
+
}
|
|
@@ -2,7 +2,7 @@ import { access } from "node:fs/promises";
|
|
|
2
2
|
import { basename, dirname, extname, join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
|
-
import { createClient } from "@libsql/client";
|
|
5
|
+
import { createClient, type InStatement } from "@libsql/client";
|
|
6
6
|
|
|
7
7
|
import type { DatabaseRow } from "../../index";
|
|
8
8
|
|
|
@@ -48,32 +48,6 @@ export async function rebuildLocalKnowledgeVectorIndex(options: {
|
|
|
48
48
|
const dimensions = options.dimensions;
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
-
await vectorClient.batch(
|
|
52
|
-
[
|
|
53
|
-
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_INDEX}`,
|
|
54
|
-
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_SOURCE_INDEX}`,
|
|
55
|
-
`DROP TABLE IF EXISTS ${KNOWLEDGE_VECTOR_TABLE}`,
|
|
56
|
-
`
|
|
57
|
-
CREATE TABLE ${KNOWLEDGE_VECTOR_TABLE} (
|
|
58
|
-
id INTEGER PRIMARY KEY,
|
|
59
|
-
chunk_id TEXT NOT NULL UNIQUE,
|
|
60
|
-
source_id TEXT NOT NULL,
|
|
61
|
-
source_key TEXT NOT NULL,
|
|
62
|
-
content TEXT NOT NULL,
|
|
63
|
-
title TEXT,
|
|
64
|
-
section TEXT,
|
|
65
|
-
locator TEXT,
|
|
66
|
-
embedding F32_BLOB(${dimensions}) NOT NULL,
|
|
67
|
-
embedding_hash TEXT NOT NULL,
|
|
68
|
-
created_at TEXT NOT NULL
|
|
69
|
-
)
|
|
70
|
-
`,
|
|
71
|
-
`CREATE INDEX ${KNOWLEDGE_VECTOR_SOURCE_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(source_id)`,
|
|
72
|
-
`CREATE INDEX ${KNOWLEDGE_VECTOR_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(libsql_vector_idx(embedding, 'metric=cosine'))`,
|
|
73
|
-
],
|
|
74
|
-
"write",
|
|
75
|
-
);
|
|
76
|
-
|
|
77
51
|
const rows = await mainClient.execute({
|
|
78
52
|
sql: `
|
|
79
53
|
SELECT
|
|
@@ -97,7 +71,31 @@ export async function rebuildLocalKnowledgeVectorIndex(options: {
|
|
|
97
71
|
args: [dimensions],
|
|
98
72
|
});
|
|
99
73
|
|
|
100
|
-
|
|
74
|
+
// Drop, recreate, and refill in one write batch so a failed rebuild never
|
|
75
|
+
// leaves a half-populated index behind, and inserts share one transaction.
|
|
76
|
+
const schemaStatements: InStatement[] = [
|
|
77
|
+
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_INDEX}`,
|
|
78
|
+
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_SOURCE_INDEX}`,
|
|
79
|
+
`DROP TABLE IF EXISTS ${KNOWLEDGE_VECTOR_TABLE}`,
|
|
80
|
+
`
|
|
81
|
+
CREATE TABLE ${KNOWLEDGE_VECTOR_TABLE} (
|
|
82
|
+
id INTEGER PRIMARY KEY,
|
|
83
|
+
chunk_id TEXT NOT NULL UNIQUE,
|
|
84
|
+
source_id TEXT NOT NULL,
|
|
85
|
+
source_key TEXT NOT NULL,
|
|
86
|
+
content TEXT NOT NULL,
|
|
87
|
+
title TEXT,
|
|
88
|
+
section TEXT,
|
|
89
|
+
locator TEXT,
|
|
90
|
+
embedding F32_BLOB(${dimensions}) NOT NULL,
|
|
91
|
+
embedding_hash TEXT NOT NULL,
|
|
92
|
+
created_at TEXT NOT NULL
|
|
93
|
+
)
|
|
94
|
+
`,
|
|
95
|
+
`CREATE INDEX ${KNOWLEDGE_VECTOR_SOURCE_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(source_id)`,
|
|
96
|
+
`CREATE INDEX ${KNOWLEDGE_VECTOR_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(libsql_vector_idx(embedding, 'metric=cosine'))`,
|
|
97
|
+
];
|
|
98
|
+
const inserts: InStatement[] = [];
|
|
101
99
|
|
|
102
100
|
for (const row of rows.rows) {
|
|
103
101
|
const chunkId = readString(row, "chunk_id");
|
|
@@ -112,7 +110,7 @@ export async function rebuildLocalKnowledgeVectorIndex(options: {
|
|
|
112
110
|
continue;
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
|
|
113
|
+
inserts.push({
|
|
116
114
|
sql: `
|
|
117
115
|
INSERT INTO ${KNOWLEDGE_VECTOR_TABLE} (
|
|
118
116
|
chunk_id,
|
|
@@ -141,9 +139,11 @@ export async function rebuildLocalKnowledgeVectorIndex(options: {
|
|
|
141
139
|
createdAt,
|
|
142
140
|
],
|
|
143
141
|
});
|
|
144
|
-
chunksIndexed += 1;
|
|
145
142
|
}
|
|
146
143
|
|
|
144
|
+
await vectorClient.batch([...schemaStatements, ...inserts], "write");
|
|
145
|
+
const chunksIndexed = inserts.length;
|
|
146
|
+
|
|
147
147
|
await vectorClient.execute("PRAGMA wal_checkpoint(TRUNCATE)").catch(() => undefined);
|
|
148
148
|
|
|
149
149
|
return {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export type RuntimePromptContextOptions = {
|
|
2
|
+
timeZone?: string | null;
|
|
3
|
+
env?: Record<string, string | undefined>;
|
|
4
|
+
now?: Date;
|
|
5
|
+
clock?: RuntimeClockContext;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type RuntimePromptContext = {
|
|
9
|
+
isoTimestamp: string;
|
|
10
|
+
timeZone: string;
|
|
11
|
+
localDate: string;
|
|
12
|
+
localWeekday: string;
|
|
13
|
+
localDateTime: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RuntimeClockContext = RuntimePromptContext & {
|
|
17
|
+
now: Date;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const FALLBACK_TIME_ZONE = "UTC";
|
|
21
|
+
const LOCALE = "en-US";
|
|
22
|
+
|
|
23
|
+
export function appendRuntimePromptContext(
|
|
24
|
+
instructions: string,
|
|
25
|
+
options: RuntimePromptContextOptions = {},
|
|
26
|
+
): string {
|
|
27
|
+
const context = options.clock ?? buildRuntimePromptContext(options);
|
|
28
|
+
|
|
29
|
+
return `${instructions.trimEnd()}
|
|
30
|
+
|
|
31
|
+
Runtime context:
|
|
32
|
+
- Current timestamp: ${context.isoTimestamp}
|
|
33
|
+
- Time zone: ${context.timeZone}
|
|
34
|
+
- Local date: ${context.localDate}
|
|
35
|
+
- Local weekday: ${context.localWeekday}
|
|
36
|
+
- Local date and time: ${context.localDateTime}
|
|
37
|
+
|
|
38
|
+
Use this runtime context to interpret relative dates and times such as today, tomorrow, next Friday, and morning/afternoon. If a scheduling request depends on a different user or business time zone, ask for confirmation before booking.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildRuntimePromptContext(options: RuntimePromptContextOptions = {}): RuntimePromptContext {
|
|
42
|
+
const clock = buildRuntimeClockContext(options);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
isoTimestamp: clock.isoTimestamp,
|
|
46
|
+
timeZone: clock.timeZone,
|
|
47
|
+
localDate: clock.localDate,
|
|
48
|
+
localWeekday: clock.localWeekday,
|
|
49
|
+
localDateTime: clock.localDateTime,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildRuntimeClockContext(options: RuntimePromptContextOptions = {}): RuntimeClockContext {
|
|
54
|
+
if (options.clock) {
|
|
55
|
+
return options.clock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = new Date((options.now ?? new Date()).getTime());
|
|
59
|
+
|
|
60
|
+
if (Number.isNaN(now.getTime())) {
|
|
61
|
+
throw new Error("runtime prompt context requires a valid Date.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const timeZone = resolveRuntimeTimeZone(options);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
now,
|
|
68
|
+
isoTimestamp: now.toISOString(),
|
|
69
|
+
timeZone,
|
|
70
|
+
localDate: formatLocalDate(now, timeZone),
|
|
71
|
+
localWeekday: formatDateTime(now, timeZone, {
|
|
72
|
+
weekday: "long",
|
|
73
|
+
}),
|
|
74
|
+
localDateTime: formatDateTime(now, timeZone, {
|
|
75
|
+
weekday: "long",
|
|
76
|
+
year: "numeric",
|
|
77
|
+
month: "long",
|
|
78
|
+
day: "numeric",
|
|
79
|
+
hour: "numeric",
|
|
80
|
+
minute: "2-digit",
|
|
81
|
+
timeZoneName: "short",
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveRuntimeTimeZone(options: RuntimePromptContextOptions = {}): string {
|
|
87
|
+
if (options.timeZone?.trim()) {
|
|
88
|
+
return normalizeTimeZone(options.timeZone, "timeZone");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const explicitEnvTimeZone = options.env?.AGENTKIT_TIME_ZONE?.trim();
|
|
92
|
+
|
|
93
|
+
if (explicitEnvTimeZone) {
|
|
94
|
+
return normalizeTimeZone(explicitEnvTimeZone, "AGENTKIT_TIME_ZONE");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const processTimeZone = options.env?.TZ?.trim();
|
|
98
|
+
|
|
99
|
+
if (processTimeZone && isValidTimeZone(processTimeZone)) {
|
|
100
|
+
return processTimeZone;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return normalizeTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || FALLBACK_TIME_ZONE, "runtime time zone");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isValidTimeZone(value: string): boolean {
|
|
107
|
+
try {
|
|
108
|
+
new Intl.DateTimeFormat(LOCALE, { timeZone: value }).format(new Date(0));
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTimeZone(value: string, source: string): string {
|
|
116
|
+
const timeZone = value.trim();
|
|
117
|
+
|
|
118
|
+
if (!isValidTimeZone(timeZone)) {
|
|
119
|
+
throw new Error(`${source} must be a valid IANA time zone such as "UTC" or "America/New_York".`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return timeZone;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatLocalDate(date: Date, timeZone: string): string {
|
|
126
|
+
const parts = new Intl.DateTimeFormat(LOCALE, {
|
|
127
|
+
timeZone,
|
|
128
|
+
year: "numeric",
|
|
129
|
+
month: "2-digit",
|
|
130
|
+
day: "2-digit",
|
|
131
|
+
}).formatToParts(date);
|
|
132
|
+
const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
133
|
+
return `${byType.year}-${byType.month}-${byType.day}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatDateTime(date: Date, timeZone: string, options: Intl.DateTimeFormatOptions): string {
|
|
137
|
+
return new Intl.DateTimeFormat(LOCALE, {
|
|
138
|
+
...options,
|
|
139
|
+
timeZone,
|
|
140
|
+
}).format(date);
|
|
141
|
+
}
|
|
@@ -7,6 +7,10 @@ export type RuntimeContractTarget = {
|
|
|
7
7
|
|
|
8
8
|
export type RuntimeContractOptions = {
|
|
9
9
|
expectedAgentName: string;
|
|
10
|
+
auth?: {
|
|
11
|
+
chat?: HeadersInit;
|
|
12
|
+
admin?: HeadersInit;
|
|
13
|
+
};
|
|
10
14
|
tool: {
|
|
11
15
|
name: string;
|
|
12
16
|
input: unknown;
|
|
@@ -20,20 +24,74 @@ export async function assertRuntimeContract(
|
|
|
20
24
|
): Promise<void> {
|
|
21
25
|
const baseUrl = target.baseUrl.replace(/\/$/, "");
|
|
22
26
|
|
|
23
|
-
const root = await fetch(`${baseUrl}
|
|
27
|
+
const root = await fetch(`${baseUrl}/`, { headers: options.auth?.chat });
|
|
24
28
|
expect(root.status).toBe(200);
|
|
25
29
|
|
|
26
30
|
const health = await fetchJson(`${baseUrl}/health`);
|
|
27
31
|
expect(health.status).toBe(200);
|
|
28
32
|
expect(health.body).toMatchObject({ status: "ok" });
|
|
29
33
|
|
|
30
|
-
const inspect = await fetchJson(`${baseUrl}/_agentkit
|
|
34
|
+
const inspect = await fetchJson(`${baseUrl}/_agentkit`, { headers: options.auth?.admin });
|
|
31
35
|
expect(inspect.status).toBe(200);
|
|
32
36
|
expect(inspect.body).toMatchObject({ agent: options.expectedAgentName });
|
|
33
37
|
|
|
38
|
+
if (options.auth) {
|
|
39
|
+
const missingAdmin = await fetchJson(`${baseUrl}/_agentkit`);
|
|
40
|
+
expect(missingAdmin.status).toBe(401);
|
|
41
|
+
expect(missingAdmin.body.error.code).toBe("unauthorized");
|
|
42
|
+
|
|
43
|
+
const invalidAdmin = await fetchJson(`${baseUrl}/_agentkit`, {
|
|
44
|
+
headers: { Authorization: "Bearer invalid-admin-token" },
|
|
45
|
+
});
|
|
46
|
+
expect(invalidAdmin.status).toBe(401);
|
|
47
|
+
expect(invalidAdmin.body.error.code).toBe("unauthorized");
|
|
48
|
+
|
|
49
|
+
const missingChat = await fetchJson(`${baseUrl}/chat`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ message: "hello" }),
|
|
53
|
+
});
|
|
54
|
+
expect(missingChat.status).toBe(401);
|
|
55
|
+
expect(missingChat.body.error.code).toBe("unauthorized");
|
|
56
|
+
|
|
57
|
+
const invalidChat = await fetchJson(`${baseUrl}/chat`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "content-type": "application/json", Authorization: "Bearer invalid-chat-token" },
|
|
60
|
+
body: JSON.stringify({ message: "hello" }),
|
|
61
|
+
});
|
|
62
|
+
expect(invalidChat.status).toBe(401);
|
|
63
|
+
expect(invalidChat.body.error.code).toBe("unauthorized");
|
|
64
|
+
|
|
65
|
+
const missingTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ input: options.tool.input }),
|
|
69
|
+
});
|
|
70
|
+
expect(missingTool.status).toBe(401);
|
|
71
|
+
expect(missingTool.body.error.code).toBe("unauthorized");
|
|
72
|
+
|
|
73
|
+
const invalidTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json", Authorization: "Bearer invalid-admin-token" },
|
|
76
|
+
body: JSON.stringify({ input: options.tool.input }),
|
|
77
|
+
});
|
|
78
|
+
expect(invalidTool.status).toBe(401);
|
|
79
|
+
expect(invalidTool.body.error.code).toBe("unauthorized");
|
|
80
|
+
|
|
81
|
+
const missingConversations = await fetchJson(`${baseUrl}/v1/conversations`);
|
|
82
|
+
expect(missingConversations.status).toBe(401);
|
|
83
|
+
expect(missingConversations.body.error.code).toBe("unauthorized");
|
|
84
|
+
|
|
85
|
+
const invalidConversations = await fetchJson(`${baseUrl}/v1/conversations`, {
|
|
86
|
+
headers: { Authorization: "Bearer invalid-admin-token" },
|
|
87
|
+
});
|
|
88
|
+
expect(invalidConversations.status).toBe(401);
|
|
89
|
+
expect(invalidConversations.body.error.code).toBe("unauthorized");
|
|
90
|
+
}
|
|
91
|
+
|
|
34
92
|
const unauthorized = await fetchJson(`${baseUrl}/chat`, {
|
|
35
93
|
method: "POST",
|
|
36
|
-
headers: { "content-type": "application/json" },
|
|
94
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
37
95
|
body: JSON.stringify({ message: { role: "assistant", content: "not allowed" } }),
|
|
38
96
|
});
|
|
39
97
|
expect(unauthorized.status).toBe(400);
|
|
@@ -41,7 +99,7 @@ export async function assertRuntimeContract(
|
|
|
41
99
|
|
|
42
100
|
const chat = await fetchJson(`${baseUrl}/chat`, {
|
|
43
101
|
method: "POST",
|
|
44
|
-
headers: { "content-type": "application/json" },
|
|
102
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
45
103
|
body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
|
|
46
104
|
});
|
|
47
105
|
expect(chat.status).toBe(200);
|
|
@@ -55,7 +113,7 @@ export async function assertRuntimeContract(
|
|
|
55
113
|
|
|
56
114
|
const continuedChat = await fetchJson(`${baseUrl}/chat`, {
|
|
57
115
|
method: "POST",
|
|
58
|
-
headers: { "content-type": "application/json" },
|
|
116
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
59
117
|
body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
|
|
60
118
|
});
|
|
61
119
|
expect(continuedChat.status).toBe(200);
|
|
@@ -69,7 +127,7 @@ export async function assertRuntimeContract(
|
|
|
69
127
|
|
|
70
128
|
const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
71
129
|
method: "POST",
|
|
72
|
-
headers: { "content-type": "application/json" },
|
|
130
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.admin),
|
|
73
131
|
body: JSON.stringify({ input: options.tool.input }),
|
|
74
132
|
});
|
|
75
133
|
expect(tool.status).toBe(200);
|
|
@@ -78,7 +136,7 @@ export async function assertRuntimeContract(
|
|
|
78
136
|
output: options.tool.expectedOutput,
|
|
79
137
|
});
|
|
80
138
|
|
|
81
|
-
const conversations = await fetchJson(`${baseUrl}/v1/conversations
|
|
139
|
+
const conversations = await fetchJson(`${baseUrl}/v1/conversations`, { headers: options.auth?.admin });
|
|
82
140
|
expect(conversations.status).toBe(200);
|
|
83
141
|
expect(conversations.body.conversations.length).toBeGreaterThanOrEqual(1);
|
|
84
142
|
expect(conversations.body.conversations.some((conversation: { id: string }) => conversation.id === chat.body.conversationId)).toBe(true);
|
|
@@ -91,3 +149,9 @@ async function fetchJson(url: string, init?: RequestInit): Promise<{ status: num
|
|
|
91
149
|
body: await response.json(),
|
|
92
150
|
};
|
|
93
151
|
}
|
|
152
|
+
|
|
153
|
+
function withHeaders(required: HeadersInit, extra: HeadersInit | undefined): Headers {
|
|
154
|
+
const headers = new Headers(extra);
|
|
155
|
+
new Headers(required).forEach((value, key) => headers.set(key, value));
|
|
156
|
+
return headers;
|
|
157
|
+
}
|