diff --git a/package.json b/package.json index c259d425..6e0c2c78 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "fast-xml-parser": "^4.4.1", "header-generator": "^2.1.54", "joplin-turndown-plugin-gfm": "^1.0.12", + "mathjs": "^13.1.1", "mustache": "^4.2.0", "object-hash": "^3.0.0", "p-queue": "^8.0.1", diff --git a/src/tools/calculator.test.ts b/src/tools/calculator.test.ts new file mode 100644 index 00000000..723040d7 --- /dev/null +++ b/src/tools/calculator.test.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2024 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CalculatorTool } from "@/tools/calculator.js"; +import { beforeEach, expect } from "vitest"; + +describe("Calculator", () => { + let instance: CalculatorTool; + + beforeEach(() => { + instance = new CalculatorTool(); + }); + + it("Runs", async () => { + const x1 = 1; + const y1 = 1; + const x2 = 4; + const y2 = 5; + + const response = await instance.run({ + expression: `sqrt( (${x2}-${x1})^2 + (${y2}-${y1})^2 )`, + }); + expect(response.result).toBe(5); + }); + + it("Throws", async () => { + await expect( + instance.run({ + expression: "import", + }), + ).rejects.toThrowError(); + }); +}); diff --git a/src/tools/calculator.ts b/src/tools/calculator.ts new file mode 100644 index 00000000..a205c1d2 --- /dev/null +++ b/src/tools/calculator.ts @@ -0,0 +1,97 @@ +/** + * Copyright 2024 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { StringToolOutput, Tool, ToolInput } from "@/tools/base.js"; +import { z } from "zod"; +import { create, all, evaluate, ImportOptions, ImportObject, ConfigOptions } from "mathjs"; + +export interface CalculatorToolInput { + config?: ConfigOptions; + imports?: { + entries: ImportObject | ImportObject[]; + options?: ImportOptions; + }; +} + +/** + * Waring: The CalculatorTool enbales the agent (and by proxy the user) to execute arbirtary + * expressions via mathjs. + * + * Please consider the security and stability risks documented at + * https://mathjs.org/docs/expressions/security.html before using this tool. + */ +export class CalculatorTool extends Tool { + name = "Calculator"; + description = `A calculator tool that performs basic arithmetic operations like addition, subtraction, multiplication, and division. +Only use the calculator tool if you need to perform a calculation.`; + + inputSchema() { + return z.object({ + expression: z + .string() + .min(1) + .describe( + `The mathematical expression to evaluate (e.g., "2 + 3 * 4"). Use Mathjs basic expression syntax. Constants only.`, + ), + }); + } + + protected limitedEvaluate: typeof evaluate; + + constructor({ config, imports, ...options }: CalculatorToolInput = {}) { + super(options); + const math = create(all, config); + this.limitedEvaluate = math.evaluate; + // Disable use of potentially vulnerable functions + math.import( + { + // most important (hardly any functional impact) + import: function () { + throw new Error("Function import is disabled"); + }, + createUnit: function () { + throw new Error("Function createUnit is disabled"); + }, + reviver: function () { + throw new Error("Function reviver is disabled"); + }, + + // extra (has functional impact) + evaluate: function () { + throw new Error("Function evaluate is disabled"); + }, + parse: function () { + throw new Error("Function parse is disabled"); + }, + simplify: function () { + throw new Error("Function simplify is disabled"); + }, + derivative: function () { + throw new Error("Function derivative is disabled"); + }, + resolve: function () { + throw new Error("Function resolve is disabled"); + }, + }, + { override: true, ...imports?.options }, + ); + } + + protected async _run({ expression }: ToolInput) { + const result = this.limitedEvaluate(expression); + return new StringToolOutput(result); + } +} diff --git a/yarn.lock b/yarn.lock index d46cfe81..a8746c14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.25.4": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/d6143adf5aa1ce79ed374e33fdfd74fa975055a80bc6e479672ab1eadc4e4bfd7484444e17dd063a1d180e051f3ec62b357c7a2b817e7657687b47313158c3d2 + languageName: node + linkType: hard + "@bufbuild/protobuf@npm:^1.10.0": version: 1.10.0 resolution: "@bufbuild/protobuf@npm:1.10.0" @@ -2539,6 +2548,7 @@ __metadata: joplin-turndown-plugin-gfm: "npm:^1.0.12" langchain: "npm:~0.2.16" lint-staged: "npm:^15.2.9" + mathjs: "npm:^13.1.1" mustache: "npm:^4.2.0" object-hash: "npm:^3.0.0" ollama: "npm:^0.5.8" @@ -3096,6 +3106,13 @@ __metadata: languageName: node linkType: hard +"complex.js@npm:^2.1.1": + version: 2.1.1 + resolution: "complex.js@npm:2.1.1" + checksum: 10c0/c5dbb83954b472cf7fb1aebf8d140a7eb85a00b73eeb2b4ee4d192f98f038f3800f54550747b85be1114fedcbefebb2ade709b17b3cd40ab2bd546cb8fe9121c + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -3417,6 +3434,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee + languageName: node + linkType: hard + "decode-named-character-reference@npm:^1.0.0": version: 1.0.2 resolution: "decode-named-character-reference@npm:1.0.2" @@ -3852,6 +3876,13 @@ __metadata: languageName: node linkType: hard +"escape-latex@npm:^1.2.0": + version: 1.2.0 + resolution: "escape-latex@npm:1.2.0" + checksum: 10c0/b77ea1594a38625295793a61105222c283c1792d1b2511bbfd6338cf02cc427dcabce7e7c1e22ec2f5c40baf3eaf2eeaf229a62dbbb74c6e69bb4a4209f2544f + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -4382,6 +4413,13 @@ __metadata: languageName: node linkType: hard +"fraction.js@npm:^4.3.7": + version: 4.3.7 + resolution: "fraction.js@npm:4.3.7" + checksum: 10c0/df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711 + languageName: node + linkType: hard + "fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -5316,6 +5354,13 @@ __metadata: languageName: node linkType: hard +"javascript-natural-sort@npm:^0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 10c0/340f8ffc5d30fb516e06dc540e8fa9e0b93c865cf49d791fed3eac3bdc5fc71f0066fc81d44ec1433edc87caecaf9f13eec4a1fce8c5beafc709a71eaedae6fe + languageName: node + linkType: hard + "jiti@npm:^1.19.1": version: 1.21.6 resolution: "jiti@npm:1.21.6" @@ -6042,6 +6087,25 @@ __metadata: languageName: node linkType: hard +"mathjs@npm:^13.1.1": + version: 13.1.1 + resolution: "mathjs@npm:13.1.1" + dependencies: + "@babel/runtime": "npm:^7.25.4" + complex.js: "npm:^2.1.1" + decimal.js: "npm:^10.4.3" + escape-latex: "npm:^1.2.0" + fraction.js: "npm:^4.3.7" + javascript-natural-sort: "npm:^0.7.1" + seedrandom: "npm:^3.0.5" + tiny-emitter: "npm:^2.1.0" + typed-function: "npm:^4.2.1" + bin: + mathjs: bin/cli.js + checksum: 10c0/db0b9d822fed889fc5f2cf4a9875e3fa627a26c9f6b2efbe7e000e42bfe9c0d71c125124329d2c8fd27a9d22d08b8794681367744787ef1eb8465e8f95955fd3 + languageName: node + linkType: hard + "mdast-util-find-and-replace@npm:^3.0.0": version: 3.0.1 resolution: "mdast-util-find-and-replace@npm:3.0.1" @@ -7920,6 +7984,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 + languageName: node + linkType: hard + "registry-auth-token@npm:^5.0.2": version: 5.0.2 resolution: "registry-auth-token@npm:5.0.2" @@ -8260,6 +8331,13 @@ __metadata: languageName: node linkType: hard +"seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "seedrandom@npm:3.0.5" + checksum: 10c0/929752ac098ff4990b3f8e0ac39136534916e72879d6eb625230141d20db26e2f44c4d03d153d457682e8cbaab0fb7d58a1e7267a157cf23fd8cf34e25044e88 + languageName: node + linkType: hard + "semver-diff@npm:^4.0.0": version: 4.0.0 resolution: "semver-diff@npm:4.0.0" @@ -8792,6 +8870,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: 10c0/459c0bd6e636e80909898220eb390e1cba2b15c430b7b06cec6ac29d87acd29ef618b9b32532283af749f5d37af3534d0e3bde29fdf6bcefbf122784333c953d + languageName: node + linkType: hard + "tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -9028,6 +9113,13 @@ __metadata: languageName: node linkType: hard +"typed-function@npm:^4.2.1": + version: 4.2.1 + resolution: "typed-function@npm:4.2.1" + checksum: 10c0/0b4e9a359e456f7df50f3d7cc53e2408d23a516f27b50c2c0654f388110ecf407c0595b1bf2296d3d8667fae6aae311ec2af90c602385777d12fe724bae99156 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5"