@theokit/cli 3.0.2 → 4.0.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/CHANGELOG.md +257 -0
- package/LICENSE +2 -2
- package/README.md +13 -0
- package/dist/bin/theokit.cjs +59 -28
- package/dist/bin/theokit.cjs.map +1 -1
- package/dist/bin/theokit.js +59 -28
- package/dist/bin/theokit.js.map +1 -1
- package/dist/index.cjs +59 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -10
- package/dist/index.d.ts +127 -10
- package/dist/index.js +59 -28
- package/dist/index.js.map +1 -1
- package/package.json +19 -16
- package/templates/chatbot/.env.example +14 -0
- package/templates/chatbot/README.md +34 -0
- package/templates/chatbot/package.json +20 -0
- package/templates/chatbot/src/index.ts +88 -0
- package/templates/chatbot/tsconfig.json +12 -0
- package/templates/minimal/README.md +1 -1
- package/templates/multi-agent/.env.example +14 -0
- package/templates/multi-agent/README.md +33 -0
- package/templates/multi-agent/package.json +20 -0
- package/templates/multi-agent/src/index.ts +90 -0
- package/templates/multi-agent/tsconfig.json +12 -0
- package/templates/rag-agent/.env.example +14 -0
- package/templates/rag-agent/README.md +34 -0
- package/templates/rag-agent/package.json +21 -0
- package/templates/rag-agent/src/index.ts +115 -0
- package/templates/rag-agent/tsconfig.json +12 -0
- package/templates/telegram-bot/README.md +4 -4
- package/templates/telegram-bot/package.json +2 -2
- package/templates/telegram-bot/src/index.ts +2 -2
- package/templates/workflow-automation/.env.example +14 -0
- package/templates/workflow-automation/README.md +33 -0
- package/templates/workflow-automation/package.json +20 -0
- package/templates/workflow-automation/src/index.ts +86 -0
- package/templates/workflow-automation/tsconfig.json +12 -0
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theokit/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Developer CLI for @theokit/sdk — init, dev, inspect, eval. Adoption Roadmap #1.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/usetheokit/theokit-sdk#readme",
|
|
7
|
+
"bugs": "https://github.com/usetheokit/theokit-sdk/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/usetheokit/theokit-sdk.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
6
13
|
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
7
15
|
"engines": {
|
|
8
16
|
"node": ">=22.12.0"
|
|
9
17
|
},
|
|
10
18
|
"main": "./dist/index.cjs",
|
|
11
19
|
"module": "./dist/index.js",
|
|
12
20
|
"types": "./dist/index.d.ts",
|
|
13
|
-
"bin": {
|
|
14
|
-
"theokit": "./dist/bin/theokit.js"
|
|
15
|
-
},
|
|
16
21
|
"exports": {
|
|
17
22
|
".": {
|
|
18
23
|
"import": {
|
|
@@ -32,16 +37,19 @@
|
|
|
32
37
|
"LICENSE",
|
|
33
38
|
"CHANGELOG.md"
|
|
34
39
|
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"provenance": true,
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
35
44
|
"dependencies": {
|
|
36
45
|
"@clack/prompts": "^0.7.0",
|
|
37
46
|
"commander": "^12.1.0",
|
|
38
47
|
"picocolors": "^1.0.0",
|
|
39
48
|
"tsx": "^4.19.0",
|
|
40
|
-
"
|
|
41
|
-
"@theokit/sdk": "^4.43.0"
|
|
49
|
+
"@theokit/sdk": "^4.57.0"
|
|
42
50
|
},
|
|
43
51
|
"peerDependencies": {
|
|
44
|
-
"@theokit/acp": "^
|
|
52
|
+
"@theokit/acp": "^4.0.0"
|
|
45
53
|
},
|
|
46
54
|
"peerDependenciesMeta": {
|
|
47
55
|
"@theokit/acp": {
|
|
@@ -53,18 +61,13 @@
|
|
|
53
61
|
"tsup": "^8.5.0",
|
|
54
62
|
"typescript": "^5.8.0",
|
|
55
63
|
"vitest": "^4.1.8",
|
|
56
|
-
"@theokit/acp": "
|
|
64
|
+
"@theokit/acp": "4.0.0"
|
|
57
65
|
},
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
},
|
|
61
|
-
"repository": {
|
|
62
|
-
"type": "git",
|
|
63
|
-
"url": "git+https://github.com/usetheodev/theokit-sdk.git",
|
|
64
|
-
"directory": "packages/cli"
|
|
66
|
+
"bin": {
|
|
67
|
+
"theokit": "./dist/bin/theokit.js"
|
|
65
68
|
},
|
|
66
69
|
"scripts": {
|
|
67
|
-
"build": "tsup",
|
|
70
|
+
"build": "tsup && node ../../tools/repair-dts-imports.mjs .",
|
|
68
71
|
"typecheck": "tsc --noEmit",
|
|
69
72
|
"test": "vitest run"
|
|
70
73
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copy to .env and fill in:
|
|
2
|
+
#
|
|
3
|
+
# THEOKIT_API_KEY — any non-empty string. Used by the SDK for cloud
|
|
4
|
+
# catalog reads; not strictly required for chat against Anthropic/OpenAI
|
|
5
|
+
# directly. Set to "local" if you're not using Theo cloud.
|
|
6
|
+
THEOKIT_API_KEY=local
|
|
7
|
+
|
|
8
|
+
# Provider key — pick ONE. The SDK auto-detects which one is set.
|
|
9
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
10
|
+
# OPENAI_API_KEY=sk-...
|
|
11
|
+
# OPENROUTER_API_KEY=sk-or-...
|
|
12
|
+
|
|
13
|
+
# Override the model (default: anthropic/claude-3-5-sonnet-latest).
|
|
14
|
+
# AGENT_MODEL=openai/gpt-4o-mini
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A conversational agent that remembers, scaffolded by `theokit init`.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
# Edit .env — set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY
|
|
11
|
+
pnpm dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this does
|
|
15
|
+
|
|
16
|
+
1. `Agent.getOrCreate(id, …)` — a stable id, so a second run resumes the same
|
|
17
|
+
conversation instead of starting a fresh one.
|
|
18
|
+
2. Reads a line, streams the reply, loops.
|
|
19
|
+
3. Reports `result.status === "error"` with the provider's reason, so a run that
|
|
20
|
+
ends without answering says why.
|
|
21
|
+
|
|
22
|
+
Persistence needs no configuration: every finished turn is appended to a session
|
|
23
|
+
transcript automatically. `SESSION_DIR` only chooses where it lands.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node 22.12+.
|
|
28
|
+
- One of: Anthropic / OpenAI / OpenRouter API key.
|
|
29
|
+
|
|
30
|
+
## Next steps
|
|
31
|
+
|
|
32
|
+
- `SESSION_DIR=~/.claude` writes sessions the Claude Code CLI can `--continue`.
|
|
33
|
+
- `AGENT_ID=support` runs a second, independent conversation from the same code.
|
|
34
|
+
- Add tools with `Tool.create({ name, description, inputSchema, handler })`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx --env-file=.env src/index.ts",
|
|
8
|
+
"start": "tsx --env-file=.env src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@theokit/sdk": "^{{sdkVersion}}"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"tsx": "^4.19.0",
|
|
15
|
+
"typescript": "^5.8.0"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.12.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{projectName}} — a conversational agent that remembers.
|
|
3
|
+
*
|
|
4
|
+
* Reads a line, streams the reply, and keeps the thread going. Every finished
|
|
5
|
+
* turn is persisted automatically, so the conversation survives a restart —
|
|
6
|
+
* there is nothing to wire for that. `local.sessionDir` only chooses WHERE the
|
|
7
|
+
* transcript lives; point it at `~/.claude` and the Claude Code CLI can
|
|
8
|
+
* `--continue` the same session.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
import { Agent, type Run } from "@theokit/sdk";
|
|
13
|
+
|
|
14
|
+
const API_KEY = process.env.THEOKIT_API_KEY ?? "local";
|
|
15
|
+
const MODEL = process.env.AGENT_MODEL ?? "anthropic/claude-3-5-sonnet-latest";
|
|
16
|
+
|
|
17
|
+
/** A stable id is what makes the next run resume this conversation instead of starting a new one. */
|
|
18
|
+
const AGENT_ID = process.env.AGENT_ID ?? "chatbot";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Drain one run's assistant stream to stdout and hand back the text.
|
|
22
|
+
*
|
|
23
|
+
* Extracted because the loop is the same in every template and nests three deep inside whatever
|
|
24
|
+
* calls it — reading it once here beats re-reading it inside each caller.
|
|
25
|
+
*/
|
|
26
|
+
async function streamReply(run: Run): Promise<string> {
|
|
27
|
+
let text = "";
|
|
28
|
+
for await (const event of run.stream()) {
|
|
29
|
+
if (event.type !== "assistant") continue;
|
|
30
|
+
for (const part of event.message.content) {
|
|
31
|
+
if (part.type === "text") {
|
|
32
|
+
process.stdout.write(part.text);
|
|
33
|
+
text += part.text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main(): Promise<void> {
|
|
41
|
+
// `getOrCreate` — not `create`. On a second run it reattaches to the stored
|
|
42
|
+
// transcript for this id; `create` would start a fresh thread each time.
|
|
43
|
+
const agent = await Agent.getOrCreate(AGENT_ID, {
|
|
44
|
+
apiKey: API_KEY,
|
|
45
|
+
model: { id: MODEL },
|
|
46
|
+
local: {
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
...(process.env.SESSION_DIR !== undefined ? { sessionDir: process.env.SESSION_DIR } : {}),
|
|
49
|
+
},
|
|
50
|
+
systemPrompt:
|
|
51
|
+
"You are a friendly assistant. Keep answers to one to three sentences, " +
|
|
52
|
+
"and use what was said earlier in the conversation.",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
console.log(`${AGENT_ID} ready · model ${MODEL} · Ctrl+C to quit\n`);
|
|
57
|
+
|
|
58
|
+
// A `for await` over the prompt, not a recursive callback: the loop below
|
|
59
|
+
// reads as the conversation it is, and an error in one turn does not bury the
|
|
60
|
+
// stack under every turn before it.
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const line = (await rl.question("You: ")).trim();
|
|
64
|
+
if (line.length === 0) continue;
|
|
65
|
+
|
|
66
|
+
const run = await agent.send(line);
|
|
67
|
+
process.stdout.write("Bot: ");
|
|
68
|
+
await streamReply(run);
|
|
69
|
+
|
|
70
|
+
const result = await run.wait();
|
|
71
|
+
process.stdout.write("\n\n");
|
|
72
|
+
|
|
73
|
+
// A run can END without succeeding. Reporting the reason here is the
|
|
74
|
+
// difference between "the bot went quiet" and a diagnosis.
|
|
75
|
+
if (result.status === "error") {
|
|
76
|
+
console.error(`run failed: ${result.error?.message ?? "no reason reported"}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
rl.close();
|
|
81
|
+
await agent.dispose();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((cause) => {
|
|
86
|
+
console.error("chatbot failed:", cause instanceof Error ? cause.message : cause);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
@@ -24,6 +24,6 @@ pnpm dev
|
|
|
24
24
|
|
|
25
25
|
## Next steps
|
|
26
26
|
|
|
27
|
-
- Add custom tools
|
|
27
|
+
- Add custom tools with `Tool.create({ name, description, inputSchema, handler })`.
|
|
28
28
|
- Switch to a local Ollama model by setting `AGENT_MODEL=ollama/llama3.2:3b`.
|
|
29
29
|
- Wire to a chat platform via `@theokit/gateway`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copy to .env and fill in:
|
|
2
|
+
#
|
|
3
|
+
# THEOKIT_API_KEY — any non-empty string. Used by the SDK for cloud
|
|
4
|
+
# catalog reads; not strictly required for chat against Anthropic/OpenAI
|
|
5
|
+
# directly. Set to "local" if you're not using Theo cloud.
|
|
6
|
+
THEOKIT_API_KEY=local
|
|
7
|
+
|
|
8
|
+
# Provider key — pick ONE. The SDK auto-detects which one is set.
|
|
9
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
10
|
+
# OPENAI_API_KEY=sk-...
|
|
11
|
+
# OPENROUTER_API_KEY=sk-or-...
|
|
12
|
+
|
|
13
|
+
# Override the model (default: anthropic/claude-3-5-sonnet-latest).
|
|
14
|
+
# AGENT_MODEL=openai/gpt-4o-mini
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A router and two specialists, scaffolded by `theokit init`.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
pnpm dev "Translate to French: good morning"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What this does
|
|
14
|
+
|
|
15
|
+
1. `AgentFactory.create({ … })` captures the configuration all three agents
|
|
16
|
+
share — key, model, workspace.
|
|
17
|
+
2. A classifier reads the input and names the specialist.
|
|
18
|
+
3. `factory.forSession(id, { systemPrompt })` builds that specialist; only the
|
|
19
|
+
prompt differs.
|
|
20
|
+
|
|
21
|
+
Writing `Agent.create` three times would work, and would drift the shared half
|
|
22
|
+
apart on the first edit. The factory is what keeps one prefix in one place.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node 22.12+.
|
|
27
|
+
- One of: Anthropic / OpenAI / OpenRouter API key.
|
|
28
|
+
|
|
29
|
+
## Next steps
|
|
30
|
+
|
|
31
|
+
- Swap `forSession` for `getOrCreate` when a specialist should remember earlier
|
|
32
|
+
turns.
|
|
33
|
+
- Add a specialist by adding one entry to `SPECIALISTS` — nothing else changes.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx --env-file=.env src/index.ts",
|
|
8
|
+
"start": "tsx --env-file=.env src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@theokit/sdk": "^{{sdkVersion}}"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"tsx": "^4.19.0",
|
|
15
|
+
"typescript": "^5.8.0"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.12.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{projectName}} — a router and two specialists.
|
|
3
|
+
*
|
|
4
|
+
* A classifier reads the input and names the specialist; that specialist answers.
|
|
5
|
+
* All three share one configuration prefix through `AgentFactory`, so the only
|
|
6
|
+
* thing that varies per agent is its system prompt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AgentFactory, type Run } from "@theokit/sdk";
|
|
10
|
+
|
|
11
|
+
const API_KEY = process.env.THEOKIT_API_KEY ?? "local";
|
|
12
|
+
const MODEL = process.env.AGENT_MODEL ?? "anthropic/claude-3-5-sonnet-latest";
|
|
13
|
+
|
|
14
|
+
/** The whole difference between the three agents. */
|
|
15
|
+
const SPECIALISTS = {
|
|
16
|
+
classifier:
|
|
17
|
+
'Reply with exactly one word and nothing else: "summarize" if the user wants a ' +
|
|
18
|
+
'summary, "translate" if they want a translation, otherwise "summarize".',
|
|
19
|
+
summarizer: "Condense the input into two or three bullet points. No preamble.",
|
|
20
|
+
translator:
|
|
21
|
+
"Translate the input to the language the user names. If none is named, " +
|
|
22
|
+
"translate to English. Reply with the translation only.",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
type Specialist = keyof typeof SPECIALISTS;
|
|
26
|
+
|
|
27
|
+
// One prefix, many agents — this is what `AgentFactory` is for. Writing
|
|
28
|
+
// `Agent.create` three times would drift the shared half apart on the first edit.
|
|
29
|
+
const factory = AgentFactory.create({
|
|
30
|
+
apiKey: API_KEY,
|
|
31
|
+
model: { id: MODEL },
|
|
32
|
+
local: { cwd: process.cwd() },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Drain one run's assistant stream and hand back the text.
|
|
37
|
+
*
|
|
38
|
+
* Extracted because the loop is the same in every template and nests three deep inside whatever
|
|
39
|
+
* calls it — reading it once here beats re-reading it inside each caller.
|
|
40
|
+
*/
|
|
41
|
+
async function streamReply(run: Run): Promise<string> {
|
|
42
|
+
let text = "";
|
|
43
|
+
for await (const event of run.stream()) {
|
|
44
|
+
if (event.type !== "assistant") continue;
|
|
45
|
+
for (const part of event.message.content) {
|
|
46
|
+
if (part.type === "text") {
|
|
47
|
+
text += part.text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function ask(role: Specialist, message: string): Promise<string> {
|
|
55
|
+
// `forSession` — a fresh agent per call. Use `getOrCreate` instead when the
|
|
56
|
+
// specialist should remember earlier turns.
|
|
57
|
+
const agent = await factory.forSession(`multi-${role}`, {
|
|
58
|
+
systemPrompt: SPECIALISTS[role],
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const run = await agent.send(message);
|
|
62
|
+
const reply = await streamReply(run);
|
|
63
|
+
const result = await run.wait();
|
|
64
|
+
if (result.status === "error") {
|
|
65
|
+
throw new Error(`${role} failed: ${result.error?.message ?? "no reason reported"}`);
|
|
66
|
+
}
|
|
67
|
+
return reply.trim();
|
|
68
|
+
} finally {
|
|
69
|
+
await agent.dispose();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main(): Promise<void> {
|
|
74
|
+
const input = process.argv[2] ?? "Summarize: the quick brown fox jumps over the lazy dog.";
|
|
75
|
+
console.log(`Input: ${input}\n`);
|
|
76
|
+
|
|
77
|
+
const verdict = (await ask("classifier", input)).toLowerCase();
|
|
78
|
+
// The model is asked for one word and usually gives one. `includes` rather
|
|
79
|
+
// than `===` so a stray period or quote does not silently route everything
|
|
80
|
+
// to the fallback — the failure would look like a bad classifier.
|
|
81
|
+
const role: Specialist = verdict.includes("translate") ? "translator" : "summarizer";
|
|
82
|
+
console.log(`Router chose: ${role} (said "${verdict}")\n`);
|
|
83
|
+
|
|
84
|
+
console.log(await ask(role, input));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch((cause) => {
|
|
88
|
+
console.error("multi-agent failed:", cause instanceof Error ? cause.message : cause);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copy to .env and fill in:
|
|
2
|
+
#
|
|
3
|
+
# THEOKIT_API_KEY — any non-empty string. Used by the SDK for cloud
|
|
4
|
+
# catalog reads; not strictly required for chat against Anthropic/OpenAI
|
|
5
|
+
# directly. Set to "local" if you're not using Theo cloud.
|
|
6
|
+
THEOKIT_API_KEY=local
|
|
7
|
+
|
|
8
|
+
# Provider key — pick ONE. The SDK auto-detects which one is set.
|
|
9
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
10
|
+
# OPENAI_API_KEY=sk-...
|
|
11
|
+
# OPENROUTER_API_KEY=sk-or-...
|
|
12
|
+
|
|
13
|
+
# Override the model (default: anthropic/claude-3-5-sonnet-latest).
|
|
14
|
+
# AGENT_MODEL=openai/gpt-4o-mini
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Retrieval-augmented generation over your own files, scaffolded by `theokit init`.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
mkdir -p .theokit/memory && echo "# Notes" > .theokit/memory/MEMORY.md
|
|
11
|
+
pnpm dev "What is in this knowledge base?"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What this does
|
|
15
|
+
|
|
16
|
+
1. `Memory.openIndex({ cwd })` — SQLite full-text search, no native dependency.
|
|
17
|
+
2. `index.sync()` scans `.theokit/memory/` and reports what it indexed. An empty
|
|
18
|
+
index says so up front, so a thin answer is not mistaken for a weak model.
|
|
19
|
+
3. `Tool.create(…)` exposes the index to the model, which decides when to search.
|
|
20
|
+
4. The agent answers from retrieved snippets and cites them.
|
|
21
|
+
|
|
22
|
+
`sources` belongs to the SEARCH, not to opening the index — it narrows which
|
|
23
|
+
corpora a single query reads.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node 22.12+.
|
|
28
|
+
- One of: Anthropic / OpenAI / OpenRouter API key.
|
|
29
|
+
|
|
30
|
+
## Next steps
|
|
31
|
+
|
|
32
|
+
- Hybrid vector recall: `Memory.openIndex({ cwd, embedding: { provider: "openai" } })`.
|
|
33
|
+
- LanceDB backend: `backend: "lance"` — which then REQUIRES an embedding runtime
|
|
34
|
+
and the `@lancedb/lancedb` peer.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx --env-file=.env src/index.ts",
|
|
8
|
+
"start": "tsx --env-file=.env src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@theokit/sdk": "^{{sdkVersion}}",
|
|
12
|
+
"zod": "^4.0.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"tsx": "^4.19.0",
|
|
16
|
+
"typescript": "^5.8.0"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.12.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{projectName}} — retrieval-augmented generation over your own files.
|
|
3
|
+
*
|
|
4
|
+
* Indexes `.theokit/memory/`, exposes the index to the model as a tool, and
|
|
5
|
+
* lets the model decide when to search. The agent answers from retrieved
|
|
6
|
+
* snippets and cites them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Agent, Memory, type Run, Tool } from "@theokit/sdk";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
const API_KEY = process.env.THEOKIT_API_KEY ?? "local";
|
|
13
|
+
const MODEL = process.env.AGENT_MODEL ?? "anthropic/claude-3-5-sonnet-latest";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Drain one run's assistant stream to stdout and hand back the text.
|
|
17
|
+
*
|
|
18
|
+
* Extracted because the loop is the same in every template and nests three deep inside whatever
|
|
19
|
+
* calls it — reading it once here beats re-reading it inside each caller.
|
|
20
|
+
*/
|
|
21
|
+
async function streamReply(run: Run): Promise<string> {
|
|
22
|
+
let text = "";
|
|
23
|
+
for await (const event of run.stream()) {
|
|
24
|
+
if (event.type !== "assistant") continue;
|
|
25
|
+
for (const part of event.message.content) {
|
|
26
|
+
if (part.type === "text") {
|
|
27
|
+
process.stdout.write(part.text);
|
|
28
|
+
text += part.text;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function main(): Promise<void> {
|
|
36
|
+
// Default backend is SQLite with full-text search and no native dependency.
|
|
37
|
+
// Add `embedding: { provider: "openai" }` for hybrid vector recall, or
|
|
38
|
+
// `backend: "lance"` (which then REQUIRES an embedding runtime).
|
|
39
|
+
const index = await Memory.openIndex({ cwd: process.cwd() });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const synced = await index.sync();
|
|
43
|
+
console.log(
|
|
44
|
+
`Indexed ${synced.filesScanned} file(s) · ${synced.chunksWritten} chunk(s) written · ` +
|
|
45
|
+
`backend ${index.status().backend}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (index.status().chunksIndexed === 0) {
|
|
49
|
+
// Saying this up front is the difference between "the model is bad" and
|
|
50
|
+
// "there was nothing to retrieve".
|
|
51
|
+
console.warn(
|
|
52
|
+
"\nThe index is empty. Put some markdown under .theokit/memory/ and re-run,\n" +
|
|
53
|
+
"otherwise the agent has nothing to cite and will say so.\n",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const searchKnowledge = Tool.create({
|
|
58
|
+
name: "search_knowledge",
|
|
59
|
+
description:
|
|
60
|
+
"Search the local knowledge base and return matching snippets with citations. " +
|
|
61
|
+
"Call this before answering any question about the user's own documents.",
|
|
62
|
+
inputSchema: z.object({
|
|
63
|
+
query: z.string().describe("What to look for, in natural language."),
|
|
64
|
+
maxResults: z.number().int().min(1).max(20).default(5),
|
|
65
|
+
}),
|
|
66
|
+
handler: async ({ query, maxResults }) => {
|
|
67
|
+
// `sources` belongs to the SEARCH, not to opening the index: it narrows
|
|
68
|
+
// which corpora this one query reads.
|
|
69
|
+
const hits = await index.search(query, {
|
|
70
|
+
maxResults,
|
|
71
|
+
sources: ["memory", "wiki"],
|
|
72
|
+
});
|
|
73
|
+
if (hits.length === 0) return "No matching documents.";
|
|
74
|
+
return hits
|
|
75
|
+
.map((h, i) => `[${i + 1}] ${h.citation} (score ${h.score.toFixed(2)})\n${h.snippet}`)
|
|
76
|
+
.join("\n\n");
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const agent = await Agent.create({
|
|
81
|
+
agentId: "rag-agent",
|
|
82
|
+
apiKey: API_KEY,
|
|
83
|
+
model: { id: MODEL },
|
|
84
|
+
systemPrompt:
|
|
85
|
+
"Answer from the local knowledge base. Call search_knowledge first, cite the " +
|
|
86
|
+
"sources it returns by their [n] markers, and say plainly when the base does " +
|
|
87
|
+
"not contain the answer rather than filling the gap from memory.",
|
|
88
|
+
tools: [searchKnowledge],
|
|
89
|
+
local: { cwd: process.cwd() },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const question = process.argv[2] ?? "What is in this knowledge base?";
|
|
94
|
+
console.log(`\nQuestion: ${question}\n`);
|
|
95
|
+
|
|
96
|
+
const run = await agent.send(question);
|
|
97
|
+
await streamReply(run);
|
|
98
|
+
const result = await run.wait();
|
|
99
|
+
process.stdout.write("\n");
|
|
100
|
+
if (result.status === "error") {
|
|
101
|
+
console.error(`\nrun failed: ${result.error?.message ?? "no reason reported"}`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
await agent.dispose();
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
await index.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((cause) => {
|
|
113
|
+
console.error("rag-agent failed:", cause instanceof Error ? cause.message : cause);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
@@ -22,14 +22,14 @@ Then DM your bot in Telegram. Every message gets streamed through the agent.
|
|
|
22
22
|
## What this does
|
|
23
23
|
|
|
24
24
|
- `GatewayRunner` + `TelegramAdapter` handle the platform plumbing.
|
|
25
|
-
- `
|
|
26
|
-
|
|
25
|
+
- `AgentFactory.create` captures the shared config once; `factory.getOrCreate`
|
|
26
|
+
keeps one persistent agent per chat, so memory and state survive across turns.
|
|
27
27
|
- Each inbound message → `agent.send` → reply back via `ctx.reply`.
|
|
28
28
|
|
|
29
29
|
## Customize
|
|
30
30
|
|
|
31
31
|
- Switch model: `AGENT_MODEL=ollama/llama3.2:3b` for local Ollama.
|
|
32
|
-
- Add custom tools:
|
|
33
|
-
`
|
|
32
|
+
- Add custom tools: `Tool.create({ name, description, inputSchema, handler })`
|
|
33
|
+
from `@theokit/sdk`, passed as `AgentFactory.create({ tools: [...] })`.
|
|
34
34
|
- Slash commands: `runner.command("name", handler)` — see
|
|
35
35
|
`@theokit/gateway` docs.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@theokit/sdk": "^{{sdkVersion}}",
|
|
12
|
-
"@theokit/gateway": "^
|
|
13
|
-
"@theokit/gateway-telegram": "^
|
|
12
|
+
"@theokit/gateway": "^0.5.0",
|
|
13
|
+
"@theokit/gateway-telegram": "^0.1.0",
|
|
14
14
|
"grammy": "^1.30.0",
|
|
15
15
|
"zod": "^4.0.0"
|
|
16
16
|
},
|