Skip to content

Commit 9452281

Browse files
authored
feat(langgraph): Add tool support to LangGraph templates (#130)
* feat(langgraph): Add tool support to LangGraph templates This PR adds tool calls to LangGraph templates. It creates tools using `@langchain/core/tools` with zod schemas. * Use zodType macro for JSON Schema type mapping * Guard tool import by variant * Add toolcalls to cloudflare too * Check langchain style function args as well * Revert expansion of toolcall args to be in line with OTel
1 parent d559ed5 commit 9452281

6 files changed

Lines changed: 117 additions & 4 deletions

File tree

src/runner/templates/agents/browser/langgraph/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
{ "package": "@sentry/browser", "version": "sentry" },
88
{ "package": "@langchain/langgraph", "version": "framework" },
99
{ "package": "@langchain/openai", "version": "1.2.12" },
10-
{ "package": "@langchain/core", "version": "1.1.30" }
10+
{ "package": "@langchain/core", "version": "1.1.30" },
11+
{ "package": "zod", "version": "3" }
1112
],
1213
"versions": ["1.2.0"],
1314
"sentryVersions": ["latest"],

src/runner/templates/agents/browser/langgraph/template.njk

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
{% extends "base.browser.njk" %}
1010

1111
{# Macro to render message content - handles both string and multimodal array #}
12+
{# Macro to convert JSON Schema type to Zod type #}
13+
{% macro zodType(type_str) -%}
14+
{% if type_str == 'number' %}z.number(){% elif type_str == 'integer' %}z.number().int(){% elif type_str == 'boolean' %}z.boolean(){% else %}z.string(){% endif %}
15+
{%- endmacro %}
16+
1217
{% macro renderLangChainContent(content) %}
1318
{% if content is string %}
1419
"{{ content }}"
@@ -34,6 +39,10 @@
3439
{% endif %}
3540
const { ChatOpenAI } = await import("@langchain/openai");
3641
const { HumanMessage, SystemMessage } = await import("@langchain/core/messages");
42+
{% if variant == "compiled" and agent and agent.tools and agent.tools.length > 0 %}
43+
const { tool } = await import("@langchain/core/tools");
44+
const { z } = await import("zod");
45+
{% endif %}
3746

3847
{% if variant == "custom-state" %}
3948
const { StateGraph, Annotation, START, END } = langgraphModule;
@@ -56,7 +65,33 @@
5665
apiKey: OPENAI_API_KEY,
5766
});
5867

59-
const agent = createReactAgent({ llm, tools: [], name: "{{ agent.name }}" });
68+
{% if agent and agent.tools and agent.tools.length > 0 %}
69+
const tools = [
70+
{% for toolDef in agent.tools %}
71+
tool(async (input) => {
72+
{% if toolDef.error %}
73+
throw new Error("{{ toolDef.error }}");
74+
{% elif toolDef.result is defined %}
75+
return {{ toolDef.result | dump }};
76+
{% else %}
77+
return input;
78+
{% endif %}
79+
}, {
80+
name: "{{ toolDef.name }}",
81+
description: "{{ toolDef.description }}",
82+
schema: z.object({
83+
{% if toolDef.parameters and toolDef.parameters.properties %}
84+
{% for paramName, paramDef in toolDef.parameters.properties %}
85+
{{ paramName }}: {{ zodType(paramDef.type) }}{% if paramDef.description %}.describe("{{ paramDef.description }}"){% endif %},
86+
{% endfor %}
87+
{% endif %}
88+
}),
89+
}),
90+
{% endfor %}
91+
];
92+
{% endif %}
93+
94+
const agent = createReactAgent({ llm, tools: {% if agent and agent.tools and agent.tools.length > 0 %}tools{% else %}[]{% endif %}, name: "{{ agent.name }}" });
6095
Sentry.instrumentLangGraph(agent, { recordInputs: true, recordOutputs: true });
6196

6297
log('LangGraph initialized with instrumentLangGraph on compiled graph');

src/runner/templates/agents/cloudflare/langgraph/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
{
1616
"package": "@langchain/core",
1717
"version": "0.3.49"
18+
},
19+
{
20+
"package": "zod",
21+
"version": "3"
1822
}
1923
],
2024
"versions": ["0.2.67"],

src/runner/templates/agents/cloudflare/langgraph/template.njk

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{% extends "base.cloudflare.njk" %}
22

3+
{# Macro to convert JSON Schema type to Zod type #}
4+
{% macro zodType(type_str) -%}
5+
{% if type_str == 'number' %}z.number(){% elif type_str == 'integer' %}z.number().int(){% elif type_str == 'boolean' %}z.boolean(){% else %}z.string(){% endif %}
6+
{%- endmacro %}
7+
38
{# Macro to render message content for LangChain/LangGraph - handles both string and multimodal array #}
49
{% macro renderLangChainContent(content) %}
510
{% if content is string %}
@@ -21,14 +26,44 @@
2126
import { ChatOpenAI } from "@langchain/openai";
2227
import { createReactAgent } from "@langchain/langgraph/prebuilt";
2328
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
29+
{% if agent and agent.tools and agent.tools.length > 0 %}
30+
import { tool } from "@langchain/core/tools";
31+
import { z } from "zod";
32+
{% endif %}
2433
{% endblock %}
2534

2635
{% block dynamic_imports %}
36+
{% if agent and agent.tools and agent.tools.length > 0 %}
37+
const tools = [
38+
{% for toolDef in agent.tools %}
39+
tool(async (input) => {
40+
{% if toolDef.error %}
41+
throw new Error("{{ toolDef.error }}");
42+
{% elif toolDef.result is defined %}
43+
return {{ toolDef.result | dump }};
44+
{% else %}
45+
return input;
46+
{% endif %}
47+
}, {
48+
name: "{{ toolDef.name }}",
49+
description: "{{ toolDef.description }}",
50+
schema: z.object({
51+
{% if toolDef.parameters and toolDef.parameters.properties %}
52+
{% for paramName, paramDef in toolDef.parameters.properties %}
53+
{{ paramName }}: {{ zodType(paramDef.type) }}{% if paramDef.description %}.describe("{{ paramDef.description }}"){% endif %},
54+
{% endfor %}
55+
{% endif %}
56+
}),
57+
}),
58+
{% endfor %}
59+
];
60+
{% endif %}
61+
2762
const llm = new ChatOpenAI({
2863
modelName: {% if causeAPIError %}"invalid-model"{% else %}"{{ inputs[0].model }}"{% endif %},
2964
openAIApiKey: env.OPENAI_API_KEY,
3065
});
31-
const agent = createReactAgent({ llm, tools: [] });
66+
const agent = createReactAgent({ llm, tools: {% if agent and agent.tools and agent.tools.length > 0 %}tools{% else %}[]{% endif %}, name: "{{ agent.name }}" });
3267
Sentry.instrumentLangGraph(agent, { recordInputs: true, recordOutputs: true });
3368
{% endblock %}
3469

src/runner/templates/agents/node/langgraph/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
{
1616
"package": "@langchain/core",
1717
"version": "0.3.49"
18+
},
19+
{
20+
"package": "zod",
21+
"version": "3"
1822
}
1923
],
2024
"versions": ["0.2.67"],

src/runner/templates/agents/node/langgraph/template.njk

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,48 @@
1717
{%- endif %}
1818
{% endmacro %}
1919

20+
{# Macro to convert JSON Schema type to Zod type #}
21+
{% macro zodType(type_str) -%}
22+
{% if type_str == 'number' %}z.number(){% elif type_str == 'integer' %}z.number().int(){% elif type_str == 'boolean' %}z.boolean(){% else %}z.string(){% endif %}
23+
{%- endmacro %}
24+
2025
{% block dynamic_imports %}
2126
const { ChatOpenAI } = await import("@langchain/openai");
2227
const { createReactAgent } = await import("@langchain/langgraph/prebuilt");
2328
const { HumanMessage, SystemMessage } = await import("@langchain/core/messages");
29+
{% if agent and agent.tools and agent.tools.length > 0 %}
30+
const { tool } = await import("@langchain/core/tools");
31+
const { z } = await import("zod");
32+
33+
const tools = [
34+
{% for toolDef in agent.tools %}
35+
tool(async (input) => {
36+
{% if toolDef.error %}
37+
throw new Error("{{ toolDef.error }}");
38+
{% elif toolDef.result is defined %}
39+
return {{ toolDef.result | dump }};
40+
{% else %}
41+
return input;
42+
{% endif %}
43+
}, {
44+
name: "{{ toolDef.name }}",
45+
description: "{{ toolDef.description }}",
46+
schema: z.object({
47+
{% if toolDef.parameters and toolDef.parameters.properties %}
48+
{% for paramName, paramDef in toolDef.parameters.properties %}
49+
{{ paramName }}: {{ zodType(paramDef.type) }}{% if paramDef.description %}.describe("{{ paramDef.description }}"){% endif %},
50+
{% endfor %}
51+
{% endif %}
52+
}),
53+
}),
54+
{% endfor %}
55+
];
56+
{% endif %}
57+
2458
const llm = new ChatOpenAI({
2559
modelName: {% if causeAPIError %}"invalid-model"{% else %}"{{ inputs[0].model }}"{% endif %},
2660
});
27-
const agent = createReactAgent({ llm, tools: [], name: "{{ agent.name }}" });
61+
const agent = createReactAgent({ llm, tools: {% if agent and agent.tools and agent.tools.length > 0 %}tools{% else %}[]{% endif %}, name: "{{ agent.name }}" });
2862
{% endblock %}
2963

3064
{% block test %}

0 commit comments

Comments
 (0)