From 13d67cf37ea698a6bee60b9e1e6c0b36a57efafc Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 8 Aug 2025 19:34:36 -0400 Subject: [PATCH] fix(ollama): reasoning should come before text content (#32476) --- .../ollama/langchain_ollama/_compat.py | 12 ++--- .../tests/unit_tests/v1/test_chat_models.py | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/libs/partners/ollama/langchain_ollama/_compat.py b/libs/partners/ollama/langchain_ollama/_compat.py index ac2cedf4d5b..de09e7f939f 100644 --- a/libs/partners/ollama/langchain_ollama/_compat.py +++ b/libs/partners/ollama/langchain_ollama/_compat.py @@ -181,12 +181,6 @@ def _convert_to_v1_from_ollama_format(response: dict[str, Any]) -> AIMessage: """Convert Ollama API response to AIMessage.""" content: list[types.ContentBlock] = [] - # Handle text content - if "message" in response and "content" in response["message"]: - text_content = response["message"]["content"] - if text_content: - content.append(TextContentBlock(type="text", text=text_content)) - # Handle reasoning content first (should come before main response) if "message" in response and "thinking" in response["message"]: thinking_content = response["message"]["thinking"] @@ -198,6 +192,12 @@ def _convert_to_v1_from_ollama_format(response: dict[str, Any]) -> AIMessage: ) ) + # Handle text content + if "message" in response and "content" in response["message"]: + text_content = response["message"]["content"] + if text_content: + content.append(TextContentBlock(type="text", text=text_content)) + # Handle tool calls if "message" in response and "tool_calls" in response["message"]: tool_calls = response["message"]["tool_calls"] diff --git a/libs/partners/ollama/tests/unit_tests/v1/test_chat_models.py b/libs/partners/ollama/tests/unit_tests/v1/test_chat_models.py index e6b82b391c5..0eaed6d3fe4 100644 --- a/libs/partners/ollama/tests/unit_tests/v1/test_chat_models.py +++ b/libs/partners/ollama/tests/unit_tests/v1/test_chat_models.py @@ -185,6 +185,50 @@ class TestMessageConversion: assert result.content[0].get("text") == "Hello" assert result.response_metadata.get("context") == test_context + def test_reasoning_content_block_comes_before_text(self) -> None: + """Test that ReasoningContentBlock always comes before TextContentBlock.""" + ollama_response = { + "model": MODEL_NAME, + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "The final answer is 4.", + "thinking": "Let me calculate: 2 + 2 = 4", + }, + "done": True, + "done_reason": "stop", + } + + result = _convert_to_v1_from_ollama_format(ollama_response) + + assert isinstance(result, AIMessage) + assert len(result.content) == 2 + assert result.content[0].get("type") == "reasoning" + assert result.content[1].get("type") == "text" + + def test_reasoning_content_block_comes_before_text_in_chunks(self) -> None: + """Test ReasoningContentBlock comes before TextContentBlock in chunks.""" + chunk = { + "model": MODEL_NAME, + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "The answer is 42.", + "thinking": "I need to think about this carefully...", + }, + "done": False, + } + + result = _convert_chunk_to_v1(chunk) + + assert len(result.content) == 2 + assert result.content[0].get("type") == "reasoning" + reasoning_text = "I need to think about this carefully..." + assert result.content[0].get("reasoning") == reasoning_text + + assert result.content[1].get("type") == "text" + assert result.content[1].get("text") == "The answer is 42." + def test_convert_empty_content(self) -> None: """Test converting empty content blocks.""" message = HumanMessage(content=[])