Spaces:
Running
Running
Liam Dyer
Mishig
commited on
Simplify tool error handling (#1168)
Browse files* feat: simplify tool error handling
* feat: resolve linting and type errors
---------
Co-authored-by: Mishig <[email protected]>
- src/lib/server/textGeneration/tools.ts +31 -39
- src/lib/server/tools/calculator.ts +2 -7
- src/lib/server/tools/directlyAnswer.ts +0 -2
- src/lib/server/tools/documentParser.ts +2 -14
- src/lib/server/tools/images/editing.ts +3 -18
- src/lib/server/tools/images/generation.ts +0 -2
- src/lib/server/tools/index.ts +2 -8
- src/lib/server/tools/web/search.ts +0 -2
- src/lib/server/tools/web/url.ts +0 -2
- src/lib/utils/stringifyError.ts +12 -0
src/lib/server/textGeneration/tools.ts
CHANGED
@@ -19,6 +19,7 @@ import { toolHasName } from "../tools/utils";
|
|
19 |
import type { MessageFile } from "$lib/types/Message";
|
20 |
import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
|
21 |
import { MetricsServer } from "../metrics";
|
|
|
22 |
|
23 |
function makeFilesPrompt(files: MessageFile[], fileMessageIndex: number): string {
|
24 |
if (files.length === 0) {
|
@@ -48,7 +49,7 @@ export function pickTools(
|
|
48 |
});
|
49 |
}
|
50 |
|
51 |
-
async function*
|
52 |
ctx: BackendToolContext,
|
53 |
tools: BackendTool[],
|
54 |
call: ToolCall
|
@@ -72,47 +73,38 @@ async function* runTool(
|
|
72 |
uuid,
|
73 |
call,
|
74 |
};
|
|
|
75 |
try {
|
76 |
-
|
77 |
-
const toolResult = yield* tool.call(call.parameters, ctx);
|
78 |
-
if (toolResult.status === ToolResultStatus.Error) {
|
79 |
-
yield {
|
80 |
-
type: MessageUpdateType.Tool,
|
81 |
-
subtype: MessageToolUpdateType.Error,
|
82 |
-
uuid,
|
83 |
-
message: toolResult.message,
|
84 |
-
};
|
85 |
-
} else {
|
86 |
-
yield {
|
87 |
-
type: MessageUpdateType.Tool,
|
88 |
-
subtype: MessageToolUpdateType.Result,
|
89 |
-
uuid,
|
90 |
-
result: { ...toolResult, call } as ToolResult,
|
91 |
-
};
|
92 |
-
}
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
}
|
109 |
-
} catch (cause) {
|
110 |
MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
return {
|
113 |
call,
|
114 |
status: ToolResultStatus.Error,
|
115 |
-
message:
|
116 |
};
|
117 |
}
|
118 |
}
|
@@ -160,12 +152,12 @@ export async function* runTools(
|
|
160 |
calls.push(
|
161 |
...JSON5.parse(block).filter(isExternalToolCall).map(externalToToolCall).filter(Boolean)
|
162 |
);
|
163 |
-
} catch (
|
164 |
// error parsing the calls
|
165 |
yield {
|
166 |
type: MessageUpdateType.Status,
|
167 |
status: MessageUpdateStatus.Error,
|
168 |
-
message:
|
169 |
};
|
170 |
}
|
171 |
}
|
@@ -179,7 +171,7 @@ export async function* runTools(
|
|
179 |
|
180 |
const toolContext: BackendToolContext = { conv, messages, preprompt, assistant, ip, username };
|
181 |
const toolResults: (ToolResult | undefined)[] = yield* mergeAsyncGenerators(
|
182 |
-
calls.map((call) =>
|
183 |
);
|
184 |
return toolResults.filter((result): result is ToolResult => result !== undefined);
|
185 |
}
|
|
|
19 |
import type { MessageFile } from "$lib/types/Message";
|
20 |
import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
|
21 |
import { MetricsServer } from "../metrics";
|
22 |
+
import { stringifyError } from "$lib/utils/stringifyError";
|
23 |
|
24 |
function makeFilesPrompt(files: MessageFile[], fileMessageIndex: number): string {
|
25 |
if (files.length === 0) {
|
|
|
49 |
});
|
50 |
}
|
51 |
|
52 |
+
async function* callTool(
|
53 |
ctx: BackendToolContext,
|
54 |
tools: BackendTool[],
|
55 |
call: ToolCall
|
|
|
73 |
uuid,
|
74 |
call,
|
75 |
};
|
76 |
+
|
77 |
try {
|
78 |
+
const toolResult = yield* tool.call(call.parameters, ctx);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
+
yield {
|
81 |
+
type: MessageUpdateType.Tool,
|
82 |
+
subtype: MessageToolUpdateType.Result,
|
83 |
+
uuid,
|
84 |
+
result: { ...toolResult, call } as ToolResult,
|
85 |
+
};
|
86 |
+
|
87 |
+
MetricsServer.getMetrics().tool.toolUseDuration.observe(
|
88 |
+
{ tool: call.name },
|
89 |
+
Date.now() - startTime
|
90 |
+
);
|
91 |
+
|
92 |
+
return { ...toolResult, call } as ToolResult;
|
93 |
+
} catch (error) {
|
|
|
|
|
94 |
MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
|
95 |
+
logger.error(error, `Failed while running tool ${call.name}`);
|
96 |
+
|
97 |
+
yield {
|
98 |
+
type: MessageUpdateType.Tool,
|
99 |
+
subtype: MessageToolUpdateType.Error,
|
100 |
+
uuid,
|
101 |
+
message: stringifyError(error),
|
102 |
+
};
|
103 |
+
|
104 |
return {
|
105 |
call,
|
106 |
status: ToolResultStatus.Error,
|
107 |
+
message: stringifyError(error),
|
108 |
};
|
109 |
}
|
110 |
}
|
|
|
152 |
calls.push(
|
153 |
...JSON5.parse(block).filter(isExternalToolCall).map(externalToToolCall).filter(Boolean)
|
154 |
);
|
155 |
+
} catch (e) {
|
156 |
// error parsing the calls
|
157 |
yield {
|
158 |
type: MessageUpdateType.Status,
|
159 |
status: MessageUpdateStatus.Error,
|
160 |
+
message: stringifyError(e),
|
161 |
};
|
162 |
}
|
163 |
}
|
|
|
171 |
|
172 |
const toolContext: BackendToolContext = { conv, messages, preprompt, assistant, ip, username };
|
173 |
const toolResults: (ToolResult | undefined)[] = yield* mergeAsyncGenerators(
|
174 |
+
calls.map((call) => callTool(toolContext, tools, call))
|
175 |
);
|
176 |
return toolResults.filter((result): result is ToolResult => result !== undefined);
|
177 |
}
|
src/lib/server/tools/calculator.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
2 |
import type { BackendTool } from ".";
|
3 |
import vm from "node:vm";
|
4 |
|
@@ -22,14 +21,10 @@ const calculator: BackendTool = {
|
|
22 |
const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, "");
|
23 |
|
24 |
return {
|
25 |
-
status: ToolResultStatus.Success,
|
26 |
outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }],
|
27 |
};
|
28 |
-
} catch (
|
29 |
-
|
30 |
-
status: ToolResultStatus.Error,
|
31 |
-
message: "Invalid expression",
|
32 |
-
};
|
33 |
}
|
34 |
},
|
35 |
};
|
|
|
|
|
1 |
import type { BackendTool } from ".";
|
2 |
import vm from "node:vm";
|
3 |
|
|
|
21 |
const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, "");
|
22 |
|
23 |
return {
|
|
|
24 |
outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }],
|
25 |
};
|
26 |
+
} catch (cause) {
|
27 |
+
throw Error("Invalid expression", { cause });
|
|
|
|
|
|
|
28 |
}
|
29 |
},
|
30 |
};
|
src/lib/server/tools/directlyAnswer.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
2 |
import type { BackendTool } from ".";
|
3 |
|
4 |
const directlyAnswer: BackendTool = {
|
@@ -10,7 +9,6 @@ const directlyAnswer: BackendTool = {
|
|
10 |
parameterDefinitions: {},
|
11 |
async *call() {
|
12 |
return {
|
13 |
-
status: ToolResultStatus.Success,
|
14 |
outputs: [],
|
15 |
display: false,
|
16 |
};
|
|
|
|
|
1 |
import type { BackendTool } from ".";
|
2 |
|
3 |
const directlyAnswer: BackendTool = {
|
|
|
9 |
parameterDefinitions: {},
|
10 |
async *call() {
|
11 |
return {
|
|
|
12 |
outputs: [],
|
13 |
display: false,
|
14 |
};
|
src/lib/server/tools/documentParser.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import type { BackendTool } from ".";
|
2 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
3 |
import { callSpace, getIpToken } from "./utils";
|
4 |
import { downloadFile } from "$lib/server/files/downloadFile";
|
5 |
|
@@ -29,18 +28,8 @@ const documentParser: BackendTool = {
|
|
29 |
|
30 |
const message = messages[fileMessageIndex];
|
31 |
const files = message?.files ?? [];
|
32 |
-
if (!files || files.length === 0)
|
33 |
-
|
34 |
-
status: ToolResultStatus.Error,
|
35 |
-
message: "User did not provide a pdf to parse",
|
36 |
-
};
|
37 |
-
}
|
38 |
-
if (fileIndex >= files.length) {
|
39 |
-
return {
|
40 |
-
status: ToolResultStatus.Error,
|
41 |
-
message: "Model provided an invalid file index",
|
42 |
-
};
|
43 |
-
}
|
44 |
|
45 |
const file = files[fileIndex];
|
46 |
const fileBlob = await downloadFile(files[fileIndex].value, conv._id)
|
@@ -62,7 +51,6 @@ const documentParser: BackendTool = {
|
|
62 |
documentMarkdown = documentMarkdown.slice(0, 30_000) + "\n\n... (truncated)";
|
63 |
}
|
64 |
return {
|
65 |
-
status: ToolResultStatus.Success,
|
66 |
outputs: [{ [file.name]: documentMarkdown }],
|
67 |
display: false,
|
68 |
};
|
|
|
1 |
import type { BackendTool } from ".";
|
|
|
2 |
import { callSpace, getIpToken } from "./utils";
|
3 |
import { downloadFile } from "$lib/server/files/downloadFile";
|
4 |
|
|
|
28 |
|
29 |
const message = messages[fileMessageIndex];
|
30 |
const files = message?.files ?? [];
|
31 |
+
if (!files || files.length === 0) throw Error("User did not provide a pdf to parse");
|
32 |
+
if (fileIndex >= files.length) throw Error("Model provided an invalid file index");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
const file = files[fileIndex];
|
35 |
const fileBlob = await downloadFile(files[fileIndex].value, conv._id)
|
|
|
51 |
documentMarkdown = documentMarkdown.slice(0, 30_000) + "\n\n... (truncated)";
|
52 |
}
|
53 |
return {
|
|
|
54 |
outputs: [{ [file.name]: documentMarkdown }],
|
55 |
display: false,
|
56 |
};
|
src/lib/server/tools/images/editing.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import type { BackendTool } from "..";
|
2 |
import { uploadFile } from "../../files/uploadFile";
|
3 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
4 |
import { MessageUpdateType } from "$lib/types/MessageUpdate";
|
5 |
import { callSpace, getIpToken, type GradioImage } from "../utils";
|
6 |
import { downloadFile } from "$lib/server/files/downloadFile";
|
@@ -44,23 +43,10 @@ const imageEditing: BackendTool = {
|
|
44 |
|
45 |
const message = messages[fileMessageIndex];
|
46 |
const images = message?.files ?? [];
|
47 |
-
if (!images || images.length === 0)
|
48 |
-
|
49 |
-
status: ToolResultStatus.Error,
|
50 |
-
message: "User did not provide an image to edit.",
|
51 |
-
};
|
52 |
-
}
|
53 |
-
if (fileIndex >= images.length) {
|
54 |
-
return {
|
55 |
-
status: ToolResultStatus.Error,
|
56 |
-
message: "Model provided an invalid file index",
|
57 |
-
};
|
58 |
-
}
|
59 |
if (!images[fileIndex].mime.startsWith("image/")) {
|
60 |
-
|
61 |
-
status: ToolResultStatus.Error,
|
62 |
-
message: "Model provided a file indx which is not an image",
|
63 |
-
};
|
64 |
}
|
65 |
|
66 |
// todo: should handle multiple images
|
@@ -96,7 +82,6 @@ const imageEditing: BackendTool = {
|
|
96 |
};
|
97 |
|
98 |
return {
|
99 |
-
status: ToolResultStatus.Success,
|
100 |
outputs: [
|
101 |
{
|
102 |
imageEditing: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it.`,
|
|
|
1 |
import type { BackendTool } from "..";
|
2 |
import { uploadFile } from "../../files/uploadFile";
|
|
|
3 |
import { MessageUpdateType } from "$lib/types/MessageUpdate";
|
4 |
import { callSpace, getIpToken, type GradioImage } from "../utils";
|
5 |
import { downloadFile } from "$lib/server/files/downloadFile";
|
|
|
43 |
|
44 |
const message = messages[fileMessageIndex];
|
45 |
const images = message?.files ?? [];
|
46 |
+
if (!images || images.length === 0) throw Error("User did not provide an image to edit");
|
47 |
+
if (fileIndex >= images.length) throw Error("Model provided an invalid file index");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
if (!images[fileIndex].mime.startsWith("image/")) {
|
49 |
+
throw Error("Model provided a file idex which is not an image");
|
|
|
|
|
|
|
50 |
}
|
51 |
|
52 |
// todo: should handle multiple images
|
|
|
82 |
};
|
83 |
|
84 |
return {
|
|
|
85 |
outputs: [
|
86 |
{
|
87 |
imageEditing: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it.`,
|
src/lib/server/tools/images/generation.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import type { BackendTool } from "..";
|
2 |
import { uploadFile } from "../../files/uploadFile";
|
3 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
4 |
import { MessageUpdateType } from "$lib/types/MessageUpdate";
|
5 |
import { callSpace, getIpToken, type GradioImage } from "../utils";
|
6 |
|
@@ -81,7 +80,6 @@ const imageGeneration: BackendTool = {
|
|
81 |
}
|
82 |
|
83 |
return {
|
84 |
-
status: ToolResultStatus.Success,
|
85 |
outputs: [
|
86 |
{
|
87 |
imageGeneration: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it.`,
|
|
|
1 |
import type { BackendTool } from "..";
|
2 |
import { uploadFile } from "../../files/uploadFile";
|
|
|
3 |
import { MessageUpdateType } from "$lib/types/MessageUpdate";
|
4 |
import { callSpace, getIpToken, type GradioImage } from "../utils";
|
5 |
|
|
|
80 |
}
|
81 |
|
82 |
return {
|
|
|
83 |
outputs: [
|
84 |
{
|
85 |
imageGeneration: `An image has been generated for the following prompt: "${prompt}". Answer as if the user can already see the image. Do not try to insert the image or to add space for it. The user can already see the image. Do not try to describe the image as you the model cannot see it.`,
|
src/lib/server/tools/index.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
2 |
-
import type { Tool,
|
3 |
|
4 |
import calculator from "./calculator";
|
5 |
import directlyAnswer from "./directlyAnswer";
|
@@ -15,17 +15,11 @@ export type BackendToolContext = Pick<
|
|
15 |
"conv" | "messages" | "assistant" | "ip" | "username"
|
16 |
> & { preprompt?: string };
|
17 |
|
18 |
-
// typescript can't narrow a discriminated union after applying a generic like Omit to it
|
19 |
-
// so we have to define the omitted types and create a new union
|
20 |
-
type ToolResultSuccessOmitted = Omit<ToolResultSuccess, "call">;
|
21 |
-
type ToolResultErrorOmitted = Omit<ToolResultError, "call">;
|
22 |
-
type ToolResultOmitted = ToolResultSuccessOmitted | ToolResultErrorOmitted;
|
23 |
-
|
24 |
export interface BackendTool extends Tool {
|
25 |
call(
|
26 |
params: Record<string, string | number | boolean>,
|
27 |
context: BackendToolContext
|
28 |
-
): AsyncGenerator<MessageUpdate,
|
29 |
}
|
30 |
|
31 |
export const allTools: BackendTool[] = [
|
|
|
1 |
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
2 |
+
import type { Tool, ToolResultSuccess } from "$lib/types/Tool";
|
3 |
|
4 |
import calculator from "./calculator";
|
5 |
import directlyAnswer from "./directlyAnswer";
|
|
|
15 |
"conv" | "messages" | "assistant" | "ip" | "username"
|
16 |
> & { preprompt?: string };
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
export interface BackendTool extends Tool {
|
19 |
call(
|
20 |
params: Record<string, string | number | boolean>,
|
21 |
context: BackendToolContext
|
22 |
+
): AsyncGenerator<MessageUpdate, Omit<ToolResultSuccess, "status" | "call" | "type">, undefined>;
|
23 |
}
|
24 |
|
25 |
export const allTools: BackendTool[] = [
|
src/lib/server/tools/web/search.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
2 |
import type { BackendTool } from "..";
|
3 |
import { runWebSearch } from "../../websearch/runWebSearch";
|
4 |
|
@@ -23,7 +22,6 @@ const websearch: BackendTool = {
|
|
23 |
.join("\n------------\n");
|
24 |
|
25 |
return {
|
26 |
-
status: ToolResultStatus.Success,
|
27 |
outputs: [{ websearch: chunks }],
|
28 |
display: false,
|
29 |
};
|
|
|
|
|
1 |
import type { BackendTool } from "..";
|
2 |
import { runWebSearch } from "../../websearch/runWebSearch";
|
3 |
|
|
|
22 |
.join("\n------------\n");
|
23 |
|
24 |
return {
|
|
|
25 |
outputs: [{ websearch: chunks }],
|
26 |
display: false,
|
27 |
};
|
src/lib/server/tools/web/url.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify";
|
2 |
import { scrapeUrl } from "$lib/server/websearch/scrape/scrape";
|
3 |
-
import { ToolResultStatus } from "$lib/types/Tool";
|
4 |
import type { BackendTool } from "..";
|
5 |
|
6 |
const fetchUrl: BackendTool = {
|
@@ -22,7 +21,6 @@ const fetchUrl: BackendTool = {
|
|
22 |
const { title, markdownTree } = await scrapeUrl(url, Infinity);
|
23 |
|
24 |
return {
|
25 |
-
status: ToolResultStatus.Success,
|
26 |
outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }],
|
27 |
display: false,
|
28 |
};
|
|
|
1 |
import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify";
|
2 |
import { scrapeUrl } from "$lib/server/websearch/scrape/scrape";
|
|
|
3 |
import type { BackendTool } from "..";
|
4 |
|
5 |
const fetchUrl: BackendTool = {
|
|
|
21 |
const { title, markdownTree } = await scrapeUrl(url, Infinity);
|
22 |
|
23 |
return {
|
|
|
24 |
outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }],
|
25 |
display: false,
|
26 |
};
|
src/lib/utils/stringifyError.ts
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** Takes an unknown error and attempts to convert it to a string */
|
2 |
+
export function stringifyError(error: unknown): string {
|
3 |
+
if (error instanceof Error) return error.message;
|
4 |
+
if (typeof error === "string") return error;
|
5 |
+
if (typeof error === "object" && error !== null) {
|
6 |
+
// try a few common properties
|
7 |
+
if ("message" in error && typeof error.message === "string") return error.message;
|
8 |
+
if ("body" in error && typeof error.body === "string") return error.body;
|
9 |
+
if ("name" in error && typeof error.name === "string") return error.name;
|
10 |
+
}
|
11 |
+
return "Unknown error";
|
12 |
+
}
|