|
1 | 1 | import { describe, test, expect } from "vitest"; |
2 | 2 | import { |
3 | 3 | convertResponseContentToChatGenerationChunk, |
| 4 | + convertMessageContentToParts, |
4 | 5 | mapGenerateContentResultToChatResult, |
5 | 6 | } from "../utils/common.js"; |
| 7 | +import { AIMessage } from "@langchain/core/messages"; |
6 | 8 | import type { |
7 | 9 | EnhancedGenerateContentResponse, |
8 | 10 | FinishReason, |
@@ -269,3 +271,108 @@ describe("Streaming thinking content handling", () => { |
269 | 271 | expect(Array.isArray(content)).toBe(true); |
270 | 272 | }); |
271 | 273 | }); |
| 274 | + |
| 275 | +// https://github.com/langchain-ai/langchainjs/issues/10103 |
| 276 | +describe("Round-trip thinking content handling", () => { |
| 277 | + test("thinking block with signature converts back to Gemini part", () => { |
| 278 | + const message = new AIMessage({ |
| 279 | + content: [ |
| 280 | + { |
| 281 | + type: "thinking", |
| 282 | + thinking: "Let me reason about this...", |
| 283 | + signature: "sig123", |
| 284 | + }, |
| 285 | + { type: "text", text: "The answer is 42." }, |
| 286 | + ], |
| 287 | + }); |
| 288 | + |
| 289 | + const parts = convertMessageContentToParts(message, true, []); |
| 290 | + |
| 291 | + expect(parts).toHaveLength(2); |
| 292 | + expect(parts[0]).toEqual({ |
| 293 | + text: "Let me reason about this...", |
| 294 | + thought: true, |
| 295 | + thoughtSignature: "sig123", |
| 296 | + }); |
| 297 | + expect(parts[1]).toEqual({ text: "The answer is 42." }); |
| 298 | + }); |
| 299 | + |
| 300 | + test("thinking block without signature converts back without thoughtSignature", () => { |
| 301 | + const message = new AIMessage({ |
| 302 | + content: [ |
| 303 | + { type: "thinking", thinking: "Some thinking" }, |
| 304 | + { type: "text", text: "Some answer" }, |
| 305 | + ], |
| 306 | + }); |
| 307 | + |
| 308 | + const parts = convertMessageContentToParts(message, true, []); |
| 309 | + |
| 310 | + expect(parts).toHaveLength(2); |
| 311 | + expect(parts[0]).toEqual({ |
| 312 | + text: "Some thinking", |
| 313 | + thought: true, |
| 314 | + }); |
| 315 | + expect(parts[0]).not.toHaveProperty("thoughtSignature"); |
| 316 | + expect(parts[1]).toEqual({ text: "Some answer" }); |
| 317 | + }); |
| 318 | + |
| 319 | + test("thinking-only content (no text block) works", () => { |
| 320 | + const message = new AIMessage({ |
| 321 | + content: [ |
| 322 | + { |
| 323 | + type: "thinking", |
| 324 | + thinking: "Only thinking, no answer", |
| 325 | + signature: "sigABC", |
| 326 | + }, |
| 327 | + ], |
| 328 | + }); |
| 329 | + |
| 330 | + const parts = convertMessageContentToParts(message, true, []); |
| 331 | + |
| 332 | + expect(parts).toHaveLength(1); |
| 333 | + expect(parts[0]).toEqual({ |
| 334 | + text: "Only thinking, no answer", |
| 335 | + thought: true, |
| 336 | + thoughtSignature: "sigABC", |
| 337 | + }); |
| 338 | + }); |
| 339 | + |
| 340 | + test("full round-trip: Gemini response -> LangChain -> Gemini parts", () => { |
| 341 | + const originalParts = [ |
| 342 | + { |
| 343 | + text: "Let me think step by step...", |
| 344 | + thought: true, |
| 345 | + thoughtSignature: "roundtrip-sig", |
| 346 | + }, |
| 347 | + { |
| 348 | + text: "The final answer is 7.", |
| 349 | + }, |
| 350 | + ] as GoogleGenerativeAIPart[]; |
| 351 | + |
| 352 | + // Gemini response -> LangChain AIMessage |
| 353 | + const mockResponse = createMockResponse([ |
| 354 | + { |
| 355 | + content: { role: "model", parts: originalParts }, |
| 356 | + finishReason: "STOP" as FinishReason, |
| 357 | + index: 0, |
| 358 | + safetyRatings: [], |
| 359 | + }, |
| 360 | + ]); |
| 361 | + |
| 362 | + const chatResult = mapGenerateContentResultToChatResult(mockResponse); |
| 363 | + const aiMessage = chatResult.generations[0].message; |
| 364 | + |
| 365 | + // LangChain AIMessage -> Gemini parts (outgoing direction) |
| 366 | + const roundTrippedParts = convertMessageContentToParts(aiMessage, true, []); |
| 367 | + |
| 368 | + expect(roundTrippedParts).toHaveLength(2); |
| 369 | + expect(roundTrippedParts[0]).toEqual({ |
| 370 | + text: "Let me think step by step...", |
| 371 | + thought: true, |
| 372 | + thoughtSignature: "roundtrip-sig", |
| 373 | + }); |
| 374 | + expect(roundTrippedParts[1]).toEqual({ |
| 375 | + text: "The final answer is 7.", |
| 376 | + }); |
| 377 | + }); |
| 378 | +}); |
0 commit comments