Liam Dyer Mishig commited on
Commit
aa0485a
·
unverified ·
1 Parent(s): 5459f31

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 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* runTool(
52
  ctx: BackendToolContext,
53
  tools: BackendTool[],
54
  call: ToolCall
@@ -72,47 +73,38 @@ async function* runTool(
72
  uuid,
73
  call,
74
  };
 
75
  try {
76
- try {
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
- MetricsServer.getMetrics().tool.toolUseDuration.observe(
95
- { tool: call.name },
96
- Date.now() - startTime
97
- );
98
-
99
- return { ...toolResult, call } as ToolResult;
100
- } catch (e) {
101
- MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
102
- yield {
103
- type: MessageUpdateType.Tool,
104
- subtype: MessageToolUpdateType.Error,
105
- uuid,
106
- message: e instanceof Error ? e.message : String(e),
107
- };
108
- }
109
- } catch (cause) {
110
  MetricsServer.getMetrics().tool.toolUseCountError.inc({ tool: call.name });
111
- console.error(Error(`Failed while running tool ${call.name}`), { cause });
 
 
 
 
 
 
 
 
112
  return {
113
  call,
114
  status: ToolResultStatus.Error,
115
- message: cause instanceof Error ? cause.message : String(cause),
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 (cause) {
164
  // error parsing the calls
165
  yield {
166
  type: MessageUpdateType.Status,
167
  status: MessageUpdateStatus.Error,
168
- message: cause instanceof Error ? cause.message : String(cause),
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) => runTool(toolContext, tools, 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 (e) {
29
- return {
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
- return {
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
- return {
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
- return {
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, ToolResultError, ToolResultSuccess } from "$lib/types/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, ToolResultOmitted, undefined>;
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
+ }