diff --git a/control-plane/package-lock.json b/control-plane/package-lock.json index 6331f2c2..428c9d3b 100644 --- a/control-plane/package-lock.json +++ b/control-plane/package-lock.json @@ -41,7 +41,6 @@ "jsonschema": "^1.4.1", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "langchain": "^0.2.17", "langfuse": "^3.31.0", "lodash": "^4.17.21", "node-cache": "^5.1.2", @@ -7755,35 +7754,6 @@ } } }, - "node_modules/@langchain/openai": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.2.11.tgz", - "integrity": "sha512-Pu8+WfJojCgSf0bAsXb4AjqvcDyAWyoEB1AoCRNACgEnBWZuitz3hLwCo9I+6hAbeg3QJ37g82yKcmvKAg1feg==", - "license": "MIT", - "dependencies": { - "@langchain/core": ">=0.2.26 <0.3.0", - "js-tiktoken": "^1.0.12", - "openai": "^4.57.3", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@langchain/textsplitters": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.0.3.tgz", - "integrity": "sha512-cXWgKE3sdWLSqAa8ykbCcUsUF1Kyr5J3HOWYGuobhPEycXW4WI++d5DhzdpL238mzoEXTi90VqfSCra37l5YqA==", - "license": "MIT", - "dependencies": { - "@langchain/core": ">0.2.0 <0.3.0", - "js-tiktoken": "^1.0.12" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13845,18 +13815,6 @@ "node": "*" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -16564,7 +16522,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -17664,15 +17622,6 @@ "underscore": "1.12.1" } }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jsonschema": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", @@ -17804,266 +17753,6 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, - "node_modules/langchain": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.2.20.tgz", - "integrity": "sha512-tbels6Rr524iMM3VOQ4aTGnEOOjAA1BQuBR8u/8gJ2yT48lMtIQRAN32Y4KVjKK+hEWxHHlmLBrtgLpTphFjNA==", - "license": "MIT", - "dependencies": { - "@langchain/core": ">=0.2.21 <0.3.0", - "@langchain/openai": ">=0.1.0 <0.3.0", - "@langchain/textsplitters": "~0.0.0", - "binary-extensions": "^2.2.0", - "js-tiktoken": "^1.0.12", - "js-yaml": "^4.1.0", - "jsonpointer": "^5.0.1", - "langsmith": "^0.1.56-rc.1", - "openapi-types": "^12.1.3", - "p-retry": "4", - "uuid": "^10.0.0", - "yaml": "^2.2.1", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@aws-sdk/client-s3": "*", - "@aws-sdk/client-sagemaker-runtime": "*", - "@aws-sdk/client-sfn": "*", - "@aws-sdk/credential-provider-node": "*", - "@azure/storage-blob": "*", - "@browserbasehq/sdk": "*", - "@gomomento/sdk": "*", - "@gomomento/sdk-core": "*", - "@gomomento/sdk-web": "^1.51.1", - "@langchain/anthropic": "*", - "@langchain/aws": "*", - "@langchain/cohere": "*", - "@langchain/google-genai": "*", - "@langchain/google-vertexai": "*", - "@langchain/groq": "*", - "@langchain/mistralai": "*", - "@langchain/ollama": "*", - "@mendable/firecrawl-js": "*", - "@notionhq/client": "*", - "@pinecone-database/pinecone": "*", - "@supabase/supabase-js": "*", - "@vercel/kv": "*", - "@xata.io/client": "*", - "apify-client": "*", - "assemblyai": "*", - "axios": "*", - "cheerio": "*", - "chromadb": "*", - "convex": "*", - "couchbase": "*", - "d3-dsv": "*", - "epub2": "*", - "fast-xml-parser": "*", - "handlebars": "^4.7.8", - "html-to-text": "*", - "ignore": "*", - "ioredis": "*", - "jsdom": "*", - "mammoth": "*", - "mongodb": "*", - "node-llama-cpp": "*", - "notion-to-md": "*", - "officeparser": "*", - "pdf-parse": "*", - "peggy": "^3.0.2", - "playwright": "*", - "puppeteer": "*", - "pyodide": ">=0.24.1 <0.27.0", - "redis": "*", - "sonix-speech-recognition": "*", - "srt-parser-2": "*", - "typeorm": "*", - "weaviate-ts-client": "*", - "web-auth-library": "*", - "ws": "*", - "youtube-transcript": "*", - "youtubei.js": "*" - }, - "peerDependenciesMeta": { - "@aws-sdk/client-s3": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@browserbasehq/sdk": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { - "optional": true - }, - "@gomomento/sdk-web": { - "optional": true - }, - "@langchain/anthropic": { - "optional": true - }, - "@langchain/aws": { - "optional": true - }, - "@langchain/cohere": { - "optional": true - }, - "@langchain/google-genai": { - "optional": true - }, - "@langchain/google-vertexai": { - "optional": true - }, - "@langchain/groq": { - "optional": true - }, - "@langchain/mistralai": { - "optional": true - }, - "@langchain/ollama": { - "optional": true - }, - "@mendable/firecrawl-js": { - "optional": true - }, - "@notionhq/client": { - "optional": true - }, - "@pinecone-database/pinecone": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "apify-client": { - "optional": true - }, - "assemblyai": { - "optional": true - }, - "axios": { - "optional": true - }, - "cheerio": { - "optional": true - }, - "chromadb": { - "optional": true - }, - "convex": { - "optional": true - }, - "couchbase": { - "optional": true - }, - "d3-dsv": { - "optional": true - }, - "epub2": { - "optional": true - }, - "faiss-node": { - "optional": true - }, - "fast-xml-parser": { - "optional": true - }, - "handlebars": { - "optional": true - }, - "html-to-text": { - "optional": true - }, - "ignore": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "mammoth": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "node-llama-cpp": { - "optional": true - }, - "notion-to-md": { - "optional": true - }, - "officeparser": { - "optional": true - }, - "pdf-parse": { - "optional": true - }, - "peggy": { - "optional": true - }, - "playwright": { - "optional": true - }, - "puppeteer": { - "optional": true - }, - "pyodide": { - "optional": true - }, - "redis": { - "optional": true - }, - "sonix-speech-recognition": { - "optional": true - }, - "srt-parser-2": { - "optional": true - }, - "typeorm": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "ws": { - "optional": true - }, - "youtube-transcript": { - "optional": true - }, - "youtubei.js": { - "optional": true - } - } - }, "node_modules/langfuse": { "version": "3.31.0", "resolved": "https://registry.npmjs.org/langfuse/-/langfuse-3.31.0.tgz", @@ -18845,12 +18534,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT" - }, "node_modules/openapi-typescript": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-5.4.2.tgz", @@ -21911,18 +21594,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, - "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/control-plane/package.json b/control-plane/package.json index 16929b22..59aa6d90 100644 --- a/control-plane/package.json +++ b/control-plane/package.json @@ -51,7 +51,6 @@ "jsonschema": "^1.4.1", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "langchain": "^0.2.17", "langfuse": "^3.31.0", "lodash": "^4.17.21", "node-cache": "^5.1.2", diff --git a/control-plane/src/modules/workflows/agent/agent.ai.test.ts b/control-plane/src/modules/workflows/agent/agent.ai.test.ts index 048cbd87..01c70920 100644 --- a/control-plane/src/modules/workflows/agent/agent.ai.test.ts +++ b/control-plane/src/modules/workflows/agent/agent.ai.test.ts @@ -1,8 +1,8 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { createWorkflowAgent } from "./agent"; import { z } from "zod"; import { assertResultMessage } from "../workflow-messages"; import { redisClient } from "../../redis"; +import { AgentTool } from "./tool"; if (process.env.CI) { jest.retryTimes(3); @@ -20,7 +20,7 @@ describe("Agent", () => { }; const tools = [ - new DynamicStructuredTool({ + new AgentTool({ name: "echo", description: "Echoes the input", schema: z.object({ @@ -349,7 +349,7 @@ describe("Agent", () => { it("should respect mock responses", async () => { const tools = [ - new DynamicStructuredTool({ + new AgentTool({ name: "searchHaystack", description: "Search haystack", schema: z.object({ diff --git a/control-plane/src/modules/workflows/agent/agent.ts b/control-plane/src/modules/workflows/agent/agent.ts index cdb126fc..4dbbd22b 100644 --- a/control-plane/src/modules/workflows/agent/agent.ts +++ b/control-plane/src/modules/workflows/agent/agent.ts @@ -1,4 +1,3 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { START, StateGraph } from "@langchain/langgraph"; import { type Run } from "../workflows"; import { MODEL_CALL_NODE_NAME, handleModelCall } from "./nodes/model-call"; @@ -12,14 +11,15 @@ import { } from "./nodes/edges"; import { AgentMessage } from "../workflow-messages"; import { buildMockModel, buildModel } from "../../models"; +import { AgentTool } from "./tool"; export type ReleventToolLookup = ( state: WorkflowAgentState, -) => Promise; +) => Promise; export type ToolFetcher = ( toolCall: Required["invocations"][number], -) => Promise; +) => Promise; export const createWorkflowAgent = async ({ workflow, diff --git a/control-plane/src/modules/workflows/agent/nodes/model-call.test.ts b/control-plane/src/modules/workflows/agent/nodes/model-call.test.ts index f7297ee5..23ac0202 100644 --- a/control-plane/src/modules/workflows/agent/nodes/model-call.test.ts +++ b/control-plane/src/modules/workflows/agent/nodes/model-call.test.ts @@ -1,4 +1,3 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { ReleventToolLookup } from "../agent"; import { handleModelCall } from "./model-call"; import { z } from "zod"; @@ -9,6 +8,7 @@ import { assertGenericMessage, } from "../../workflow-messages"; import { Model } from "../../../models"; +import { AgentTool } from "../tool"; describe("handleModelCall", () => { const workflow = { @@ -49,13 +49,13 @@ describe("handleModelCall", () => { const findRelevantTools: ReleventToolLookup = async () => { return [ // eslint-disable-next-line @typescript-eslint/no-explicit-any - new DynamicStructuredTool({ + new AgentTool({ name: "testTool", description: "A test tool", func: functionHandler, schema: z.object({}), }), - new DynamicStructuredTool({ + new AgentTool({ name: "notify", description: "Send a message", func: functionHandler, diff --git a/control-plane/src/modules/workflows/agent/nodes/model-call.ts b/control-plane/src/modules/workflows/agent/nodes/model-call.ts index cc8c1672..f0d799dd 100644 --- a/control-plane/src/modules/workflows/agent/nodes/model-call.ts +++ b/control-plane/src/modules/workflows/agent/nodes/model-call.ts @@ -13,7 +13,6 @@ import { ulid } from "ulid"; import { deserializeFunctionSchema } from "../../../service-definitions"; import { validateFunctionSchema } from "inferable"; import { JsonSchemaInput } from "inferable/bin/types"; -import { toolSchema } from "./tool-parser"; import { Model } from "../../../models"; import { ToolUseBlock } from "@anthropic-ai/sdk/resources"; @@ -114,7 +113,9 @@ const _handleModelCall = async ( }) .strict(); - const schemaString = toolSchema(relevantSchemas).join("\n"); + const schemaString = relevantSchemas.map((tool) => { + return `${tool.name} - ${tool.description} ${tool.schema}`; + }); const systemPrompt = [ "You are a helpful assistant with access to a set of tools designed to assist in completing tasks.", diff --git a/control-plane/src/modules/workflows/agent/nodes/tool-call.test.ts b/control-plane/src/modules/workflows/agent/nodes/tool-call.test.ts index d1d197a4..b1f38a5f 100644 --- a/control-plane/src/modules/workflows/agent/nodes/tool-call.test.ts +++ b/control-plane/src/modules/workflows/agent/nodes/tool-call.test.ts @@ -1,5 +1,4 @@ import { handleToolCalls } from "./tool-call"; -import { DynamicStructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { SpecialResultTypes } from "../tools/functions"; import { NotFoundError } from "../../../../utilities/errors"; @@ -7,6 +6,7 @@ import { ulid } from "ulid"; import { WorkflowAgentState } from "../state"; import { assertResultMessage } from "../../workflow-messages"; import { redisClient } from "../../../redis"; +import { AgentTool } from "../tool"; describe("handleToolCalls", () => { const workflow = { @@ -17,7 +17,7 @@ describe("handleToolCalls", () => { const toolHandler = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tool = new DynamicStructuredTool({ + const tool = new AgentTool({ description: "Echoes the input", func: toolHandler, name: "console_echo", @@ -181,20 +181,12 @@ describe("handleToolCalls", () => { message: expect.stringContaining( `Provided input did not match schema for ${tool.name}`, ), - parseResult: expect.objectContaining({ - success: false, - error: expect.objectContaining({ - issues: expect.arrayContaining([ - expect.objectContaining({ - code: "invalid_type", - expected: "string", - received: "undefined", - path: ["input"], - message: "Required", - }), - ]), - }), - }), + parseResult: expect.arrayContaining([ + expect.objectContaining({ + message: "is not allowed to have the additional property \"wrongKey\"" + } + ) + ]), }), ); }); diff --git a/control-plane/src/modules/workflows/agent/nodes/tool-call.ts b/control-plane/src/modules/workflows/agent/nodes/tool-call.ts index 69db84d2..c1bfd11b 100644 --- a/control-plane/src/modules/workflows/agent/nodes/tool-call.ts +++ b/control-plane/src/modules/workflows/agent/nodes/tool-call.ts @@ -1,9 +1,3 @@ -import { - DynamicStructuredTool, - ToolInputParsingException, -} from "@langchain/core/tools"; -import { ToolExecutor } from "@langchain/langgraph/prebuilt"; -import { AgentAction } from "langchain/agents"; import { ulid } from "ulid"; import { AgentError, @@ -18,6 +12,7 @@ import { Run } from "../../workflows"; import { ToolFetcher } from "../agent"; import { WorkflowAgentState } from "../state"; import { SpecialResultTypes, parseFunctionResponse } from "../tools/functions"; +import { AgentTool, AgentToolInputError } from "../tool"; export const TOOL_CALL_NODE_NAME = "action"; @@ -125,7 +120,7 @@ const _handleToolCall = async ( ): Promise> => { logger.info("Executing tool call"); - let tool: DynamicStructuredTool | undefined; + let tool: AgentTool | undefined; const toolName = toolCall.toolName; const toolInput = toolCall.input; @@ -177,14 +172,6 @@ const _handleToolCall = async ( }; } - const executor = new ToolExecutor({ tools: [tool] }); - - const action: AgentAction = { - tool: toolName, - toolInput: toolInput, - log: `Invoking ${toolName} with input: ${toolInput}`, - }; - events.write({ type: "callingFunction", clusterId: workflow.clusterId, @@ -196,7 +183,7 @@ const _handleToolCall = async ( }); try { - const rawResponse = await executor.invoke(action); + const rawResponse = await tool.execute(toolInput); if (!rawResponse) { throw new AgentError("Received empty response from tool executor"); } @@ -289,7 +276,7 @@ const _handleToolCall = async ( throw new AgentError("Unknown result type encountered"); } catch (error) { - if (error instanceof ToolInputParsingException) { + if (error instanceof AgentToolInputError) { events.write({ type: "functionErrored", clusterId: workflow.clusterId, @@ -304,25 +291,12 @@ const _handleToolCall = async ( toolName, }); - const parseResult = tool.schema.safeParse(action.toolInput); - - if (parseResult.success) { - logger.warn( - "Tool invocation failed with ToolInputParsingException, but the input was parsed successfully", - { - toolName, - toolCallId, - }, - ); - } - trackCustomerTelemetry({ type: "toolCall", toolName, clusterId: workflow.clusterId, runId: workflow.id, input: toolInput, - output: parseResult, startedAt, completedAt: Date.now(), level: "ERROR", @@ -336,7 +310,7 @@ const _handleToolCall = async ( data: { result: { message: `Provided input did not match schema for ${toolName}, check your input`, - parseResult, + parseResult: error.validatorResult.errors }, id: toolCallId, }, diff --git a/control-plane/src/modules/workflows/agent/nodes/tool-parser.test.ts b/control-plane/src/modules/workflows/agent/nodes/tool-parser.test.ts index f9e4216d..a9d420be 100644 --- a/control-plane/src/modules/workflows/agent/nodes/tool-parser.test.ts +++ b/control-plane/src/modules/workflows/agent/nodes/tool-parser.test.ts @@ -1,61 +1,4 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; -import { z } from "zod"; -import { mostRelevantKMeansCluster, toolSchema } from "./tool-parser"; - -const sampleTools = [ - new DynamicStructuredTool({ - name: "inputRequest", - description: - "Asks the user to provide additional input via an input dialog.", - schema: z.object({ - inputs: z - .array( - z.object({ - name: z.string(), - default: z.string().optional(), - description: z.string().optional(), - options: z.array(z.string()).optional(), - }), - ) - .describe("The requested details."), - }), - func: async () => {}, - }), -]; - -describe("toolContext", () => { - it("should return the tool context", () => { - const jsonSchema = { - type: "object", - properties: { - inputs: { - type: "array", - items: { - type: "object", - properties: { - name: { type: "string" }, - default: { type: "string" }, - description: { type: "string" }, - options: { type: "array", items: { type: "string" } }, - }, - required: ["name"], - additionalProperties: false, - }, - description: "The requested details.", - }, - }, - required: ["inputs"], - additionalProperties: false, - }; - - const context = toolSchema(sampleTools); - expect(context).toEqual([ - `inputRequest - Asks the user to provide additional input via an input dialog. ${JSON.stringify( - jsonSchema, - )}`, - ]); - }); -}); +import { mostRelevantKMeansCluster } from "./tool-parser"; describe("mostRelevantKMeansCluster", () => { it("should return the most relevant k-means cluster", () => { diff --git a/control-plane/src/modules/workflows/agent/nodes/tool-parser.ts b/control-plane/src/modules/workflows/agent/nodes/tool-parser.ts index cbbc7781..554973ec 100644 --- a/control-plane/src/modules/workflows/agent/nodes/tool-parser.ts +++ b/control-plane/src/modules/workflows/agent/nodes/tool-parser.ts @@ -1,17 +1,5 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; -import { zodToJsonSchema } from "zod-to-json-schema"; import skmeans from "skmeans"; -export function toolSchema(tools: DynamicStructuredTool[]) { - return tools.map((tool) => { - const jsonSchema = zodToJsonSchema(tool.schema); - - delete jsonSchema.$schema; - - return `${tool.name} - ${tool.description} ${JSON.stringify(jsonSchema)}`; - }); -} - export function mostRelevantKMeansCluster( tools: T[], ): T[] { diff --git a/control-plane/src/modules/workflows/agent/run.test.ts b/control-plane/src/modules/workflows/agent/run.test.ts index 64574ba4..6fcc53b3 100644 --- a/control-plane/src/modules/workflows/agent/run.test.ts +++ b/control-plane/src/modules/workflows/agent/run.test.ts @@ -129,7 +129,8 @@ describe("buildMockTools", () => { expect(Object.keys(tools)).toEqual(["testService_someFunction"]); const result = await tools["testService_someFunction"].func({ test: "" }); - expect(JSON.parse(result)).toEqual({ + expect(result).toBeDefined(); + expect(JSON.parse(result!)).toEqual({ result: { foo: "bar", }, diff --git a/control-plane/src/modules/workflows/agent/run.ts b/control-plane/src/modules/workflows/agent/run.ts index f2ee89bf..76764609 100644 --- a/control-plane/src/modules/workflows/agent/run.ts +++ b/control-plane/src/modules/workflows/agent/run.ts @@ -1,4 +1,3 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { NotFoundError } from "../../../utilities/errors"; import { getClusterContextText } from "../../cluster"; @@ -32,6 +31,7 @@ import { buildCurrentDateTimeTool } from "./tools/date-time"; import { CURRENT_DATE_TIME_TOOL_NAME } from "./tools/date-time"; import { env } from "../../../utilities/env"; import { events } from "../../observability/events"; +import { AgentTool } from "./tool"; /** * Run a workflow from the most recent saved state @@ -82,7 +82,7 @@ export const processRun = async ( }), ); - const mockToolsMap: Record = + const mockToolsMap: Record = await buildMockTools(run); let mockModelResponses; @@ -347,7 +347,7 @@ export const findRelevantTools = async (state: WorkflowAgentState) => { const start = Date.now(); const workflow = state.workflow; - const tools: DynamicStructuredTool[] = []; + const tools: AgentTool[] = []; const attachedFunctions = workflow.attachedFunctions ?? []; // If functions are explicitly attached, skip relevant tools search @@ -415,7 +415,7 @@ export const findRelevantTools = async (state: WorkflowAgentState) => { }; export const buildMockTools = async (workflow: Run) => { - const mocks: Record = {}; + const mocks: Record = {}; if (!workflow.testMocks || Object.keys(workflow.testMocks).length === 0) { return mocks; } diff --git a/control-plane/src/modules/workflows/agent/tool.ts b/control-plane/src/modules/workflows/agent/tool.ts new file mode 100644 index 00000000..3fc3dd06 --- /dev/null +++ b/control-plane/src/modules/workflows/agent/tool.ts @@ -0,0 +1,57 @@ +import { Validator, ValidatorResult } from "jsonschema"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from "zod"; + +const validator = new Validator(); + +export class AgentToolInputError extends Error { + validatorResult: ValidatorResult; + + constructor(validatorResult: ValidatorResult) { + super(validatorResult.errors.map((e) => e.stack).join("\n")); + this.validatorResult = validatorResult; + } +} + +export class AgentTool { + name: string + description: string + func: (input: unknown) => Promise + schema?: string; + + constructor({ + name, + description, + func, + schema, + }: { + name: string, + description: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + func: (input: any) => Promise, + schema?: string | z.ZodObject; + }) { + if (!!schema && typeof schema !== "string") { + schema = JSON.stringify(zodToJsonSchema(schema)); + } + + this.name = name + this.description = description + this.func = func + this.schema = schema + } + + private validate(input: unknown): ValidatorResult { + const result = validator.validate(input, JSON.parse(this.schema ?? "{}")); + return result + } + + public execute(input: unknown): Promise { + const result = this.validate(input); + if (result.valid) { + return this.func(input); + } else { + throw new AgentToolInputError(result); + } + } +} diff --git a/control-plane/src/modules/workflows/agent/tools/cluster-internal-tools.ts b/control-plane/src/modules/workflows/agent/tools/cluster-internal-tools.ts index fb24ce46..b9314d83 100644 --- a/control-plane/src/modules/workflows/agent/tools/cluster-internal-tools.ts +++ b/control-plane/src/modules/workflows/agent/tools/cluster-internal-tools.ts @@ -1,4 +1,3 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { Run } from "../../workflows"; import { buildAccessKnowledgeArtifacts, @@ -10,6 +9,7 @@ import { buildCurrentDateTimeTool, CURRENT_DATE_TIME_TOOL_NAME, } from "./date-time"; +import { AgentTool } from "../tool"; const clusterSettingsCache = createCache<{ enableKnowledgebase: boolean; @@ -20,7 +20,7 @@ const CACHE_TTL = 60 * 2; // 2 minutes export type InternalToolBuilder = ( workflow: Run, toolCallId: string -) => DynamicStructuredTool | Promise; +) => AgentTool | Promise; export const getClusterInternalTools = async ( clusterId: string diff --git a/control-plane/src/modules/workflows/agent/tools/date-time.ts b/control-plane/src/modules/workflows/agent/tools/date-time.ts index eaeda49f..e377ef15 100644 --- a/control-plane/src/modules/workflows/agent/tools/date-time.ts +++ b/control-plane/src/modules/workflows/agent/tools/date-time.ts @@ -1,10 +1,10 @@ import { z } from "zod"; -import { DynamicStructuredTool } from "@langchain/core/tools"; +import { AgentTool } from "../tool"; export const CURRENT_DATE_TIME_TOOL_NAME = "currentDateTime"; -export const buildCurrentDateTimeTool = (): DynamicStructuredTool => - new DynamicStructuredTool({ +export const buildCurrentDateTimeTool = (): AgentTool => + new AgentTool({ name: CURRENT_DATE_TIME_TOOL_NAME, description: "Retrieves the current date and time in ISO 8601 format.", schema: z.object({}), diff --git a/control-plane/src/modules/workflows/agent/tools/functions.ts b/control-plane/src/modules/workflows/agent/tools/functions.ts index 7c6fdcf7..4723fec2 100644 --- a/control-plane/src/modules/workflows/agent/tools/functions.ts +++ b/control-plane/src/modules/workflows/agent/tools/functions.ts @@ -1,4 +1,3 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import assert from "assert"; import { z } from "zod"; import { AgentError, JobPollTimeoutError } from "../../../../utilities/errors"; @@ -6,11 +5,11 @@ import * as jobs from "../../../jobs/jobs"; import { logger } from "../../../observability/logger"; import { packer } from "../../../packer"; import { - deserializeFunctionSchema, getServiceDefinition, serviceFunctionEmbeddingId, } from "../../../service-definitions"; import { summariseJobResultIfNecessary } from "../summarizer"; +import { AgentTool } from "../tool"; export const SpecialResultTypes = { jobTimeout: "inferableJobTimeout", @@ -30,31 +29,17 @@ export const buildAbstractServiceFunctionTool = ({ functionName: string; serviceName: string; description?: string; - schema: unknown; -}): DynamicStructuredTool => { + schema?: string; +}): AgentTool => { const toolName = serviceFunctionEmbeddingId({ serviceName, functionName }); - let deserialized = null; - - try { - deserialized = deserializeFunctionSchema(schema); - } catch (e) { - logger.error( - `Failed to deserialize schema for ${toolName} (${serviceName}.${functionName})`, - { schema, error: e }, - ); - throw new AgentError( - `Failed to deserialize schema for ${toolName} (${serviceName}.${functionName})`, - ); - } - - return new DynamicStructuredTool({ + return new AgentTool({ name: toolName, description: ( description ?? `${serviceName}-${functionName} function` ).substring(0, 1024), - schema: deserialized, - func: async () => {}, + schema, + func: async () => undefined, }); }; @@ -75,7 +60,7 @@ export const buildServiceFunctionTool = ({ toolCallId: string; description?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - schema: any; + schema?: string; workflow: { clusterId: string; id: string; @@ -83,7 +68,7 @@ export const buildServiceFunctionTool = ({ authContext?: unknown; context?: unknown; }; -}): DynamicStructuredTool => { +}): AgentTool => { const tool = buildAbstractServiceFunctionTool({ functionName, serviceName, diff --git a/control-plane/src/modules/workflows/agent/tools/knowledge-artifacts.ts b/control-plane/src/modules/workflows/agent/tools/knowledge-artifacts.ts index c5e634d2..4616f6c7 100644 --- a/control-plane/src/modules/workflows/agent/tools/knowledge-artifacts.ts +++ b/control-plane/src/modules/workflows/agent/tools/knowledge-artifacts.ts @@ -1,24 +1,22 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { DynamicStructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { logger } from "../../../observability/logger"; import { getKnowledge } from "../../../knowledge/knowledgebase"; import { Run } from "../../workflows"; import * as events from "../../../observability/events"; import { getAllUniqueTags } from "../../../embeddings/embeddings"; +import { AgentTool } from "../tool"; export const ACCESS_KNOWLEDGE_ARTIFACTS_TOOL_NAME = "accessKnowledgeArtifacts"; export const buildAccessKnowledgeArtifacts = async ( workflow: Run, -): Promise> => { +): Promise => { const tags = await getAllUniqueTags( workflow.clusterId, "knowledgebase-artifact", ); - return new DynamicStructuredTool({ + return new AgentTool({ name: ACCESS_KNOWLEDGE_ARTIFACTS_TOOL_NAME, description: "Retrieves relevant knowledge artifacts based on a given query.", @@ -44,7 +42,7 @@ export const buildAccessKnowledgeArtifacts = async ( tag: input.tag, }); - await events.write({ + events.write({ type: "knowledgeArtifactsAccessed", clusterId: workflow.clusterId, workflowId: workflow.id, diff --git a/control-plane/src/modules/workflows/agent/tools/mock-function.ts b/control-plane/src/modules/workflows/agent/tools/mock-function.ts index 34965958..09efb073 100644 --- a/control-plane/src/modules/workflows/agent/tools/mock-function.ts +++ b/control-plane/src/modules/workflows/agent/tools/mock-function.ts @@ -1,10 +1,10 @@ -import { DynamicStructuredTool } from "@langchain/core/tools"; import { AgentError } from "../../../../utilities/errors"; import { logger } from "../../../observability/logger"; import { deserializeFunctionSchema, serviceFunctionEmbeddingId, } from "../../../service-definitions"; +import { AgentTool } from "../tool"; /** * Build a tool from a service function with a handler that immediately returns a mock result @@ -22,7 +22,7 @@ export const buildMockFunctionTool = ({ description?: string; schema: unknown; mockResult: unknown; -}): DynamicStructuredTool => { +}): AgentTool => { const toolName = serviceFunctionEmbeddingId({ serviceName, functionName }); let deserialized = null; @@ -39,7 +39,7 @@ export const buildMockFunctionTool = ({ ); } - return new DynamicStructuredTool({ + return new AgentTool({ name: toolName, description: ( description ?? `${serviceName}-${functionName} function` diff --git a/control-plane/src/modules/workflows/agent/utils.ts b/control-plane/src/modules/workflows/agent/utils.ts index c0a013f6..ef113d1f 100644 --- a/control-plane/src/modules/workflows/agent/utils.ts +++ b/control-plane/src/modules/workflows/agent/utils.ts @@ -1,39 +1,7 @@ import { getEncoding } from "js-tiktoken"; -import { - AIMessage, - AIMessageChunk, - BaseMessage, - ToolMessage, -} from "@langchain/core/messages"; //https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb // gpt-4, gpt-3.5-turbo, text-embedding-ada-002, text-embedding-3-small, text-embedding-3-large export const estimateTokenCount = async (input?: string) => getEncoding("cl100k_base").encode(input ?? "").length; -export const isFunctionResult = (message?: BaseMessage) => { - return message instanceof ToolMessage; -}; - -export const isFunctionCall = (message?: BaseMessage) => { - try { - assertAIMessageLike(message); - } catch { - return false; - } - - return message.tool_calls && message.tool_calls.length > 0; -}; - -export const isAIMessageLike = (message?: BaseMessage) => { - return message instanceof AIMessage || message instanceof AIMessageChunk; -}; - -export function assertAIMessageLike( - message?: BaseMessage, -): asserts message is AIMessage | AIMessageChunk { - if (!message) throw new Error("No message provided to assertion"); - - if (!isAIMessageLike(message)) - throw new Error("Expected instance of AIMessage or AIMessageChunk"); -}