-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
Describe the bug
Langfuse expect ML format for the playground but the problem is that langchain provides ML Format but not langgraph for tool calls.
LangChain Exemple:
[ { "content": "Can you multiply 8 by 9 and then tell me the weather in London?", "role": "user" }, { "content": "", "role": "assistant" }, { "content": "Interesting! Given both the calculation result and London weather, what would you recommend for outdoor activities?", "role": "user" } ]
{ "content": "", "role": "assistant", "additional_kwargs": { "tool_calls": [ { "index": 0, "id": "tool_0_calculator", "type": "function", "function": { "name": "calculator", "arguments": { "input": { "operation": "*", "a": 8, "b": 9 } } } }, { "index": 1, "id": "tool_1_get_weather", "type": "function", "function": { "name": "get_weather", "arguments": { "input": { "city": "London" } } } } ] } }
LangChain Ouput:
Just normal ML Format
LangGraph Trace:
[ { "content": "Can you multiply 8 by 9 and then tell me the weather in London?", "role": "user" }, { "content": "", "role": "assistant", "additional_kwargs": { "tool_calls": [ { "index": 0, "id": "tool_0_calculator", "type": "function", "function": { "name": "calculator", "arguments": { "input": { "operation": "*", "a": 8, "b": 9 } } } }, { "index": 1, "id": "tool_1_get_weather", "type": "function", "function": { "name": "get_weather", "arguments": { "input": { "city": "London" } } } } ] } }, { "content": "Tools output: Unknown operation, this message has already been sent to the user, never send it again.", "additional_kwargs": {}, "role": "calculator" }, { "content": "Tools output: Weather in London: Cloudy, 15°C, this message has already been sent to the user, never send it again.", "additional_kwargs": {}, "role": "get_weather" }, { "content": "8 multiplied by 9 is 72. The weather in London is Cloudy, 15°C.\n", "role": "assistant" }, { "content": "Interesting! Given both the calculation result and London weather, what would you recommend for outdoor activities?", "role": "user" } ]
You can clearly see that the role takes the tool call name, and this cause problem because we can't open the playground
To reproduce
import { ChatOpenAI } from '@langchain/openai';
import { DynamicTool } from '@langchain/core/tools';
import { CallbackHandler } from 'langfuse-langchain';
import { StateGraph, START, END } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { Annotation } from '@langchain/langgraph';
// Define a simple calculator tool
const calculatorTool = new DynamicTool({
name: 'calculator',
description:
'Performs basic arithmetic operations. Input should be a JSON string with operation, a, and b fields.',
func: async (input: string) => {
try {
const { operation, a, b } = JSON.parse(input);
switch (operation) {
case 'add':
return `${a} + ${b} = ${a + b}`;
case 'subtract':
return `${a} - ${b} = ${a - b}`;
case 'multiply':
return `${a} * ${b} = ${a * b}`;
case 'divide':
if (b === 0) return 'Error: Division by zero';
return `${a} / ${b} = ${a / b}`;
default:
return 'Unknown operation';
}
} catch {
return 'Error: Invalid input format. Expected JSON with operation, a, and b fields.';
}
},
});
// Define a mock weather tool
const weatherTool = new DynamicTool({
name: 'get_weather',
description:
'Gets the current weather for a given city. Input should be a JSON string with a city field.',
func: async (input: string) => {
try {
const { city } = JSON.parse(input);
// Mock weather data
const mockWeather: Record<string, string> = {
'new york': 'Sunny, 72°F',
london: 'Cloudy, 15°C',
tokyo: 'Rainy, 18°C',
paris: 'Partly cloudy, 20°C',
};
const weather = mockWeather[city.toLowerCase()] || 'Weather data not available';
return `Weather in ${city}: ${weather}`;
} catch {
return 'Error: Invalid input format. Expected JSON with a city field.';
}
},
});
// Define the graph state
const GraphState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
// Agent node that calls the model
async function agentNode(state: typeof GraphState.State) {
const model = new ChatOpenAI({
model: 'google/gemini-2.0-flash-001',
temperature: 0.2,
configuration: {
baseURL: 'https://openrouter.ai/api/v1',
apiKey: process.env.OPENROUTER_API_KEY,
},
}).bindTools([calculatorTool, weatherTool]);
const response = await model.invoke(state.messages);
return { messages: [response] };
}
// Tool node that executes tools
async function toolNode(state: typeof GraphState.State) {
const toolNodeInstance = new ToolNode([calculatorTool, weatherTool]);
const result = await toolNodeInstance.invoke(state);
// Log tool execution details (similar to your production code)
console.log('\n🔧 Tool Execution Results:');
for (const message of result.messages as BaseMessage[]) {
if (message.getType() === "tool") {
console.log(` - Tool: ${message.name}`);
console.log(` - Content: ${message.content}`);
console.log(` - Role: ${message.getType()}`);
console.log(` - Message Name: ${(message as { name?: string }).name || 'unknown'}`);
// Simulate the content modification like in your production code
// message.content = `Tools output: ${message.content}, this message has already been sent to the user, never send it again.`;
}
}
return result;
}
// Router function to decide next step
function shouldContinue(state: typeof GraphState.State) {
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
return "tools";
}
return END;
}
// Create the LangGraph
function createTestGraph() {
const workflow = new StateGraph(GraphState)
.addNode("agent", agentNode)
.addNode("tools", toolNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue, {
tools: "tools",
[END]: END,
})
.addEdge("tools", "agent");
return workflow.compile();
}
async function testLangChainTools() {
console.log('🚀 Testing LangGraph Tool Calls with Langfuse Tracing\n');
// Validate required environment variables
const requiredEnvVars = {
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
LANGFUSE_PUBLIC_KEY: process.env.LANGFUSE_PUBLIC_KEY,
LANGFUSE_SECRET_KEY: process.env.LANGFUSE_SECRET_KEY,
};
const missingVars = Object.entries(requiredEnvVars)
.filter(([_, value]) => !value)
.map(([key]) => key);
if (missingVars.length > 0) {
console.error('❌ Missing required environment variables:');
missingVars.forEach(varName => console.error(` - ${varName}`));
console.error('\nPlease set these variables in your .env.local file or environment.');
console.error('\nExample:');
console.error('OPENROUTER_API_KEY=your_openrouter_api_key_here');
console.error('LANGFUSE_PUBLIC_KEY=your_langfuse_public_key_here');
console.error('LANGFUSE_SECRET_KEY=your_langfuse_secret_key_here');
console.error('LANGFUSE_HOST=https://cloud.langfuse.com');
return;
}
// Initialize Langfuse callback handler
const langfuseHandler = new CallbackHandler({
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
secretKey: process.env.LANGFUSE_SECRET_KEY,
baseUrl: process.env.LANGFUSE_HOST || 'https://cloud.langfuse.com',
sessionId: `langgraph-tools-test-${Date.now()}`,
userId: 'test-user',
});
// Create the LangGraph
const graph = createTestGraph();
// Test cases with follow-up discussions
const testCases = [
{
initial: 'What is 15 + 27?',
followUp: 'Great! Now can you multiply that result by 3?'
},
{
initial: 'Calculate 144 divided by 12',
followUp: 'Perfect! Can you tell me what that result plus 5 equals?'
},
{
initial: "What's the weather like in New York?",
followUp: 'Thanks! Based on that weather, should I bring an umbrella?'
},
{
initial: 'Can you multiply 8 by 9 and then tell me the weather in London?',
followUp: 'Interesting! Given both the calculation result and London weather, what would you recommend for outdoor activities?'
},
];
for (const [index, testCase] of testCases.entries()) {
console.log(`\n📝 Test ${index + 1}: ${testCase.initial}`);
console.log('─'.repeat(70));
try {
// Create a new trace for each test case
const traceHandler = new CallbackHandler({
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
secretKey: process.env.LANGFUSE_SECRET_KEY,
baseUrl: process.env.LANGFUSE_HOST || 'https://cloud.langfuse.com',
sessionId: `langgraph-tools-test-${Date.now()}-${index}`,
userId: 'test-user',
metadata: {
testCase: index + 1,
initialQuestion: testCase.initial,
followUpQuestion: testCase.followUp,
timestamp: new Date().toISOString(),
},
tags: ['langgraph-test', 'tool-calling', 'gemini-2.0', 'discussion-flow'],
});
// PART 1: Initial tool call using LangGraph
console.log('🔵 Initial Request:', testCase.initial);
const initialState = {
messages: [new HumanMessage(testCase.initial)]
};
const result = await graph.invoke(initialState, {
callbacks: [traceHandler],
});
console.log('\n📊 Graph Execution Complete');
console.log('📝 Final Messages:');
result.messages.forEach((msg: BaseMessage, i: number) => {
console.log(` ${i + 1}. ${msg.getType()}: ${msg.content}`);
if (msg.getType() === "tool") {
console.log(` - Tool Name: ${(msg as { name?: string }).name || 'unknown'}`);
console.log(` - Tool Role: ${msg.getType()}`);
}
});
// PART 2: Follow-up discussion
console.log('\n🟡 Follow-up Question:', testCase.followUp);
const followUpState = {
messages: [...result.messages, new HumanMessage(testCase.followUp)]
};
const followUpResult = await graph.invoke(followUpState, {
callbacks: [traceHandler],
});
console.log('\n📊 Follow-up Graph Execution Complete');
console.log('📝 New Messages:');
const newMessages = followUpResult.messages.slice(result.messages.length);
newMessages.forEach((msg: BaseMessage, i: number) => {
console.log(` ${i + 1}. ${msg.getType()}: ${msg.content}`);
if (msg.getType() === "tool") {
console.log(` - Tool Name: ${(msg as { name?: string }).name || 'unknown'}`);
console.log(` - Tool Role: ${msg.getType()}`);
}
});
// Flush the trace to Langfuse
await traceHandler.shutdownAsync();
} catch (error) {
console.error('❌ Error:', error.message);
console.error('Stack:', error.stack);
}
}
// Final flush to ensure all traces are sent
await langfuseHandler.shutdownAsync();
console.log('\n✅ All traces sent to Langfuse!');
}
// Run the test if this file is executed directly
testLangChainTools().catch(console.error);
### SDK and container versions
"@langchain/core": "^0.3.39",
"@langchain/langgraph": "^0.2.45",
"@langchain/langgraph-checkpoint-postgres": "^0.0.4",
"@langchain/openai": "^0.3.17",
"langchain": "^0.3.19",
"langfuse-langchain": "^3.37.2",
Langfuse V 3.63.0
### Additional information
This is really frustrating because we can't use the playground to debug tool calls
Are you interested to contribute a fix for this bug?
No
