Skip to Content

LangChain.js

@typia/langchain plugs typia controllers into LangChain.jsย . Every method on your TypeScript class โ€” or every endpoint in an OpenAPI document โ€” becomes a DynamicStructuredTool ready for AgentExecutor and friends, with the same parse/coerce/validate/feedback machinery as typia.llm.application wired in automatically.

signature
import { toLangChainTools } from "@typia/langchain"; export function toLangChainTools(props: { controllers: Array<ILlmController | IHttpLlmController>; prefix?: boolean; // default false; if true, tool names are "{controller}_{method}" }): DynamicStructuredTool[];

undefined

@typia/langchain
export function toLangChainTools(props: { controllers: Array<ILlmController | IHttpLlmController>; prefix?: boolean | undefined; }): DynamicStructuredTool[];

Setup

Terminal
npm install @typia/langchain @langchain/core npm install typia npx typia setup

From a TypeScript class

src/main.ts
import { ChainValues, Runnable } from "@langchain/core"; import { ChatPromptTemplate } from "@langchain/core/prompts"; import { DynamicStructuredTool } from "@langchain/core/tools"; import { ChatOpenAI } from "@langchain/openai"; import { toLangChainTools } from "@typia/langchain"; import { AgentExecutor, createToolCallingAgent } from "langchain/agents"; import typia from "typia"; import { Calculator } from "./Calculator"; const tools: DynamicStructuredTool[] = toLangChainTools({ controllers: [ typia.llm.controller<Calculator>("calculator", new Calculator()), ], }); const agent: Runnable = createToolCallingAgent({ llm: new ChatOpenAI({ model: "gpt-4o" }), tools, prompt: ChatPromptTemplate.fromMessages([ ["system", "You are a helpful assistant."], ["human", "{input}"], ["placeholder", "{agent_scratchpad}"], ]), }); const executor: AgentExecutor = new AgentExecutor({ agent, tools }); const result: ChainValues = await executor.invoke({ input: "What is 10 + 5?", });

Every method on Calculator is now a LangChain tool. JSDoc comments become tool descriptions, TypeScript types become JSON schemas. Tool names default to the bare method name; pass prefix: true to get {controllerName}_{methodName}.

Calculator.ts
export class Calculator { /** * Add two numbers. * * @param p The input containing two numbers to add * @returns The sum of a and b */ add(p: Calculator.IProps): Calculator.IResult { return { value: p.x + p.y }; } /** * Subtract two numbers. * * @param p The input containing two numbers to subtract * @returns The difference of a and b */ subtract(p: Calculator.IProps): Calculator.IResult { return { value: p.x - p.y }; } /** * Multiply two numbers. * * @param p The input containing two numbers to multiply * @returns The product of a and b */ multiply(p: Calculator.IProps): Calculator.IResult { return { value: p.x * p.y }; } /** * Divide two numbers. * * @param p The input containing two numbers to divide * @returns The quotient of a and b */ divide(p: Calculator.IProps): Calculator.IResult { if (p.y === 0) { throw new Error("Division by zero is not allowed"); } return { value: p.x / p.y }; } } export namespace Calculator { export interface IProps { /** First operand */ x: number; /** Second operand */ y: number; } /** Result of a calculation. */ export interface IResult { /** Calculated value */ value: number; } }

Method type rules. Every methodโ€™s parameter type must be a keyworded object with static keys (no primitives, arrays, or unions). The return type must be an object or void. See typia.llm.application restrictions for the full list.

From an OpenAPI document

For REST APIs documented with Swagger / OpenAPI, swap HttpLlm.controller in:

src/main.ts
import { DynamicStructuredTool } from "@langchain/core/tools"; import { toLangChainTools } from "@typia/langchain"; import { HttpLlm } from "@typia/utils"; const tools: DynamicStructuredTool[] = toLangChainTools({ controllers: [ HttpLlm.controller({ name: "shopping", document: await fetch( "https://shopping-be.wrtn.ai/editor/swagger.json", ).then((r) => r.json()), connection: { host: "https://shopping-be.wrtn.ai", headers: { Authorization: "Bearer ********" }, }, }), ], });

The function calling harness

Every tool carries the harness โ€” lenient parsing, type coercion, validation feedback. When validation fails, the tool returns the original input annotated with // โŒ markers:

{ "name": "John", "age": "twenty", // โŒ [{"path":"$input.age","expected":"number"}] "email": "not-an-email", // โŒ [{"path":"$input.email","expected":"string & Format<\"email\">"}] "hobbies": "reading" // โŒ [{"path":"$input.hobbies","expected":"Array<string>"}] }

For the mechanics see LlmJson.

Why typia replaces LangChainโ€™s built-in validator

LangChain validates tool arguments with @cfworker/json-schema, which throws ToolInputParsingException before anything else can run. @typia/langchain works around this by registering a permissive passthrough Zod schema (z.record(z.unknown())) โ€” then runs typiaโ€™s much more detailed validator inside the tool body. You get the harness output instead of a generic JSON-schema error.

Structured output

For LangChainโ€™s withStructuredOutput, hand it the schema from typia.llm.parameters and validate the result with typia.validate:

src/main.ts
import { ChatOpenAI } from "@langchain/openai"; import { dedent, LlmJson } from "@typia/utils"; import typia, { tags } from "typia"; interface IMember { email: string & tags.Format<"email">; name: string; age: number & tags.Minimum<0> & tags.Maximum<100>; hobbies: string[]; joined_at: string & tags.Format<"date">; } const model = new ChatOpenAI({ model: "gpt-4o" }) .withStructuredOutput(typia.llm.parameters<IMember>()); const member: IMember = await model.invoke(dedent` I am a new member of the community. My name is John Doe, and I am 25 years old. I like playing basketball and reading books, and joined to this community at 2022-01-01. `); const result = typia.validate<IMember>(member); if (!result.success) { console.error(LlmJson.stringify(result)); // โ†’ send `LlmJson.stringify(result)` back to the model for correction }
Terminal
{ email: 'john.doe@example.com', name: 'John Doe', age: 25, hobbies: [ 'playing basketball', 'reading books' ], joined_at: '2022-01-01' }

The IMember interface is the single source of truth. The schema and the validator both come from it; the feedback loop closes the gap between what the model produced and what your code expected.

Where to go next

Last updated on