index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import { z } from "zod";
  2. import { validate, } from "@cfworker/json-schema";
  3. import { CallbackManager, parseCallbackConfigArg, } from "../callbacks/manager.js";
  4. import { BaseLangChain } from "../language_models/base.js";
  5. import { ensureConfig, patchConfig, pickRunnableConfigKeys, } from "../runnables/config.js";
  6. import { isDirectToolOutput, ToolMessage } from "../messages/tool.js";
  7. import { AsyncLocalStorageProviderSingleton } from "../singletons/index.js";
  8. import { _configHasToolCallId, _isToolCall, ToolInputParsingException, } from "./utils.js";
  9. import { isZodSchema } from "../utils/types/is_zod_schema.js";
  10. import { validatesOnlyStrings } from "../utils/json_schema.js";
  11. export { isLangChainTool, isRunnableToolLike, isStructuredTool, isStructuredToolParams, } from "./types.js";
  12. export { ToolInputParsingException };
  13. /**
  14. * Base class for Tools that accept input of any shape defined by a Zod schema.
  15. */
  16. export class StructuredTool extends BaseLangChain {
  17. get lc_namespace() {
  18. return ["langchain", "tools"];
  19. }
  20. constructor(fields) {
  21. super(fields ?? {});
  22. /**
  23. * Whether to return the tool's output directly.
  24. *
  25. * Setting this to true means that after the tool is called,
  26. * an agent should stop looping.
  27. */
  28. Object.defineProperty(this, "returnDirect", {
  29. enumerable: true,
  30. configurable: true,
  31. writable: true,
  32. value: false
  33. });
  34. Object.defineProperty(this, "verboseParsingErrors", {
  35. enumerable: true,
  36. configurable: true,
  37. writable: true,
  38. value: false
  39. });
  40. /**
  41. * The tool response format.
  42. *
  43. * If "content" then the output of the tool is interpreted as the contents of a
  44. * ToolMessage. If "content_and_artifact" then the output is expected to be a
  45. * two-tuple corresponding to the (content, artifact) of a ToolMessage.
  46. *
  47. * @default "content"
  48. */
  49. Object.defineProperty(this, "responseFormat", {
  50. enumerable: true,
  51. configurable: true,
  52. writable: true,
  53. value: "content"
  54. });
  55. this.verboseParsingErrors =
  56. fields?.verboseParsingErrors ?? this.verboseParsingErrors;
  57. this.responseFormat = fields?.responseFormat ?? this.responseFormat;
  58. }
  59. /**
  60. * Invokes the tool with the provided input and configuration.
  61. * @param input The input for the tool.
  62. * @param config Optional configuration for the tool.
  63. * @returns A Promise that resolves with the tool's output.
  64. */
  65. async invoke(input, config) {
  66. let toolInput;
  67. let enrichedConfig = ensureConfig(config);
  68. if (_isToolCall(input)) {
  69. toolInput = input.args;
  70. enrichedConfig = {
  71. ...enrichedConfig,
  72. toolCall: input,
  73. };
  74. }
  75. else {
  76. toolInput = input;
  77. }
  78. return this.call(toolInput, enrichedConfig);
  79. }
  80. /**
  81. * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
  82. *
  83. * Calls the tool with the provided argument, configuration, and tags. It
  84. * parses the input according to the schema, handles any errors, and
  85. * manages callbacks.
  86. * @param arg The input argument for the tool.
  87. * @param configArg Optional configuration or callbacks for the tool.
  88. * @param tags Optional tags for the tool.
  89. * @returns A Promise that resolves with a string.
  90. */
  91. async call(arg, configArg,
  92. /** @deprecated */
  93. tags) {
  94. // Determine the actual input that needs parsing/validation.
  95. // If arg is a ToolCall, use its args; otherwise, use arg directly.
  96. const inputForValidation = _isToolCall(arg) ? arg.args : arg;
  97. let parsed; // This will hold the successfully parsed input of the expected output type.
  98. if (isZodSchema(this.schema)) {
  99. try {
  100. // Validate the inputForValidation - TS needs help here as it can't exclude ToolCall based on the check
  101. parsed = await this.schema.parseAsync(inputForValidation);
  102. }
  103. catch (e) {
  104. let message = `Received tool input did not match expected schema`;
  105. if (this.verboseParsingErrors) {
  106. message = `${message}\nDetails: ${e.message}`;
  107. }
  108. // Pass the original raw input arg to the exception
  109. throw new ToolInputParsingException(message, JSON.stringify(arg));
  110. }
  111. }
  112. else {
  113. const result = validate(inputForValidation, this.schema);
  114. if (!result.valid) {
  115. let message = `Received tool input did not match expected schema`;
  116. if (this.verboseParsingErrors) {
  117. message = `${message}\nDetails: ${result.errors
  118. .map((e) => `${e.keywordLocation}: ${e.error}`)
  119. .join("\n")}`;
  120. }
  121. // Pass the original raw input arg to the exception
  122. throw new ToolInputParsingException(message, JSON.stringify(arg));
  123. }
  124. // Assign the validated input to parsed
  125. // We cast here because validate() doesn't narrow the type sufficiently for TS, but we know it's valid.
  126. parsed = inputForValidation;
  127. }
  128. const config = parseCallbackConfigArg(configArg);
  129. const callbackManager_ = CallbackManager.configure(config.callbacks, this.callbacks, config.tags || tags, this.tags, config.metadata, this.metadata, { verbose: this.verbose });
  130. const runManager = await callbackManager_?.handleToolStart(this.toJSON(),
  131. // Log the original raw input arg
  132. typeof arg === "string" ? arg : JSON.stringify(arg), config.runId, undefined, undefined, undefined, config.runName);
  133. delete config.runId;
  134. let result;
  135. try {
  136. // Pass the correctly typed parsed input to _call
  137. result = await this._call(parsed, runManager, config);
  138. }
  139. catch (e) {
  140. await runManager?.handleToolError(e);
  141. throw e;
  142. }
  143. let content;
  144. let artifact;
  145. if (this.responseFormat === "content_and_artifact") {
  146. if (Array.isArray(result) && result.length === 2) {
  147. [content, artifact] = result;
  148. }
  149. else {
  150. throw new Error(`Tool response format is "content_and_artifact" but the output was not a two-tuple.\nResult: ${JSON.stringify(result)}`);
  151. }
  152. }
  153. else {
  154. content = result;
  155. }
  156. let toolCallId;
  157. // Extract toolCallId ONLY if the original arg was a ToolCall
  158. if (_isToolCall(arg)) {
  159. toolCallId = arg.id;
  160. }
  161. // Or if it was provided in the config's toolCall property
  162. if (!toolCallId && _configHasToolCallId(config)) {
  163. toolCallId = config.toolCall.id;
  164. }
  165. const formattedOutput = _formatToolOutput({
  166. content,
  167. artifact,
  168. toolCallId,
  169. name: this.name,
  170. });
  171. await runManager?.handleToolEnd(formattedOutput);
  172. return formattedOutput;
  173. }
  174. }
  175. /**
  176. * Base class for Tools that accept input as a string.
  177. */
  178. export class Tool extends StructuredTool {
  179. constructor(fields) {
  180. super(fields);
  181. Object.defineProperty(this, "schema", {
  182. enumerable: true,
  183. configurable: true,
  184. writable: true,
  185. value: z
  186. .object({ input: z.string().optional() })
  187. .transform((obj) => obj.input)
  188. });
  189. }
  190. /**
  191. * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
  192. *
  193. * Calls the tool with the provided argument and callbacks. It handles
  194. * string inputs specifically.
  195. * @param arg The input argument for the tool, which can be a string, undefined, or an input of the tool's schema.
  196. * @param callbacks Optional callbacks for the tool.
  197. * @returns A Promise that resolves with a string.
  198. */
  199. // Match the base class signature including the generics and conditional return type
  200. call(arg, callbacks) {
  201. // Prepare the input for the base class call method.
  202. // If arg is string or undefined, wrap it; otherwise, pass ToolCall or { input: ... } directly.
  203. const structuredArg = typeof arg === "string" || arg == null ? { input: arg } : arg;
  204. // Ensure TConfig is passed to super.call
  205. return super.call(structuredArg, callbacks);
  206. }
  207. }
  208. /**
  209. * A tool that can be created dynamically from a function, name, and description.
  210. */
  211. export class DynamicTool extends Tool {
  212. static lc_name() {
  213. return "DynamicTool";
  214. }
  215. constructor(fields) {
  216. super(fields);
  217. Object.defineProperty(this, "name", {
  218. enumerable: true,
  219. configurable: true,
  220. writable: true,
  221. value: void 0
  222. });
  223. Object.defineProperty(this, "description", {
  224. enumerable: true,
  225. configurable: true,
  226. writable: true,
  227. value: void 0
  228. });
  229. Object.defineProperty(this, "func", {
  230. enumerable: true,
  231. configurable: true,
  232. writable: true,
  233. value: void 0
  234. });
  235. this.name = fields.name;
  236. this.description = fields.description;
  237. this.func = fields.func;
  238. this.returnDirect = fields.returnDirect ?? this.returnDirect;
  239. }
  240. /**
  241. * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
  242. */
  243. async call(arg, configArg) {
  244. const config = parseCallbackConfigArg(configArg);
  245. if (config.runName === undefined) {
  246. config.runName = this.name;
  247. }
  248. // Call the Tool class's call method, passing generics through
  249. // Cast config to TConfig to satisfy the super.call signature
  250. return super.call(arg, config);
  251. }
  252. /** @ignore */
  253. async _call(input, // DynamicTool's _call specifically expects a string after schema transformation
  254. runManager, parentConfig) {
  255. return this.func(input, runManager, parentConfig);
  256. }
  257. }
  258. /**
  259. * A tool that can be created dynamically from a function, name, and
  260. * description, designed to work with structured data. It extends the
  261. * StructuredTool class and overrides the _call method to execute the
  262. * provided function when the tool is called.
  263. *
  264. * Schema can be passed as Zod or JSON schema. The tool will not validate
  265. * input if JSON schema is passed.
  266. */
  267. export class DynamicStructuredTool extends StructuredTool {
  268. static lc_name() {
  269. return "DynamicStructuredTool";
  270. }
  271. constructor(fields) {
  272. super(fields);
  273. Object.defineProperty(this, "name", {
  274. enumerable: true,
  275. configurable: true,
  276. writable: true,
  277. value: void 0
  278. });
  279. Object.defineProperty(this, "description", {
  280. enumerable: true,
  281. configurable: true,
  282. writable: true,
  283. value: void 0
  284. });
  285. Object.defineProperty(this, "func", {
  286. enumerable: true,
  287. configurable: true,
  288. writable: true,
  289. value: void 0
  290. });
  291. Object.defineProperty(this, "schema", {
  292. enumerable: true,
  293. configurable: true,
  294. writable: true,
  295. value: void 0
  296. });
  297. this.name = fields.name;
  298. this.description = fields.description;
  299. this.func = fields.func;
  300. this.returnDirect = fields.returnDirect ?? this.returnDirect;
  301. this.schema = fields.schema;
  302. }
  303. /**
  304. * @deprecated Use .invoke() instead. Will be removed in 0.3.0.
  305. */
  306. // Match the base class signature
  307. async call(arg, configArg,
  308. /** @deprecated */
  309. tags) {
  310. const config = parseCallbackConfigArg(configArg);
  311. if (config.runName === undefined) {
  312. config.runName = this.name;
  313. }
  314. // Call the base class method, passing generics through
  315. // Cast config to TConfig to satisfy the super.call signature
  316. return super.call(arg, config, tags);
  317. }
  318. _call(arg, runManager, parentConfig) {
  319. return this.func(arg, runManager, parentConfig);
  320. }
  321. }
  322. /**
  323. * Abstract base class for toolkits in LangChain. Toolkits are collections
  324. * of tools that agents can use. Subclasses must implement the `tools`
  325. * property to provide the specific tools for the toolkit.
  326. */
  327. export class BaseToolkit {
  328. getTools() {
  329. return this.tools;
  330. }
  331. }
  332. export function tool(func, fields) {
  333. const isShapelessZodSchema = fields.schema &&
  334. isZodSchema(fields.schema) &&
  335. (!("shape" in fields.schema) || !fields.schema.shape);
  336. const isStringJSONSchema = validatesOnlyStrings(fields.schema);
  337. // If the schema is not provided, or it's a shapeless schema (e.g. a ZodString), create a DynamicTool
  338. if (!fields.schema || isShapelessZodSchema || isStringJSONSchema) {
  339. return new DynamicTool({
  340. ...fields,
  341. description: fields.description ??
  342. fields.schema?.description ??
  343. `${fields.name} tool`,
  344. func: async (input, runManager, config) => {
  345. return new Promise((resolve, reject) => {
  346. const childConfig = patchConfig(config, {
  347. callbacks: runManager?.getChild(),
  348. });
  349. void AsyncLocalStorageProviderSingleton.runWithConfig(pickRunnableConfigKeys(childConfig), async () => {
  350. try {
  351. // TS doesn't restrict the type here based on the guard above
  352. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  353. resolve(func(input, childConfig));
  354. }
  355. catch (e) {
  356. reject(e);
  357. }
  358. });
  359. });
  360. },
  361. });
  362. }
  363. const schema = fields.schema;
  364. const description = fields.description ??
  365. fields.schema.description ??
  366. `${fields.name} tool`;
  367. return new DynamicStructuredTool({
  368. ...fields,
  369. description,
  370. schema,
  371. func: async (input, runManager, config) => {
  372. return new Promise((resolve, reject) => {
  373. const childConfig = patchConfig(config, {
  374. callbacks: runManager?.getChild(),
  375. });
  376. void AsyncLocalStorageProviderSingleton.runWithConfig(pickRunnableConfigKeys(childConfig), async () => {
  377. try {
  378. resolve(func(input, childConfig));
  379. }
  380. catch (e) {
  381. reject(e);
  382. }
  383. });
  384. });
  385. },
  386. });
  387. }
  388. function _formatToolOutput(params) {
  389. const { content, artifact, toolCallId } = params;
  390. if (toolCallId && !isDirectToolOutput(content)) {
  391. if (typeof content === "string" ||
  392. (Array.isArray(content) &&
  393. content.every((item) => typeof item === "object"))) {
  394. return new ToolMessage({
  395. content,
  396. artifact,
  397. tool_call_id: toolCallId,
  398. name: params.name,
  399. });
  400. }
  401. else {
  402. return new ToolMessage({
  403. content: _stringify(content),
  404. artifact,
  405. tool_call_id: toolCallId,
  406. name: params.name,
  407. });
  408. }
  409. }
  410. else {
  411. return content;
  412. }
  413. }
  414. function _stringify(content) {
  415. try {
  416. return JSON.stringify(content, null, 2) ?? "";
  417. }
  418. catch (_noOp) {
  419. return `${content}`;
  420. }
  421. }