@devang0907/agent-dev 0.1.2 → 0.1.4
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 +12 -2
- package/dist/agent/loop.d.ts +7 -0
- package/dist/agent/loop.js +146 -30
- package/dist/agent/platform.d.ts +10 -0
- package/dist/agent/platform.js +74 -0
- package/dist/agent/session.d.ts +8 -1
- package/dist/agent/session.js +19 -0
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/read.js +15 -17
- package/dist/agent/tools/search.d.ts +5 -0
- package/dist/agent/tools/search.js +188 -0
- package/dist/agent/tools/shell.d.ts +1 -0
- package/dist/agent/tools/shell.js +63 -0
- package/dist/modes/print-mode.js +15 -0
- package/dist/providers/groq.js +7 -81
- package/dist/providers/openai-compat.d.ts +10 -0
- package/dist/providers/openai-compat.js +212 -0
- package/dist/providers/openrouter-free.js +7 -81
- package/dist/ui/ApiKeyPrompt.js +2 -3
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +115 -36
- package/dist/ui/ChatView.d.ts +2 -1
- package/dist/ui/ChatView.js +14 -4
- package/dist/ui/CommandApprovalPrompt.d.ts +11 -0
- package/dist/ui/CommandApprovalPrompt.js +15 -0
- package/dist/ui/Editor.d.ts +2 -1
- package/dist/ui/Editor.js +7 -3
- package/dist/ui/ModelSelector.js +1 -1
- package/dist/ui/SettingsView.d.ts +3 -1
- package/dist/ui/SettingsView.js +15 -9
- package/dist/ui/layout.d.ts +6 -0
- package/dist/ui/layout.js +22 -0
- package/dist/ui/scroll.d.ts +3 -0
- package/dist/ui/scroll.js +4 -0
- package/dist/ui/theme.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export const webSearchTool = {
|
|
2
|
+
name: "web_search",
|
|
3
|
+
description: "Search the internet for current information, documentation, or news",
|
|
4
|
+
parameters: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
query: { type: "string", description: "Search query" },
|
|
8
|
+
},
|
|
9
|
+
required: ["query"],
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
14
|
+
function stripHtml(text) {
|
|
15
|
+
return decodeHtmlEntities(text.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
|
|
16
|
+
}
|
|
17
|
+
function decodeHtmlEntities(text) {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, '"')
|
|
23
|
+
.replace(/'/g, "'")
|
|
24
|
+
.replace(/'/g, "'")
|
|
25
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
26
|
+
}
|
|
27
|
+
function extractAttr(tag, attr) {
|
|
28
|
+
const re = new RegExp(`${attr}=['"]([^'"]*)['"]`, "i");
|
|
29
|
+
return tag.match(re)?.[1] ?? null;
|
|
30
|
+
}
|
|
31
|
+
function decodeDdgUrl(href) {
|
|
32
|
+
try {
|
|
33
|
+
const match = href.match(/uddg=([^&]+)/);
|
|
34
|
+
if (match)
|
|
35
|
+
return decodeURIComponent(match[1]);
|
|
36
|
+
if (href.startsWith("//"))
|
|
37
|
+
return `https:${href}`;
|
|
38
|
+
return href;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return href;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseTagsByClass(html, className) {
|
|
45
|
+
const re = new RegExp(`<a\\b[^>]*class=['"]${className}['"][^>]*>[\\s\\S]*?<\\/a>`, "gi");
|
|
46
|
+
return html.match(re) ?? [];
|
|
47
|
+
}
|
|
48
|
+
function parseDdgLiteHtml(html) {
|
|
49
|
+
const linkTags = parseTagsByClass(html, "result-link");
|
|
50
|
+
const snippetRe = /<td[^>]*class=['"]result-snippet['"][^>]*>([\s\S]*?)<\/td>/gi;
|
|
51
|
+
const snippets = [];
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = snippetRe.exec(html)) !== null) {
|
|
54
|
+
snippets.push(stripHtml(match[1]));
|
|
55
|
+
}
|
|
56
|
+
const results = [];
|
|
57
|
+
for (let i = 0; i < linkTags.length && results.length < 8; i++) {
|
|
58
|
+
const tag = linkTags[i];
|
|
59
|
+
const href = extractAttr(tag, "href");
|
|
60
|
+
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
61
|
+
if (href && title) {
|
|
62
|
+
results.push({
|
|
63
|
+
title,
|
|
64
|
+
url: decodeDdgUrl(href),
|
|
65
|
+
snippet: snippets[i] ?? "",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
function parseDdgHtmlResults(html) {
|
|
72
|
+
const linkTags = parseTagsByClass(html, "result__a");
|
|
73
|
+
const snippetTags = parseTagsByClass(html, "result__snippet");
|
|
74
|
+
const results = [];
|
|
75
|
+
for (let i = 0; i < linkTags.length && results.length < 8; i++) {
|
|
76
|
+
const tag = linkTags[i];
|
|
77
|
+
const href = extractAttr(tag, "href");
|
|
78
|
+
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
79
|
+
const snippetTag = snippetTags[i];
|
|
80
|
+
const snippet = snippetTag
|
|
81
|
+
? stripHtml(snippetTag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""))
|
|
82
|
+
: "";
|
|
83
|
+
if (href && title) {
|
|
84
|
+
results.push({ title, url: decodeDdgUrl(href), snippet });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
async function fetchDdgLiteResults(query) {
|
|
90
|
+
const res = await fetch("https://lite.duckduckgo.com/lite/", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"User-Agent": USER_AGENT,
|
|
94
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
95
|
+
},
|
|
96
|
+
body: new URLSearchParams({ q: query }),
|
|
97
|
+
signal: AbortSignal.timeout(15000),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`DuckDuckGo lite failed (${res.status})`);
|
|
101
|
+
return parseDdgLiteHtml(await res.text());
|
|
102
|
+
}
|
|
103
|
+
async function fetchDdgHtmlResults(query) {
|
|
104
|
+
const res = await fetch("https://html.duckduckgo.com/html/", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": USER_AGENT,
|
|
108
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
109
|
+
},
|
|
110
|
+
body: new URLSearchParams({ q: query, b: "", kl: "" }),
|
|
111
|
+
signal: AbortSignal.timeout(15000),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
throw new Error(`DuckDuckGo html failed (${res.status})`);
|
|
115
|
+
return parseDdgHtmlResults(await res.text());
|
|
116
|
+
}
|
|
117
|
+
async function fetchWebResults(query) {
|
|
118
|
+
const lite = await fetchDdgLiteResults(query);
|
|
119
|
+
if (lite.length > 0)
|
|
120
|
+
return lite;
|
|
121
|
+
return fetchDdgHtmlResults(query);
|
|
122
|
+
}
|
|
123
|
+
async function fetchDdgInstantAnswer(query) {
|
|
124
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1`;
|
|
125
|
+
const res = await fetch(url, {
|
|
126
|
+
headers: { "User-Agent": "agent-dev/1.0" },
|
|
127
|
+
signal: AbortSignal.timeout(10000),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return null;
|
|
131
|
+
const data = (await res.json());
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (data.AbstractText) {
|
|
134
|
+
parts.push(data.Heading ? `${data.Heading}: ${data.AbstractText}` : data.AbstractText);
|
|
135
|
+
if (data.AbstractURL)
|
|
136
|
+
parts.push(`Source: ${data.AbstractURL}`);
|
|
137
|
+
}
|
|
138
|
+
for (const topic of data.RelatedTopics ?? []) {
|
|
139
|
+
if ("Text" in topic && topic.Text) {
|
|
140
|
+
parts.push(topic.Text);
|
|
141
|
+
if (topic.FirstURL)
|
|
142
|
+
parts.push(` ${topic.FirstURL}`);
|
|
143
|
+
}
|
|
144
|
+
else if ("Topics" in topic && topic.Topics) {
|
|
145
|
+
for (const sub of topic.Topics) {
|
|
146
|
+
if (sub.Text)
|
|
147
|
+
parts.push(sub.Text);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (parts.length >= 6)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
154
|
+
}
|
|
155
|
+
function formatResults(query, instant, web) {
|
|
156
|
+
const lines = [`Search results for: ${query}`, ""];
|
|
157
|
+
if (instant) {
|
|
158
|
+
lines.push("Instant answer:", instant, "");
|
|
159
|
+
}
|
|
160
|
+
if (web.length === 0) {
|
|
161
|
+
lines.push(instant ? "(No additional web results.)" : "No results found.");
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
lines.push("Web results:");
|
|
165
|
+
for (const [i, r] of web.entries()) {
|
|
166
|
+
lines.push(`${i + 1}. ${r.title}`);
|
|
167
|
+
lines.push(` ${r.url}`);
|
|
168
|
+
if (r.snippet)
|
|
169
|
+
lines.push(` ${r.snippet}`);
|
|
170
|
+
lines.push("");
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n").trim();
|
|
173
|
+
}
|
|
174
|
+
export async function executeWebSearch(args) {
|
|
175
|
+
const query = args.query?.trim();
|
|
176
|
+
if (!query)
|
|
177
|
+
return "Error: query is required";
|
|
178
|
+
try {
|
|
179
|
+
const [instant, web] = await Promise.all([
|
|
180
|
+
fetchDdgInstantAnswer(query).catch(() => null),
|
|
181
|
+
fetchWebResults(query),
|
|
182
|
+
]);
|
|
183
|
+
return formatResults(query, instant, web);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return `Error: search failed — ${err instanceof Error ? err.message : String(err)}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function executeShellCommand(command: string, workdir: string): Promise<string>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { getShellConfig, normalizeCommand } from "../platform.js";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
const INSTALL_TIMEOUT_MS = 300_000;
|
|
5
|
+
const DEV_SERVER_RE = /\b(npm run dev|npm start|yarn dev|pnpm dev|next dev|nuxt dev|vite(\s|$)|react-scripts start|node .*--watch)\b/i;
|
|
6
|
+
const INSTALL_RE = /\b(npm install|npm ci|yarn install|pnpm install|npx create-|npm audit)\b/i;
|
|
7
|
+
function getCommandTimeout(command) {
|
|
8
|
+
return INSTALL_RE.test(command) ? INSTALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
9
|
+
}
|
|
10
|
+
function isDevServerCommand(command) {
|
|
11
|
+
return DEV_SERVER_RE.test(command) || /\brun dev\b/i.test(command);
|
|
12
|
+
}
|
|
13
|
+
export async function executeShellCommand(command, workdir) {
|
|
14
|
+
const shell = getShellConfig();
|
|
15
|
+
const normalized = normalizeCommand(command, shell);
|
|
16
|
+
if (isDevServerCommand(normalized)) {
|
|
17
|
+
return [
|
|
18
|
+
"Error: dev servers run until stopped and cannot run inside the agent.",
|
|
19
|
+
`Suggested: open a separate terminal, cd to the project, then run: ${normalized.trim()}`,
|
|
20
|
+
"To verify the project here, use a one-shot command like: npm run build",
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
const timeoutMs = getCommandTimeout(normalized);
|
|
24
|
+
const args = [...shell.args, normalized];
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
const child = spawn(shell.executable, args, {
|
|
29
|
+
cwd: workdir,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
CI: "1",
|
|
33
|
+
FORCE_COLOR: "0",
|
|
34
|
+
npm_config_yes: "true",
|
|
35
|
+
},
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.kill();
|
|
40
|
+
resolve(`Error: command timed out after ${timeoutMs / 1000}s\n${combineOutput(stdout, stderr)}\nTip: dev servers (npm run dev) cannot run here — use npm run build to verify, or run the dev server in your own terminal.`.trim());
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
child.stdout?.on("data", (chunk) => {
|
|
43
|
+
stdout += String(chunk);
|
|
44
|
+
});
|
|
45
|
+
child.stderr?.on("data", (chunk) => {
|
|
46
|
+
stderr += String(chunk);
|
|
47
|
+
});
|
|
48
|
+
child.on("error", (err) => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve(`Error: ${err.message}`);
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
const out = combineOutput(stdout, stderr);
|
|
55
|
+
if (code === 0)
|
|
56
|
+
return resolve(out.trim() || "(no output)");
|
|
57
|
+
resolve(out.trim() ? `${out.trim()}\n(exit ${code})` : `Error: command failed (exit ${code})`);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function combineOutput(stdout, stderr) {
|
|
62
|
+
return stdout + (stderr ? (stdout ? `\n${stderr}` : stderr) : "");
|
|
63
|
+
}
|
package/dist/modes/print-mode.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
4
|
import { runAgentLoop } from "../agent/loop.js";
|
|
5
|
+
async function promptCommandApproval(request) {
|
|
6
|
+
console.log(chalk.yellow(`\nCommand approval required:`));
|
|
7
|
+
console.log(chalk.white(` ${request.command}`));
|
|
8
|
+
const rl = createInterface({ input, output });
|
|
9
|
+
try {
|
|
10
|
+
const answer = await rl.question(chalk.gray("Run? [y/N] "));
|
|
11
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rl.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
export async function runPrintMode(session, prompt) {
|
|
4
18
|
const model = session.getModel();
|
|
5
19
|
console.log(chalk.gray(`Model: ${model.provider}/${model.id}`));
|
|
@@ -11,6 +25,7 @@ export async function runPrintMode(session, prompt) {
|
|
|
11
25
|
messages: [...prior, userMsg],
|
|
12
26
|
settings: session.getSettings(),
|
|
13
27
|
workdir: process.cwd(),
|
|
28
|
+
onPermissionRequest: promptCommandApproval,
|
|
14
29
|
onEvent: (event) => {
|
|
15
30
|
if (event.type === "text_delta") {
|
|
16
31
|
process.stdout.write(event.delta);
|
package/dist/providers/groq.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { toOpenAIMessages, toOpenAITools, processOpenAIStream, formatApiError, } from "./openai-compat.js";
|
|
2
3
|
export const PROVIDER_ID = "groq";
|
|
3
4
|
export const DEFAULT_MODEL = "llama-3.3-70b-versatile";
|
|
4
5
|
export const API_KEY_ENV = "GROQ_API_KEY";
|
|
@@ -13,51 +14,6 @@ export function getApiKey(settings) {
|
|
|
13
14
|
export function hasAuth(settings) {
|
|
14
15
|
return !!getApiKey(settings);
|
|
15
16
|
}
|
|
16
|
-
function toOpenAIMessages(ctx) {
|
|
17
|
-
const msgs = [];
|
|
18
|
-
if (ctx.systemPrompt) {
|
|
19
|
-
msgs.push({ role: "system", content: ctx.systemPrompt });
|
|
20
|
-
}
|
|
21
|
-
for (const m of ctx.messages) {
|
|
22
|
-
if (m.role === "user") {
|
|
23
|
-
msgs.push({ role: "user", content: m.content });
|
|
24
|
-
}
|
|
25
|
-
else if (m.role === "assistant") {
|
|
26
|
-
if (m.toolCalls?.length) {
|
|
27
|
-
msgs.push({
|
|
28
|
-
role: "assistant",
|
|
29
|
-
content: m.content || null,
|
|
30
|
-
tool_calls: m.toolCalls.map((tc) => ({
|
|
31
|
-
id: tc.id,
|
|
32
|
-
type: "function",
|
|
33
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
34
|
-
})),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
msgs.push({ role: "assistant", content: m.content });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else if (m.role === "tool") {
|
|
42
|
-
msgs.push({
|
|
43
|
-
role: "tool",
|
|
44
|
-
tool_call_id: m.toolCallId,
|
|
45
|
-
content: m.content,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return msgs;
|
|
50
|
-
}
|
|
51
|
-
function toOpenAITools(tools) {
|
|
52
|
-
return tools.map((t) => ({
|
|
53
|
-
type: "function",
|
|
54
|
-
function: {
|
|
55
|
-
name: t.name,
|
|
56
|
-
description: t.description,
|
|
57
|
-
parameters: t.parameters,
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
17
|
export async function* streamChat(model, ctx, settings) {
|
|
62
18
|
const apiKey = getApiKey(settings);
|
|
63
19
|
if (!apiKey) {
|
|
@@ -65,49 +21,19 @@ export async function* streamChat(model, ctx, settings) {
|
|
|
65
21
|
return;
|
|
66
22
|
}
|
|
67
23
|
const client = new OpenAI({ apiKey, baseURL: BASE_URL });
|
|
24
|
+
const hasTools = ctx.tools.length > 0;
|
|
68
25
|
try {
|
|
69
26
|
const stream = await client.chat.completions.create({
|
|
70
27
|
model: model.id,
|
|
71
28
|
messages: toOpenAIMessages(ctx),
|
|
72
|
-
tools:
|
|
29
|
+
tools: hasTools ? toOpenAITools(ctx.tools) : undefined,
|
|
30
|
+
tool_choice: hasTools ? "auto" : undefined,
|
|
31
|
+
parallel_tool_calls: false,
|
|
73
32
|
stream: true,
|
|
74
33
|
}, { signal: ctx.signal });
|
|
75
|
-
|
|
76
|
-
for await (const chunk of stream) {
|
|
77
|
-
const delta = chunk.choices[0]?.delta;
|
|
78
|
-
if (!delta)
|
|
79
|
-
continue;
|
|
80
|
-
if (delta.content) {
|
|
81
|
-
yield { type: "text_delta", delta: delta.content };
|
|
82
|
-
}
|
|
83
|
-
if (delta.tool_calls) {
|
|
84
|
-
for (const tc of delta.tool_calls) {
|
|
85
|
-
const idx = tc.index;
|
|
86
|
-
if (!toolCalls.has(idx)) {
|
|
87
|
-
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" });
|
|
88
|
-
}
|
|
89
|
-
const existing = toolCalls.get(idx);
|
|
90
|
-
if (tc.id)
|
|
91
|
-
existing.id = tc.id;
|
|
92
|
-
if (tc.function?.name)
|
|
93
|
-
existing.name = tc.function.name;
|
|
94
|
-
if (tc.function?.arguments) {
|
|
95
|
-
existing.arguments += tc.function.arguments;
|
|
96
|
-
yield {
|
|
97
|
-
type: "tool_call_delta",
|
|
98
|
-
index: idx,
|
|
99
|
-
id: existing.id,
|
|
100
|
-
name: existing.name,
|
|
101
|
-
argumentsDelta: tc.function.arguments,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
yield { type: "done" };
|
|
34
|
+
yield* processOpenAIStream(stream);
|
|
108
35
|
}
|
|
109
36
|
catch (err) {
|
|
110
|
-
|
|
111
|
-
yield { type: "error", message };
|
|
37
|
+
yield { type: "error", message: formatApiError(err) };
|
|
112
38
|
}
|
|
113
39
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import type { ChatContext, StreamEvent, ToolCall } from "./types.js";
|
|
3
|
+
export declare function sanitizeToolParameters(params: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
export declare function toOpenAITools(tools: ChatContext["tools"]): OpenAI.Chat.ChatCompletionTool[];
|
|
5
|
+
export declare function normalizeToolCalls(toolCalls: ToolCall[]): ToolCall[];
|
|
6
|
+
export declare function parseMalformedToolCalls(text: string): ToolCall[];
|
|
7
|
+
export declare function extractFailedGeneration(errorMessage: string): string | null;
|
|
8
|
+
export declare function toOpenAIMessages(ctx: ChatContext): OpenAI.Chat.ChatCompletionMessageParam[];
|
|
9
|
+
export declare function processOpenAIStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncGenerator<StreamEvent>;
|
|
10
|
+
export declare function formatApiError(err: unknown): string;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export function sanitizeToolParameters(params) {
|
|
2
|
+
return {
|
|
3
|
+
...params,
|
|
4
|
+
type: "object",
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function toOpenAITools(tools) {
|
|
9
|
+
return tools.map((t) => ({
|
|
10
|
+
type: "function",
|
|
11
|
+
function: {
|
|
12
|
+
name: t.name,
|
|
13
|
+
description: t.description,
|
|
14
|
+
parameters: sanitizeToolParameters(t.parameters),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export function normalizeToolCalls(toolCalls) {
|
|
19
|
+
return toolCalls.map((tc, i) => ({
|
|
20
|
+
id: tc.id?.trim() || `call_${Date.now()}_${i}`,
|
|
21
|
+
name: tc.name,
|
|
22
|
+
arguments: tc.arguments?.trim() || "{}",
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
/** Recover tool calls Groq/Llama sometimes emit as malformed text instead of structured tool_calls. */
|
|
26
|
+
function unescapeJsonString(value) {
|
|
27
|
+
return value
|
|
28
|
+
.replace(/\\n/g, "\n")
|
|
29
|
+
.replace(/\\t/g, "\t")
|
|
30
|
+
.replace(/\\"/g, '"')
|
|
31
|
+
.replace(/\\\\/g, "\\");
|
|
32
|
+
}
|
|
33
|
+
function parseToolArguments(raw) {
|
|
34
|
+
const body = raw.replace(/^[\[\]\s]*/, "").trim();
|
|
35
|
+
if (!body.startsWith("{"))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
JSON.parse(body);
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Groq often truncates JSON — extract known fields.
|
|
43
|
+
}
|
|
44
|
+
const commandMatch = body.match(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
45
|
+
if (commandMatch) {
|
|
46
|
+
return JSON.stringify({ command: unescapeJsonString(commandMatch[1]) });
|
|
47
|
+
}
|
|
48
|
+
const truncatedCommand = body.match(/"command"\s*:\s*"([\s\S]+)$/);
|
|
49
|
+
if (truncatedCommand) {
|
|
50
|
+
const command = unescapeJsonString(truncatedCommand[1].replace(/\\+$/, ""));
|
|
51
|
+
return JSON.stringify({ command });
|
|
52
|
+
}
|
|
53
|
+
const queryMatch = body.match(/"query"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
54
|
+
if (queryMatch) {
|
|
55
|
+
return JSON.stringify({ query: unescapeJsonString(queryMatch[1]) });
|
|
56
|
+
}
|
|
57
|
+
const pathMatch = body.match(/"path"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
58
|
+
if (pathMatch) {
|
|
59
|
+
return JSON.stringify({ path: unescapeJsonString(pathMatch[1]) });
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function pushRecovered(results, name, args) {
|
|
64
|
+
results.push({
|
|
65
|
+
id: `recovered_${Date.now()}_${results.length}`,
|
|
66
|
+
name,
|
|
67
|
+
arguments: args,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function parseMalformedToolCalls(text) {
|
|
71
|
+
const results = [];
|
|
72
|
+
if (!text)
|
|
73
|
+
return results;
|
|
74
|
+
const patterns = [
|
|
75
|
+
/<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]*?\})\s*<\/function>/gi,
|
|
76
|
+
/<function=([a-zA-Z0-9_]+)\s*>\s*(\{[\s\S]*?\})\s*<\/function>/gi,
|
|
77
|
+
/<tool_call>\s*([a-zA-Z0-9_]+)\s*(\{[\s\S]*?\})\s*<\/tool_call>/gi,
|
|
78
|
+
];
|
|
79
|
+
for (const re of patterns) {
|
|
80
|
+
re.lastIndex = 0;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = re.exec(text)) !== null) {
|
|
83
|
+
const name = match[1].trim();
|
|
84
|
+
const args = parseToolArguments(match[2]);
|
|
85
|
+
if (name && args)
|
|
86
|
+
pushRecovered(results, name, args);
|
|
87
|
+
}
|
|
88
|
+
if (results.length > 0)
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (results.length === 0) {
|
|
92
|
+
const looseRe = /<function=([a-zA-Z0-9_]+)([\s\S]*?)<\/function>/gi;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = looseRe.exec(text)) !== null) {
|
|
95
|
+
const name = match[1].trim();
|
|
96
|
+
const args = parseToolArguments(match[2]);
|
|
97
|
+
if (name && args)
|
|
98
|
+
pushRecovered(results, name, args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (results.length === 0) {
|
|
102
|
+
const truncatedRe = /<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]+)/gi;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = truncatedRe.exec(text)) !== null) {
|
|
105
|
+
const name = match[1].trim();
|
|
106
|
+
const args = parseToolArguments(match[2]);
|
|
107
|
+
if (name && args)
|
|
108
|
+
pushRecovered(results, name, args);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
export function extractFailedGeneration(errorMessage) {
|
|
114
|
+
const marker = "Model output:";
|
|
115
|
+
const idx = errorMessage.indexOf(marker);
|
|
116
|
+
if (idx >= 0)
|
|
117
|
+
return errorMessage.slice(idx + marker.length).trim();
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
export function toOpenAIMessages(ctx) {
|
|
121
|
+
const msgs = [];
|
|
122
|
+
if (ctx.systemPrompt) {
|
|
123
|
+
msgs.push({ role: "system", content: ctx.systemPrompt });
|
|
124
|
+
}
|
|
125
|
+
for (const m of ctx.messages) {
|
|
126
|
+
if (m.role === "user") {
|
|
127
|
+
msgs.push({ role: "user", content: m.content });
|
|
128
|
+
}
|
|
129
|
+
else if (m.role === "assistant") {
|
|
130
|
+
if (m.toolCalls?.length) {
|
|
131
|
+
const toolCalls = normalizeToolCalls(m.toolCalls);
|
|
132
|
+
msgs.push({
|
|
133
|
+
role: "assistant",
|
|
134
|
+
content: m.content || null,
|
|
135
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
136
|
+
id: tc.id,
|
|
137
|
+
type: "function",
|
|
138
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
msgs.push({ role: "assistant", content: m.content });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (m.role === "tool") {
|
|
147
|
+
msgs.push({
|
|
148
|
+
role: "tool",
|
|
149
|
+
tool_call_id: m.toolCallId,
|
|
150
|
+
content: m.content,
|
|
151
|
+
...(m.name ? { name: m.name } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return msgs;
|
|
156
|
+
}
|
|
157
|
+
export async function* processOpenAIStream(stream) {
|
|
158
|
+
const toolCalls = new Map();
|
|
159
|
+
for await (const chunk of stream) {
|
|
160
|
+
const choice = chunk.choices[0];
|
|
161
|
+
if (choice?.finish_reason === "tool_calls" || choice?.delta?.tool_calls) {
|
|
162
|
+
// normal path
|
|
163
|
+
}
|
|
164
|
+
const delta = choice?.delta;
|
|
165
|
+
if (!delta)
|
|
166
|
+
continue;
|
|
167
|
+
if (delta.content) {
|
|
168
|
+
yield { type: "text_delta", delta: delta.content };
|
|
169
|
+
}
|
|
170
|
+
if (delta.tool_calls) {
|
|
171
|
+
for (const tc of delta.tool_calls) {
|
|
172
|
+
const idx = tc.index ?? 0;
|
|
173
|
+
if (!toolCalls.has(idx)) {
|
|
174
|
+
toolCalls.set(idx, { id: tc.id ?? "", name: tc.function?.name ?? "", arguments: "" });
|
|
175
|
+
}
|
|
176
|
+
const existing = toolCalls.get(idx);
|
|
177
|
+
if (tc.id)
|
|
178
|
+
existing.id = tc.id;
|
|
179
|
+
if (tc.function?.name)
|
|
180
|
+
existing.name = tc.function.name;
|
|
181
|
+
if (tc.function?.arguments) {
|
|
182
|
+
existing.arguments += tc.function.arguments;
|
|
183
|
+
yield {
|
|
184
|
+
type: "tool_call_delta",
|
|
185
|
+
index: idx,
|
|
186
|
+
id: existing.id,
|
|
187
|
+
name: existing.name,
|
|
188
|
+
argumentsDelta: tc.function.arguments,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (chunk.usage) {
|
|
194
|
+
yield {
|
|
195
|
+
type: "done",
|
|
196
|
+
usage: {
|
|
197
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
198
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
yield { type: "done" };
|
|
204
|
+
}
|
|
205
|
+
export function formatApiError(err) {
|
|
206
|
+
const apiErr = err;
|
|
207
|
+
const failed = apiErr.error?.failed_generation;
|
|
208
|
+
const msg = apiErr.error?.message ?? apiErr.message ?? "API error";
|
|
209
|
+
if (failed)
|
|
210
|
+
return `${msg}\nModel output: ${failed.slice(0, 300)}`;
|
|
211
|
+
return msg;
|
|
212
|
+
}
|