From 20997cbe9eab81b307a51c141afe18866c917772 Mon Sep 17 00:00:00 2001
From: thucpn <thucsh2@gmail.com>
Date: Mon, 17 Mar 2025 10:47:46 +0700
Subject: [PATCH] tool factory

---
 packages/tools/src/settings.ts                |  22 ----
 packages/tools/src/tools/code-generator.ts    |  75 +++++++------
 .../tools/src/tools/document-generator.ts     |   5 +-
 packages/tools/src/tools/duckduckgo.ts        |  74 ++++++-------
 packages/tools/src/tools/img-gen.ts           | 102 ++++++++++++------
 packages/tools/src/tools/interpreter.ts       |  15 ++-
 packages/tools/src/tools/weather.ts           |  62 +++++------
 7 files changed, 195 insertions(+), 160 deletions(-)
 delete mode 100644 packages/tools/src/settings.ts

diff --git a/packages/tools/src/settings.ts b/packages/tools/src/settings.ts
deleted file mode 100644
index d0d0080d4..000000000
--- a/packages/tools/src/settings.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-class GlobalToolSettings {
-  private _outputDir: string = "output/tools";
-  private _fileServerURLPrefix: string | undefined;
-
-  set outputDir(outputDir: string) {
-    this._outputDir = outputDir;
-  }
-
-  get outputDir() {
-    return this._outputDir;
-  }
-
-  set fileServerURLPrefix(fileServerURLPrefix: string | undefined) {
-    this._fileServerURLPrefix = fileServerURLPrefix;
-  }
-
-  get fileServerURLPrefix() {
-    return this._fileServerURLPrefix;
-  }
-}
-
-export const ToolSettings = new GlobalToolSettings();
diff --git a/packages/tools/src/tools/code-generator.ts b/packages/tools/src/tools/code-generator.ts
index e98a7e762..8e0302895 100644
--- a/packages/tools/src/tools/code-generator.ts
+++ b/packages/tools/src/tools/code-generator.ts
@@ -77,39 +77,44 @@ async function generateArtifact(
   }
 }
 
-export const codeGenerator = tool({
-  name: "artifact",
-  description:
-    "Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.",
-  parameters: z.object({
-    requirement: z
-      .string()
-      .describe("The description of the application you want to build."),
-    oldCode: z.string().optional().describe("The existing code to be modified"),
-    sandboxFiles: z
-      .array(z.string())
-      .optional()
-      .describe(
-        "A list of sandbox file paths. Include these files if the code requires them.",
-      ),
-  }),
-  execute: async ({
-    requirement,
-    oldCode,
-    sandboxFiles,
-  }): Promise<JSONValue> => {
-    try {
-      const artifact = await generateArtifact(
-        requirement,
-        oldCode,
-        sandboxFiles, // help the generated code use exact files
-      );
-      if (sandboxFiles) {
-        artifact.files = sandboxFiles;
+export const codeGenerator = () => {
+  return tool({
+    name: "artifact",
+    description:
+      "Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.",
+    parameters: z.object({
+      requirement: z
+        .string()
+        .describe("The description of the application you want to build."),
+      oldCode: z
+        .string()
+        .optional()
+        .describe("The existing code to be modified"),
+      sandboxFiles: z
+        .array(z.string())
+        .optional()
+        .describe(
+          "A list of sandbox file paths. Include these files if the code requires them.",
+        ),
+    }),
+    execute: async ({
+      requirement,
+      oldCode,
+      sandboxFiles,
+    }): Promise<JSONValue> => {
+      try {
+        const artifact = await generateArtifact(
+          requirement,
+          oldCode,
+          sandboxFiles, // help the generated code use exact files
+        );
+        if (sandboxFiles) {
+          artifact.files = sandboxFiles;
+        }
+        return artifact as JSONValue;
+      } catch (error) {
+        return { isError: true };
       }
-      return artifact as JSONValue;
-    } catch (error) {
-      return { isError: true };
-    }
-  },
-});
+    },
+  });
+};
diff --git a/packages/tools/src/tools/document-generator.ts b/packages/tools/src/tools/document-generator.ts
index 9a4f74130..cf00db07d 100644
--- a/packages/tools/src/tools/document-generator.ts
+++ b/packages/tools/src/tools/document-generator.ts
@@ -137,6 +137,5 @@ export class DocumentGenerator implements BaseTool<DocumentParameter> {
   }
 }
 
-export function getTools(): BaseTool[] {
-  return [new DocumentGenerator({})];
-}
+export const documentGenerator = (params?: DocumentGeneratorParams) =>
+  new DocumentGenerator(params ?? {});
diff --git a/packages/tools/src/tools/duckduckgo.ts b/packages/tools/src/tools/duckduckgo.ts
index 4f2a3e24a..628a3c702 100644
--- a/packages/tools/src/tools/duckduckgo.ts
+++ b/packages/tools/src/tools/duckduckgo.ts
@@ -8,39 +8,41 @@ export type DuckDuckGoToolOutput = {
   url: string;
 }[];
 
-export const duckduckgo = tool({
-  name: "duckduckgo_search",
-  description:
-    "Use this function to search for information (only text) in the internet using DuckDuckGo.",
-  parameters: z.object({
-    query: z.string().describe("The query to search in DuckDuckGo."),
-    region: z
-      .string()
-      .optional()
-      .describe(
-        "Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
-      ),
-    maxResults: z
-      .number()
-      .default(10)
-      .optional()
-      .describe(
-        "Optional, The maximum number of results to be returned. Default is 10.",
-      ),
-  }),
-  execute: async ({
-    query,
-    region,
-    maxResults = 10,
-  }): Promise<DuckDuckGoToolOutput> => {
-    const options = region ? { region } : {};
-    const searchResults = await search(query, options);
-    return searchResults.results.slice(0, maxResults).map((result) => {
-      return {
-        title: result.title,
-        description: result.description,
-        url: result.url,
-      };
-    });
-  },
-});
+export const duckduckgo = () => {
+  return tool({
+    name: "duckduckgo_search",
+    description:
+      "Use this function to search for information (only text) in the internet using DuckDuckGo.",
+    parameters: z.object({
+      query: z.string().describe("The query to search in DuckDuckGo."),
+      region: z
+        .string()
+        .optional()
+        .describe(
+          "Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
+        ),
+      maxResults: z
+        .number()
+        .default(10)
+        .optional()
+        .describe(
+          "Optional, The maximum number of results to be returned. Default is 10.",
+        ),
+    }),
+    execute: async ({
+      query,
+      region,
+      maxResults = 10,
+    }): Promise<DuckDuckGoToolOutput> => {
+      const options = region ? { region } : {};
+      const searchResults = await search(query, options);
+      return searchResults.results.slice(0, maxResults).map((result) => {
+        return {
+          title: result.title,
+          description: result.description,
+          url: result.url,
+        };
+      });
+    },
+  });
+};
diff --git a/packages/tools/src/tools/img-gen.ts b/packages/tools/src/tools/img-gen.ts
index 654fc6a51..2f7d32e35 100644
--- a/packages/tools/src/tools/img-gen.ts
+++ b/packages/tools/src/tools/img-gen.ts
@@ -13,6 +13,13 @@ export type ImgGeneratorToolOutput = {
   errorMessage?: string;
 };
 
+export type ImgGeneratorToolParams = {
+  outputFormat?: string;
+  outputDir?: string;
+  apiKey?: string;
+  fileServerURLPrefix?: string;
+};
+
 // Constants
 const IMG_OUTPUT_FORMAT = "webp";
 const IMG_OUTPUT_DIR = "output/tools";
@@ -33,7 +40,10 @@ function checkRequiredEnvVars() {
   }
 }
 
-async function promptToImgBuffer(prompt: string): Promise<Buffer> {
+async function promptToImgBuffer(
+  prompt: string,
+  apiKey: string,
+): Promise<Buffer> {
   const form = new FormData();
   form.append("prompt", prompt);
   form.append("output_format", IMG_OUTPUT_FORMAT);
@@ -42,7 +52,7 @@ async function promptToImgBuffer(prompt: string): Promise<Buffer> {
     .post(IMG_GEN_API, {
       body: form as unknown as Buffer | Readable | string,
       headers: {
-        Authorization: `Bearer ${process.env.STABILITY_API_KEY}`,
+        Authorization: `Bearer ${apiKey}`,
         Accept: "image/*",
       },
     })
@@ -51,43 +61,75 @@ async function promptToImgBuffer(prompt: string): Promise<Buffer> {
   return buffer;
 }
 
-function saveImage(buffer: Buffer): string {
-  const filename = `${crypto.randomUUID()}.${IMG_OUTPUT_FORMAT}`;
+function saveImage(
+  buffer: Buffer,
+  options: {
+    outputFormat?: string;
+    outputDir?: string;
+    fileServerURLPrefix?: string;
+  },
+): string {
+  const {
+    outputFormat = IMG_OUTPUT_FORMAT,
+    outputDir = IMG_OUTPUT_DIR,
+    fileServerURLPrefix = process.env.FILESERVER_URL_PREFIX,
+  } = options;
+  const filename = `${crypto.randomUUID()}.${outputFormat}`;
 
   // Create output directory if it doesn't exist
-  if (!fs.existsSync(IMG_OUTPUT_DIR)) {
-    fs.mkdirSync(IMG_OUTPUT_DIR, { recursive: true });
+  if (!fs.existsSync(outputDir)) {
+    fs.mkdirSync(outputDir, { recursive: true });
   }
 
-  const outputPath = path.join(IMG_OUTPUT_DIR, filename);
+  const outputPath = path.join(outputDir, filename);
   fs.writeFileSync(outputPath, buffer);
 
-  const url = `${process.env.FILESERVER_URL_PREFIX}/${IMG_OUTPUT_DIR}/${filename}`;
+  const url = `${fileServerURLPrefix}/${outputDir}/${filename}`;
   console.log(`Saved image to ${outputPath}.\nURL: ${url}`);
 
   return url;
 }
 
-export const imageGenerator = tool({
-  name: "image_generator",
-  description: "Use this function to generate an image based on the prompt.",
-  parameters: z.object({
-    prompt: z.string().describe("The prompt to generate the image"),
-  }),
-  execute: async ({ prompt }): Promise<ImgGeneratorToolOutput> => {
-    // Check required environment variables
-    checkRequiredEnvVars();
+export const imageGenerator = (params?: ImgGeneratorToolParams) => {
+  return tool({
+    name: "image_generator",
+    description: "Use this function to generate an image based on the prompt.",
+    parameters: z.object({
+      prompt: z.string().describe("The prompt to generate the image"),
+    }),
+    execute: async ({ prompt }): Promise<ImgGeneratorToolOutput> => {
+      const outputFormat = params?.outputFormat ?? IMG_OUTPUT_FORMAT;
+      const outputDir = params?.outputDir ?? IMG_OUTPUT_DIR;
+      const apiKey = params?.apiKey ?? process.env.STABILITY_API_KEY;
+      const fileServerURLPrefix =
+        params?.fileServerURLPrefix ?? process.env.FILESERVER_URL_PREFIX;
 
-    try {
-      const buffer = await promptToImgBuffer(prompt);
-      const imageUrl = saveImage(buffer);
-      return { isSuccess: true, imageUrl };
-    } catch (error) {
-      console.error(error);
-      return {
-        isSuccess: false,
-        errorMessage: "Failed to generate image. Please try again.",
-      };
-    }
-  },
-});
+      if (!apiKey) {
+        throw new Error(
+          "STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
+        );
+      }
+      if (!fileServerURLPrefix) {
+        throw new Error(
+          "FILESERVER_URL_PREFIX is required to display file output after generation",
+        );
+      }
+
+      try {
+        const buffer = await promptToImgBuffer(prompt, apiKey);
+        const imageUrl = saveImage(buffer, {
+          outputFormat,
+          outputDir,
+          fileServerURLPrefix,
+        });
+        return { isSuccess: true, imageUrl };
+      } catch (error) {
+        console.error(error);
+        return {
+          isSuccess: false,
+          errorMessage: "Failed to generate image. Please try again.",
+        };
+      }
+    },
+  });
+};
diff --git a/packages/tools/src/tools/interpreter.ts b/packages/tools/src/tools/interpreter.ts
index 20db2df49..354683f1b 100644
--- a/packages/tools/src/tools/interpreter.ts
+++ b/packages/tools/src/tools/interpreter.ts
@@ -14,8 +14,10 @@ export type InterpreterParameter = {
 
 export type InterpreterToolParams = {
   metadata?: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
-  apiKey?: string;
-  fileServerURLPrefix?: string;
+  apiKey?: string | undefined;
+  fileServerURLPrefix?: string | undefined;
+  outputDir?: string | undefined;
+  uploadedFilesDir?: string | undefined;
 };
 
 export type InterpreterToolOutput = {
@@ -78,8 +80,8 @@ You have a maximum of 3 retries to get the code to run successfully.
 };
 
 export class InterpreterTool implements BaseTool<InterpreterParameter> {
-  private readonly outputDir = "output/tools";
-  private readonly uploadedFilesDir = "output/uploaded";
+  private outputDir: string;
+  private uploadedFilesDir: string;
   private apiKey: string;
   private fileServerURLPrefix: string;
   metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
@@ -90,6 +92,8 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
     this.apiKey = params?.apiKey || process.env.E2B_API_KEY!;
     this.fileServerURLPrefix =
       params?.fileServerURLPrefix || process.env.FILESERVER_URL_PREFIX!;
+    this.outputDir = params?.outputDir || "output/tools";
+    this.uploadedFilesDir = params?.uploadedFilesDir || "output/uploaded";
 
     if (!this.apiKey) {
       throw new Error(
@@ -248,3 +252,6 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
     return `${this.fileServerURLPrefix}/${this.outputDir}/${filename}`;
   }
 }
+
+export const interpreter = (params?: InterpreterToolParams) =>
+  new InterpreterTool(params);
diff --git a/packages/tools/src/tools/weather.ts b/packages/tools/src/tools/weather.ts
index 6f3206b12..62be492d4 100644
--- a/packages/tools/src/tools/weather.ts
+++ b/packages/tools/src/tools/weather.ts
@@ -41,36 +41,38 @@ export type WeatherToolOutput = {
   };
 };
 
-export const weather = tool({
-  name: "weather",
-  description: `
-    Use this function to get the weather of any given location.
-    Note that the weather code should follow WMO Weather interpretation codes (WW):
-    0: Clear sky
-    1, 2, 3: Mainly clear, partly cloudy, and overcast
-    45, 48: Fog and depositing rime fog
-    51, 53, 55: Drizzle: Light, moderate, and dense intensity
-    56, 57: Freezing Drizzle: Light and dense intensity
-    61, 63, 65: Rain: Slight, moderate and heavy intensity
-    66, 67: Freezing Rain: Light and heavy intensity
-    71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
-    77: Snow grains
-    80, 81, 82: Rain showers: Slight, moderate, and violent
-    85, 86: Snow showers slight and heavy
-    95: Thunderstorm: Slight or moderate
-    96, 99: Thunderstorm with slight and heavy hail
-  `,
-  parameters: z.object({
-    location: z.string().describe("The location to get the weather"),
-  }),
-  execute: async ({
-    location,
-  }: {
-    location: string;
-  }): Promise<WeatherToolOutput> => {
-    return await getWeatherByLocation(location);
-  },
-});
+export const weather = () => {
+  return tool({
+    name: "weather",
+    description: `
+      Use this function to get the weather of any given location.
+      Note that the weather code should follow WMO Weather interpretation codes (WW):
+      0: Clear sky
+      1, 2, 3: Mainly clear, partly cloudy, and overcast
+      45, 48: Fog and depositing rime fog
+      51, 53, 55: Drizzle: Light, moderate, and dense intensity
+      56, 57: Freezing Drizzle: Light and dense intensity
+      61, 63, 65: Rain: Slight, moderate and heavy intensity
+      66, 67: Freezing Rain: Light and heavy intensity
+      71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
+      77: Snow grains
+      80, 81, 82: Rain showers: Slight, moderate, and violent
+      85, 86: Snow showers slight and heavy
+      95: Thunderstorm: Slight or moderate
+      96, 99: Thunderstorm with slight and heavy hail
+    `,
+    parameters: z.object({
+      location: z.string().describe("The location to get the weather"),
+    }),
+    execute: async ({
+      location,
+    }: {
+      location: string;
+    }): Promise<WeatherToolOutput> => {
+      return await getWeatherByLocation(location);
+    },
+  });
+};
 
 async function getWeatherByLocation(location: string) {
   const { latitude, longitude } = await getGeoLocation(location);
-- 
GitLab