plasalid 0.2.0
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/LICENSE +213 -0
- package/README.md +176 -0
- package/dist/accounts/taxonomy.d.ts +31 -0
- package/dist/accounts/taxonomy.js +189 -0
- package/dist/ai/agent.d.ts +43 -0
- package/dist/ai/agent.js +155 -0
- package/dist/ai/context.d.ts +4 -0
- package/dist/ai/context.js +33 -0
- package/dist/ai/memory.d.ts +14 -0
- package/dist/ai/memory.js +12 -0
- package/dist/ai/provider.d.ts +67 -0
- package/dist/ai/provider.js +5 -0
- package/dist/ai/providers/anthropic.d.ts +5 -0
- package/dist/ai/providers/anthropic.js +49 -0
- package/dist/ai/providers/index.d.ts +2 -0
- package/dist/ai/providers/index.js +12 -0
- package/dist/ai/providers/openai-compat.d.ts +5 -0
- package/dist/ai/providers/openai-compat.js +147 -0
- package/dist/ai/providers/openai.d.ts +5 -0
- package/dist/ai/providers/openai.js +147 -0
- package/dist/ai/redactor.d.ts +2 -0
- package/dist/ai/redactor.js +91 -0
- package/dist/ai/sanitize.d.ts +14 -0
- package/dist/ai/sanitize.js +25 -0
- package/dist/ai/system-prompt.d.ts +13 -0
- package/dist/ai/system-prompt.js +174 -0
- package/dist/ai/thai-taxonomy-hint.d.ts +8 -0
- package/dist/ai/thai-taxonomy-hint.js +22 -0
- package/dist/ai/thinking-phrases.d.ts +7 -0
- package/dist/ai/thinking-phrases.js +15 -0
- package/dist/ai/thinking.d.ts +7 -0
- package/dist/ai/thinking.js +15 -0
- package/dist/ai/tools/common.d.ts +2 -0
- package/dist/ai/tools/common.js +83 -0
- package/dist/ai/tools/index.d.ts +8 -0
- package/dist/ai/tools/index.js +34 -0
- package/dist/ai/tools/ingest.d.ts +2 -0
- package/dist/ai/tools/ingest.js +202 -0
- package/dist/ai/tools/read.d.ts +2 -0
- package/dist/ai/tools/read.js +123 -0
- package/dist/ai/tools/reconcile.d.ts +2 -0
- package/dist/ai/tools/reconcile.js +227 -0
- package/dist/ai/tools/scan.d.ts +2 -0
- package/dist/ai/tools/scan.js +24 -0
- package/dist/ai/tools/types.d.ts +26 -0
- package/dist/ai/tools/types.js +1 -0
- package/dist/ai/tools.d.ts +18 -0
- package/dist/ai/tools.js +402 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +28 -0
- package/dist/cli/commands/accounts.d.ts +1 -0
- package/dist/cli/commands/accounts.js +86 -0
- package/dist/cli/commands/data.d.ts +1 -0
- package/dist/cli/commands/data.js +28 -0
- package/dist/cli/commands/reconcile.d.ts +2 -0
- package/dist/cli/commands/reconcile.js +15 -0
- package/dist/cli/commands/revert.d.ts +1 -0
- package/dist/cli/commands/revert.js +68 -0
- package/dist/cli/commands/scan.d.ts +4 -0
- package/dist/cli/commands/scan.js +45 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +22 -0
- package/dist/cli/commands/transactions.d.ts +8 -0
- package/dist/cli/commands/transactions.js +92 -0
- package/dist/cli/commands/undo.d.ts +1 -0
- package/dist/cli/commands/undo.js +38 -0
- package/dist/cli/commands.d.ts +14 -0
- package/dist/cli/commands.js +196 -0
- package/dist/cli/format.d.ts +8 -0
- package/dist/cli/format.js +109 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +126 -0
- package/dist/cli/ink/ChatApp.d.ts +8 -0
- package/dist/cli/ink/ChatApp.js +94 -0
- package/dist/cli/ink/PromptFrame.d.ts +10 -0
- package/dist/cli/ink/PromptFrame.js +11 -0
- package/dist/cli/ink/TextInput.d.ts +13 -0
- package/dist/cli/ink/TextInput.js +24 -0
- package/dist/cli/ink/hooks/useAgent.d.ts +27 -0
- package/dist/cli/ink/hooks/useAgent.js +65 -0
- package/dist/cli/ink/hooks/useCtrlCExit.d.ts +16 -0
- package/dist/cli/ink/hooks/useCtrlCExit.js +43 -0
- package/dist/cli/ink/hooks/useFooterText.d.ts +2 -0
- package/dist/cli/ink/hooks/useFooterText.js +43 -0
- package/dist/cli/ink/hooks/useTextInput.d.ts +32 -0
- package/dist/cli/ink/hooks/useTextInput.js +356 -0
- package/dist/cli/ink/messages/AssistantMessage.d.ts +3 -0
- package/dist/cli/ink/messages/AssistantMessage.js +6 -0
- package/dist/cli/ink/messages/ErrorMessage.d.ts +4 -0
- package/dist/cli/ink/messages/ErrorMessage.js +6 -0
- package/dist/cli/ink/messages/InterruptedMessage.d.ts +1 -0
- package/dist/cli/ink/messages/InterruptedMessage.js +6 -0
- package/dist/cli/ink/messages/ThinkingLine.d.ts +12 -0
- package/dist/cli/ink/messages/ThinkingLine.js +23 -0
- package/dist/cli/ink/messages/UserMessage.d.ts +4 -0
- package/dist/cli/ink/messages/UserMessage.js +15 -0
- package/dist/cli/ink/mount.d.ts +6 -0
- package/dist/cli/ink/mount.js +12 -0
- package/dist/cli/logo.d.ts +1 -0
- package/dist/cli/logo.js +20 -0
- package/dist/cli/setup.d.ts +2 -0
- package/dist/cli/setup.js +210 -0
- package/dist/cli/ux.d.ts +38 -0
- package/dist/cli/ux.js +104 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +66 -0
- package/dist/currency.d.ts +6 -0
- package/dist/currency.js +19 -0
- package/dist/db/connection.d.ts +5 -0
- package/dist/db/connection.js +45 -0
- package/dist/db/encryption.d.ts +11 -0
- package/dist/db/encryption.js +45 -0
- package/dist/db/helpers.d.ts +16 -0
- package/dist/db/helpers.js +45 -0
- package/dist/db/queries/account_balance.d.ts +61 -0
- package/dist/db/queries/account_balance.js +146 -0
- package/dist/db/queries/journal.d.ts +95 -0
- package/dist/db/queries/journal.js +204 -0
- package/dist/db/queries/search.d.ts +7 -0
- package/dist/db/queries/search.js +19 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +95 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parser/pdf.d.ts +14 -0
- package/dist/parser/pdf.js +40 -0
- package/dist/parser/pipeline.d.ts +44 -0
- package/dist/parser/pipeline.js +160 -0
- package/dist/parser/prompts.d.ts +8 -0
- package/dist/parser/prompts.js +20 -0
- package/dist/parser/walker.d.ts +8 -0
- package/dist/parser/walker.js +42 -0
- package/dist/reconciler/pipeline.d.ts +17 -0
- package/dist/reconciler/pipeline.js +45 -0
- package/dist/reconciler/prompts.d.ts +12 -0
- package/dist/reconciler/prompts.js +22 -0
- package/dist/scanner/password-store.d.ts +34 -0
- package/dist/scanner/password-store.js +83 -0
- package/dist/scanner/pdf-unlock.d.ts +17 -0
- package/dist/scanner/pdf-unlock.js +48 -0
- package/dist/scanner/pdf.d.ts +17 -0
- package/dist/scanner/pdf.js +36 -0
- package/dist/scanner/pipeline.d.ts +32 -0
- package/dist/scanner/pipeline.js +137 -0
- package/dist/scanner/prompts.d.ts +8 -0
- package/dist/scanner/prompts.js +20 -0
- package/dist/scanner/state-machine.d.ts +60 -0
- package/dist/scanner/state-machine.js +64 -0
- package/dist/scanner/unlock.d.ts +24 -0
- package/dist/scanner/unlock.js +122 -0
- package/dist/scanner/walker.d.ts +8 -0
- package/dist/scanner/walker.js +42 -0
- package/package.json +65 -0
package/dist/ai/agent.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { buildChatSystemPrompt, buildScanSystemPrompt, buildReconcileSystemPrompt, } from "./system-prompt.js";
|
|
3
|
+
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
4
|
+
import { getConversationHistory, saveMessage } from "./memory.js";
|
|
5
|
+
import { redact, unredact } from "./redactor.js";
|
|
6
|
+
import { createProvider } from "./providers/index.js";
|
|
7
|
+
const provider = createProvider();
|
|
8
|
+
const MAX_TOOL_STEPS = 20;
|
|
9
|
+
export class AbortedError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("aborted");
|
|
12
|
+
this.name = "AbortedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function runAgent({ db, systemPrompt, tools, initialMessages, agentCtx, onProgress, signal, maxToolSteps, }) {
|
|
16
|
+
const messages = [...initialMessages];
|
|
17
|
+
const useThinking = config.thinkingBudget > 0 && provider.supportsThinking;
|
|
18
|
+
const throwIfAborted = () => {
|
|
19
|
+
if (signal?.aborted)
|
|
20
|
+
throw new AbortedError();
|
|
21
|
+
};
|
|
22
|
+
const stepLimit = maxToolSteps ?? MAX_TOOL_STEPS;
|
|
23
|
+
throwIfAborted();
|
|
24
|
+
let response = await provider.sendMessage({
|
|
25
|
+
model: config.model,
|
|
26
|
+
maxTokens: useThinking ? 16000 : 4096,
|
|
27
|
+
system: systemPrompt,
|
|
28
|
+
tools,
|
|
29
|
+
messages,
|
|
30
|
+
thinking: useThinking ? { type: "enabled", budget_tokens: config.thinkingBudget } : undefined,
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
let toolCount = 0;
|
|
35
|
+
while (response.stopReason === "tool_use" && toolCount < stepLimit) {
|
|
36
|
+
throwIfAborted();
|
|
37
|
+
messages.push({ role: "assistant", content: response.content });
|
|
38
|
+
const toolResults = [];
|
|
39
|
+
for (const block of response.content) {
|
|
40
|
+
if (block.type === "tool_use") {
|
|
41
|
+
toolCount++;
|
|
42
|
+
onProgress?.({ phase: "tool", toolName: block.name, toolCount, elapsedMs: Date.now() - startTime });
|
|
43
|
+
const result = await executeTool(db, block.name, block.input, agentCtx);
|
|
44
|
+
toolResults.push({
|
|
45
|
+
type: "tool_result",
|
|
46
|
+
tool_use_id: block.id,
|
|
47
|
+
content: redact(result),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
messages.push({ role: "user", content: toolResults });
|
|
52
|
+
onProgress?.({ phase: "responding", toolCount, elapsedMs: Date.now() - startTime });
|
|
53
|
+
throwIfAborted();
|
|
54
|
+
response = await provider.sendMessage({
|
|
55
|
+
model: config.model,
|
|
56
|
+
maxTokens: useThinking ? 16000 : 4096,
|
|
57
|
+
system: systemPrompt,
|
|
58
|
+
tools,
|
|
59
|
+
messages,
|
|
60
|
+
thinking: useThinking ? { type: "enabled", budget_tokens: config.thinkingBudget } : undefined,
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const textBlocks = response.content.filter((b) => b.type === "text");
|
|
65
|
+
const text = unredact(textBlocks.map(b => b.text).join("\n"));
|
|
66
|
+
return { text, messages };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Conversational chat used by the Ink TUI. Reuses conversation_history for context
|
|
70
|
+
* continuity, redacts PII on the way out, restores it on the way in for display.
|
|
71
|
+
*/
|
|
72
|
+
export async function handleChatMessage(db, userMessage, onProgress, signal) {
|
|
73
|
+
saveMessage(db, "user", userMessage);
|
|
74
|
+
const rawHistory = getConversationHistory(db, 30);
|
|
75
|
+
const MAX_HISTORY_CHARS = 24_000;
|
|
76
|
+
let historyChars = 0;
|
|
77
|
+
const history = [];
|
|
78
|
+
for (let i = rawHistory.length - 1; i >= 0; i--) {
|
|
79
|
+
historyChars += rawHistory[i].content.length;
|
|
80
|
+
if (historyChars > MAX_HISTORY_CHARS)
|
|
81
|
+
break;
|
|
82
|
+
history.unshift(rawHistory[i]);
|
|
83
|
+
}
|
|
84
|
+
const systemPrompt = redact(buildChatSystemPrompt(db));
|
|
85
|
+
const messages = history.map(h => ({
|
|
86
|
+
role: h.role,
|
|
87
|
+
content: redact(h.content),
|
|
88
|
+
}));
|
|
89
|
+
if (messages.length === 0 || messages[messages.length - 1].content !== redact(userMessage)) {
|
|
90
|
+
messages.push({ role: "user", content: redact(userMessage) });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const { text } = await runAgent({
|
|
94
|
+
db,
|
|
95
|
+
systemPrompt,
|
|
96
|
+
tools: getToolDefinitions("chat"),
|
|
97
|
+
initialMessages: messages,
|
|
98
|
+
onProgress,
|
|
99
|
+
signal,
|
|
100
|
+
});
|
|
101
|
+
saveMessage(db, "assistant", text);
|
|
102
|
+
return text || "I couldn't formulate a response. Could you rephrase?";
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof AbortedError || error?.name === "AbortError" || signal?.aborted) {
|
|
106
|
+
throw new AbortedError();
|
|
107
|
+
}
|
|
108
|
+
if (error.status === 401 || error.status === 403) {
|
|
109
|
+
return "API key was rejected. Run `plasalid setup` to reconfigure your credentials.";
|
|
110
|
+
}
|
|
111
|
+
if (error.status === 429) {
|
|
112
|
+
return "Rate limited. Wait a moment and try again.";
|
|
113
|
+
}
|
|
114
|
+
const safeMessage = error.status ? `API error (${error.status}): ${error.message || ""}` : error.message || "internal error";
|
|
115
|
+
console.error("AI error:", safeMessage);
|
|
116
|
+
return "Sorry, I had trouble processing that. Could you try again?";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Scan-time agent loop. Caller supplies the initial user message (which carries
|
|
121
|
+
* the PDF as a content block) and a AgentExecutionContext that scopes the file
|
|
122
|
+
* id, scanner version, and interactivity for ask_user.
|
|
123
|
+
*/
|
|
124
|
+
export async function runScanAgent(opts) {
|
|
125
|
+
const systemPrompt = redact(buildScanSystemPrompt(opts.db, opts.prompt));
|
|
126
|
+
const { text } = await runAgent({
|
|
127
|
+
db: opts.db,
|
|
128
|
+
systemPrompt,
|
|
129
|
+
tools: getToolDefinitions("scan"),
|
|
130
|
+
initialMessages: opts.initialMessages,
|
|
131
|
+
agentCtx: opts.agentCtx,
|
|
132
|
+
onProgress: opts.onProgress,
|
|
133
|
+
signal: opts.signal,
|
|
134
|
+
maxToolSteps: 40,
|
|
135
|
+
});
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reconcile-time agent loop. Walks the existing journal with the reconcile
|
|
140
|
+
* tool profile (read tools + write/merge/delete primitives).
|
|
141
|
+
*/
|
|
142
|
+
export async function runReconcileAgent(opts) {
|
|
143
|
+
const systemPrompt = redact(buildReconcileSystemPrompt(opts.db, opts.prompt));
|
|
144
|
+
const { text } = await runAgent({
|
|
145
|
+
db: opts.db,
|
|
146
|
+
systemPrompt,
|
|
147
|
+
tools: getToolDefinitions("reconcile"),
|
|
148
|
+
initialMessages: opts.initialMessages,
|
|
149
|
+
agentCtx: opts.agentCtx,
|
|
150
|
+
onProgress: opts.onProgress,
|
|
151
|
+
signal: opts.signal,
|
|
152
|
+
maxToolSteps: 60,
|
|
153
|
+
});
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
2
|
+
import { dirname, resolve } from "path";
|
|
3
|
+
import { getPlasalidDir } from "../config.js";
|
|
4
|
+
export function getContextPath() {
|
|
5
|
+
return resolve(getPlasalidDir(), "context.md");
|
|
6
|
+
}
|
|
7
|
+
export function readContext() {
|
|
8
|
+
const p = getContextPath();
|
|
9
|
+
if (!existsSync(p))
|
|
10
|
+
return "";
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(p, "utf-8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function writeContext(content) {
|
|
19
|
+
const p = getContextPath();
|
|
20
|
+
const dir = dirname(p);
|
|
21
|
+
if (!existsSync(dir))
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
writeFileSync(p, content, { encoding: "utf-8", mode: 0o600 });
|
|
24
|
+
try {
|
|
25
|
+
chmodSync(p, 0o600);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
export function createContextTemplate(userName) {
|
|
30
|
+
if (existsSync(getContextPath()))
|
|
31
|
+
return;
|
|
32
|
+
writeContext(`# Plasalid context for ${userName}\n\n## Family\n- ${userName}\n\n## Income\n- (Optional: add your primary income source so Plasalid can mark it as PII when sending data to the model.)\n\n## Notes\n- (Free-form notes about your accounts, bank preferences, or anything Plasalid should keep in mind when scanning.)\n`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
export declare function getConversationHistory(db: Database.Database, limit?: number): {
|
|
3
|
+
role: string;
|
|
4
|
+
content: string;
|
|
5
|
+
created_at: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function saveMessage(db: Database.Database, role: "user" | "assistant", content: string): void;
|
|
8
|
+
export declare function getMemories(db: Database.Database): {
|
|
9
|
+
id: number;
|
|
10
|
+
content: string;
|
|
11
|
+
category: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function saveMemory(db: Database.Database, content: string, category?: string): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getConversationHistory(db, limit = 20) {
|
|
2
|
+
return db.prepare(`SELECT role, content, created_at FROM conversation_history ORDER BY id DESC LIMIT ?`).all(limit).reverse();
|
|
3
|
+
}
|
|
4
|
+
export function saveMessage(db, role, content) {
|
|
5
|
+
db.prepare(`INSERT INTO conversation_history (role, content) VALUES (?, ?)`).run(role, content);
|
|
6
|
+
}
|
|
7
|
+
export function getMemories(db) {
|
|
8
|
+
return db.prepare(`SELECT id, content, category, created_at FROM memories ORDER BY created_at DESC`).all();
|
|
9
|
+
}
|
|
10
|
+
export function saveMemory(db, content, category = "general") {
|
|
11
|
+
db.prepare(`INSERT INTO memories (content, category) VALUES (?, ?)`).run(content, category);
|
|
12
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized types for provider abstraction.
|
|
3
|
+
* Mirrors Anthropic's content-block shape but decoupled from the SDK types.
|
|
4
|
+
*/
|
|
5
|
+
export interface TextBlock {
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolUseBlock {
|
|
10
|
+
type: "tool_use";
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
input: any;
|
|
14
|
+
}
|
|
15
|
+
export interface DocumentBlock {
|
|
16
|
+
type: "document";
|
|
17
|
+
source: {
|
|
18
|
+
type: "base64";
|
|
19
|
+
media_type: string;
|
|
20
|
+
data: string;
|
|
21
|
+
};
|
|
22
|
+
title?: string;
|
|
23
|
+
}
|
|
24
|
+
export type NormalizedContentBlock = TextBlock | ToolUseBlock | DocumentBlock;
|
|
25
|
+
export interface NormalizedResponse {
|
|
26
|
+
content: NormalizedContentBlock[];
|
|
27
|
+
stopReason: string;
|
|
28
|
+
usage?: {
|
|
29
|
+
input_tokens: number;
|
|
30
|
+
output_tokens: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface NormalizedMessage {
|
|
34
|
+
role: "user" | "assistant";
|
|
35
|
+
content: string | NormalizedContentBlock[] | NormalizedToolResult[];
|
|
36
|
+
}
|
|
37
|
+
export interface NormalizedToolResult {
|
|
38
|
+
type: "tool_result";
|
|
39
|
+
tool_use_id: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ToolDefinition {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
input_schema: {
|
|
46
|
+
type: "object";
|
|
47
|
+
properties: Record<string, any>;
|
|
48
|
+
required: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface SendMessageParams {
|
|
52
|
+
model: string;
|
|
53
|
+
system: string;
|
|
54
|
+
messages: NormalizedMessage[];
|
|
55
|
+
tools: ToolDefinition[];
|
|
56
|
+
maxTokens: number;
|
|
57
|
+
thinking?: {
|
|
58
|
+
type: "enabled";
|
|
59
|
+
budget_tokens: number;
|
|
60
|
+
};
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
export interface Provider {
|
|
64
|
+
name: string;
|
|
65
|
+
supportsThinking: boolean;
|
|
66
|
+
sendMessage(params: SendMessageParams): Promise<NormalizedResponse>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
export function createAnthropicProvider(opts) {
|
|
3
|
+
const client = new Anthropic(opts.baseURL
|
|
4
|
+
? { apiKey: opts.apiKey, baseURL: opts.baseURL }
|
|
5
|
+
: { apiKey: opts.apiKey });
|
|
6
|
+
return {
|
|
7
|
+
name: "anthropic",
|
|
8
|
+
supportsThinking: true,
|
|
9
|
+
async sendMessage(params) {
|
|
10
|
+
const apiParams = {
|
|
11
|
+
model: params.model,
|
|
12
|
+
max_tokens: params.maxTokens,
|
|
13
|
+
system: params.system,
|
|
14
|
+
tools: params.tools,
|
|
15
|
+
messages: params.messages,
|
|
16
|
+
};
|
|
17
|
+
if (params.thinking) {
|
|
18
|
+
apiParams.thinking = params.thinking;
|
|
19
|
+
}
|
|
20
|
+
const response = await client.messages.create(apiParams, {
|
|
21
|
+
signal: params.signal,
|
|
22
|
+
});
|
|
23
|
+
// Filter thinking blocks and normalize content
|
|
24
|
+
const content = [];
|
|
25
|
+
for (const block of response.content) {
|
|
26
|
+
if (block.type === "thinking")
|
|
27
|
+
continue;
|
|
28
|
+
if (block.type === "text") {
|
|
29
|
+
content.push({ type: "text", text: block.text });
|
|
30
|
+
}
|
|
31
|
+
else if (block.type === "tool_use") {
|
|
32
|
+
content.push({
|
|
33
|
+
type: "tool_use",
|
|
34
|
+
id: block.id,
|
|
35
|
+
name: block.name,
|
|
36
|
+
input: block.input,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content,
|
|
42
|
+
stopReason: response.stop_reason || "end_turn",
|
|
43
|
+
usage: response.usage
|
|
44
|
+
? { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens }
|
|
45
|
+
: undefined,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { config } from "../../config.js";
|
|
2
|
+
import { createAnthropicProvider } from "./anthropic.js";
|
|
3
|
+
import { createOpenAICompatibleProvider } from "./openai.js";
|
|
4
|
+
export function createProvider() {
|
|
5
|
+
if (config.providerType === "openai-compatible") {
|
|
6
|
+
return createOpenAICompatibleProvider({
|
|
7
|
+
apiKey: config.openaiCompatibleKey || "openai-compatible",
|
|
8
|
+
baseURL: config.openaiCompatibleBaseURL,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return createAnthropicProvider({ apiKey: config.anthropicKey });
|
|
12
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
export function createOpenAICompatibleProvider(opts) {
|
|
3
|
+
const client = new OpenAI({
|
|
4
|
+
apiKey: opts.apiKey,
|
|
5
|
+
baseURL: opts.baseURL,
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
name: "openai-compatible",
|
|
9
|
+
supportsThinking: false,
|
|
10
|
+
async sendMessage(params) {
|
|
11
|
+
const messages = convertMessages(params.system, params.messages);
|
|
12
|
+
const tools = convertTools(params.tools);
|
|
13
|
+
// Try max_tokens first (broadest compat: Ollama, vLLM, older OpenAI models),
|
|
14
|
+
// fall back to max_completion_tokens if rejected (newer OpenAI models require it)
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await client.chat.completions.create({
|
|
18
|
+
model: params.model,
|
|
19
|
+
max_tokens: params.maxTokens,
|
|
20
|
+
messages,
|
|
21
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
22
|
+
}, { signal: params.signal });
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
if (e.status === 400 && e.message?.includes("max_tokens")) {
|
|
26
|
+
response = await client.chat.completions.create({
|
|
27
|
+
model: params.model,
|
|
28
|
+
max_completion_tokens: params.maxTokens,
|
|
29
|
+
messages,
|
|
30
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
31
|
+
}, { signal: params.signal });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const choice = response.choices[0];
|
|
38
|
+
if (!choice) {
|
|
39
|
+
return { content: [], stopReason: "end_turn" };
|
|
40
|
+
}
|
|
41
|
+
const content = [];
|
|
42
|
+
if (choice.message.content) {
|
|
43
|
+
content.push({ type: "text", text: choice.message.content });
|
|
44
|
+
}
|
|
45
|
+
if (choice.message.tool_calls) {
|
|
46
|
+
for (const tc of choice.message.tool_calls) {
|
|
47
|
+
if (tc.type !== "function")
|
|
48
|
+
continue;
|
|
49
|
+
content.push({
|
|
50
|
+
type: "tool_use",
|
|
51
|
+
id: tc.id,
|
|
52
|
+
name: tc.function.name,
|
|
53
|
+
input: parseArguments(tc.function.arguments),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
stopReason: hasToolCalls ? "tool_use" : "end_turn",
|
|
61
|
+
usage: response.usage
|
|
62
|
+
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
63
|
+
: undefined,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function convertMessages(system, messages) {
|
|
69
|
+
const result = [
|
|
70
|
+
{ role: "system", content: system },
|
|
71
|
+
];
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
if (msg.role === "user") {
|
|
74
|
+
if (Array.isArray(msg.content) &&
|
|
75
|
+
msg.content.length > 0 &&
|
|
76
|
+
msg.content[0].type === "tool_result") {
|
|
77
|
+
const toolResults = msg.content;
|
|
78
|
+
for (const tr of toolResults) {
|
|
79
|
+
result.push({
|
|
80
|
+
role: "tool",
|
|
81
|
+
tool_call_id: tr.tool_use_id,
|
|
82
|
+
content: tr.content,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (Array.isArray(msg.content)) {
|
|
87
|
+
// Strip document blocks (OpenAI-compat doesn't accept them); keep text.
|
|
88
|
+
const text = msg.content
|
|
89
|
+
.filter((b) => b.type === "text")
|
|
90
|
+
.map((b) => b.text)
|
|
91
|
+
.join("\n");
|
|
92
|
+
result.push({ role: "user", content: text });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result.push({ role: "user", content: msg.content });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (Array.isArray(msg.content)) {
|
|
100
|
+
const blocks = msg.content;
|
|
101
|
+
const textParts = blocks
|
|
102
|
+
.filter((b) => b.type === "text")
|
|
103
|
+
.map((b) => b.text)
|
|
104
|
+
.join("\n");
|
|
105
|
+
const toolCalls = blocks
|
|
106
|
+
.filter((b) => b.type === "tool_use")
|
|
107
|
+
.map((b) => {
|
|
108
|
+
const tu = b;
|
|
109
|
+
return {
|
|
110
|
+
id: tu.id,
|
|
111
|
+
type: "function",
|
|
112
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
result.push({
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: textParts || null,
|
|
118
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
result.push({ role: "assistant", content: msg.content });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function convertTools(tools) {
|
|
129
|
+
return tools.map((t) => ({
|
|
130
|
+
type: "function",
|
|
131
|
+
function: {
|
|
132
|
+
name: t.name,
|
|
133
|
+
description: t.description,
|
|
134
|
+
parameters: t.input_schema,
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function parseArguments(args) {
|
|
139
|
+
if (typeof args !== "string")
|
|
140
|
+
return args;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(args);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
export function createOpenAICompatibleProvider(opts) {
|
|
3
|
+
const client = new OpenAI({
|
|
4
|
+
apiKey: opts.apiKey,
|
|
5
|
+
baseURL: opts.baseURL,
|
|
6
|
+
});
|
|
7
|
+
return {
|
|
8
|
+
name: "openai-compatible",
|
|
9
|
+
supportsThinking: false,
|
|
10
|
+
async sendMessage(params) {
|
|
11
|
+
const messages = convertMessages(params.system, params.messages);
|
|
12
|
+
const tools = convertTools(params.tools);
|
|
13
|
+
// Try max_tokens first (broadest compat: Ollama, vLLM, older OpenAI models),
|
|
14
|
+
// fall back to max_completion_tokens if rejected (newer OpenAI models require it)
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await client.chat.completions.create({
|
|
18
|
+
model: params.model,
|
|
19
|
+
max_tokens: params.maxTokens,
|
|
20
|
+
messages,
|
|
21
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
22
|
+
}, { signal: params.signal });
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
if (e.status === 400 && e.message?.includes("max_tokens")) {
|
|
26
|
+
response = await client.chat.completions.create({
|
|
27
|
+
model: params.model,
|
|
28
|
+
max_completion_tokens: params.maxTokens,
|
|
29
|
+
messages,
|
|
30
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
31
|
+
}, { signal: params.signal });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const choice = response.choices[0];
|
|
38
|
+
if (!choice) {
|
|
39
|
+
return { content: [], stopReason: "end_turn" };
|
|
40
|
+
}
|
|
41
|
+
const content = [];
|
|
42
|
+
if (choice.message.content) {
|
|
43
|
+
content.push({ type: "text", text: choice.message.content });
|
|
44
|
+
}
|
|
45
|
+
if (choice.message.tool_calls) {
|
|
46
|
+
for (const tc of choice.message.tool_calls) {
|
|
47
|
+
if (tc.type !== "function")
|
|
48
|
+
continue;
|
|
49
|
+
content.push({
|
|
50
|
+
type: "tool_use",
|
|
51
|
+
id: tc.id,
|
|
52
|
+
name: tc.function.name,
|
|
53
|
+
input: parseArguments(tc.function.arguments),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const hasToolCalls = content.some((b) => b.type === "tool_use");
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
stopReason: hasToolCalls ? "tool_use" : "end_turn",
|
|
61
|
+
usage: response.usage
|
|
62
|
+
? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens }
|
|
63
|
+
: undefined,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function convertMessages(system, messages) {
|
|
69
|
+
const result = [
|
|
70
|
+
{ role: "system", content: system },
|
|
71
|
+
];
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
if (msg.role === "user") {
|
|
74
|
+
if (Array.isArray(msg.content) &&
|
|
75
|
+
msg.content.length > 0 &&
|
|
76
|
+
msg.content[0].type === "tool_result") {
|
|
77
|
+
const toolResults = msg.content;
|
|
78
|
+
for (const tr of toolResults) {
|
|
79
|
+
result.push({
|
|
80
|
+
role: "tool",
|
|
81
|
+
tool_call_id: tr.tool_use_id,
|
|
82
|
+
content: tr.content,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (Array.isArray(msg.content)) {
|
|
87
|
+
// Strip document blocks (OpenAI-compat doesn't accept them); keep text.
|
|
88
|
+
const text = msg.content
|
|
89
|
+
.filter((b) => b.type === "text")
|
|
90
|
+
.map((b) => b.text)
|
|
91
|
+
.join("\n");
|
|
92
|
+
result.push({ role: "user", content: text });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result.push({ role: "user", content: msg.content });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (Array.isArray(msg.content)) {
|
|
100
|
+
const blocks = msg.content;
|
|
101
|
+
const textParts = blocks
|
|
102
|
+
.filter((b) => b.type === "text")
|
|
103
|
+
.map((b) => b.text)
|
|
104
|
+
.join("\n");
|
|
105
|
+
const toolCalls = blocks
|
|
106
|
+
.filter((b) => b.type === "tool_use")
|
|
107
|
+
.map((b) => {
|
|
108
|
+
const tu = b;
|
|
109
|
+
return {
|
|
110
|
+
id: tu.id,
|
|
111
|
+
type: "function",
|
|
112
|
+
function: { name: tu.name, arguments: JSON.stringify(tu.input) },
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
result.push({
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: textParts || null,
|
|
118
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
result.push({ role: "assistant", content: msg.content });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function convertTools(tools) {
|
|
129
|
+
return tools.map((t) => ({
|
|
130
|
+
type: "function",
|
|
131
|
+
function: {
|
|
132
|
+
name: t.name,
|
|
133
|
+
description: t.description,
|
|
134
|
+
parameters: t.input_schema,
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function parseArguments(args) {
|
|
139
|
+
if (typeof args !== "string")
|
|
140
|
+
return args;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(args);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|