Skip to content

bug: LangGraph Playground Disabled With tool calls #7043

@takefy-dev

Description

@takefy-dev

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

Image

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions