diff --git a/integrations/frameworks/langchain/src/langchain_openviking/tools.py b/integrations/frameworks/langchain/src/langchain_openviking/tools.py index 87d764b0e4..ac1c904d89 100644 --- a/integrations/frameworks/langchain/src/langchain_openviking/tools.py +++ b/integrations/frameworks/langchain/src/langchain_openviking/tools.py @@ -25,6 +25,7 @@ call_openviking, compact_json, ensure_client, + extract_message_text, item_value, iter_result_items, stringify, @@ -683,7 +684,10 @@ def _normalize_messages(messages: str | list[dict[str, Any]]) -> list[dict[str, } ) continue - content = str(message.get("content", "")) + raw_content = message.get("content", "") + content = ( + extract_message_text(raw_content) if isinstance(raw_content, list) else str(raw_content) + ) if role == "tool": normalized.append( { diff --git a/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py b/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py index d0affed064..8370cb5ca8 100644 --- a/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py +++ b/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py @@ -10,7 +10,13 @@ pytest.importorskip("langgraph") pytest.importorskip("langchain_openviking") -from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage +from langchain_core.messages import ( + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, + convert_to_openai_messages, +) from langchain_core.runnables import RunnableLambda from langgraph.store.base import PutOp from openviking_sdk.errors import InvalidArgumentError @@ -177,6 +183,77 @@ def test_create_openviking_tools_exposes_common_viking_primitives(): assert "VikingDB is internal vector/index storage" in health +@pytest.mark.parametrize("role", ["user", "assistant", "system", "tool"]) +@pytest.mark.parametrize("text_format", ["string", "block"]) +def test_viking_store_normalizes_langchain_text_blocks(role: str, text_format: str): + content = [ + {"type": "text", "text": "Remember cobalt deployment."}, + {"type": "text", "text": "中文确认"}, + ] + messages = { + "user": HumanMessage(content=content), + "assistant": AIMessage(content=content), + "system": SystemMessage(content=content), + "tool": ToolMessage(content=content, tool_call_id="call-normal", name="ordinary_tool"), + } + wire = convert_to_openai_messages([messages[role]], text_format=text_format) + client = InMemoryOpenVikingClient() + store = create_openviking_tools(client=client, tool_names=["viking_store"])[0] + + result = json.loads( + store.invoke({"session_id": "text-blocks", "messages": wire, "commit": False}) + ) + + assert result["messages_added"] == 1 + stored = client.sessions["text-blocks"][0] + assert stored["role"] == ("assistant" if role in {"system", "tool"} else role) + part = stored["parts"][0] + assert ( + part["tool_output" if role == "tool" else "text"] == "Remember cobalt deployment.\n中文确认" + ) + if role == "tool": + assert part["tool_id"] == "call-normal" + assert part["tool_name"] == "ordinary_tool" + assert part["tool_status"] == "completed" + + +@pytest.mark.parametrize("content", [None, False, 0]) +def test_viking_store_preserves_scalar_content(content: Any): + client = InMemoryOpenVikingClient() + store = create_openviking_tools(client=client, tool_names=["viking_store"])[0] + + store.invoke( + { + "session_id": "scalar", + "messages": [{"role": "user", "content": content}], + "commit": False, + } + ) + + assert client.sessions["scalar"][0]["parts"] == [{"type": "text", "text": str(content)}] + + +@pytest.mark.parametrize("role", ["user", "system", "tool"]) +def test_viking_store_preserves_native_parts(role: str): + client = InMemoryOpenVikingClient() + store = create_openviking_tools(client=client, tool_names=["viking_store"])[0] + parts = [{"type": "text", "text": "explicit parts control"}] + + store.invoke( + { + "session_id": "native-parts", + "messages": [ + {"role": role, "content": [{"type": "text", "text": "unused"}], "parts": parts} + ], + "commit": False, + } + ) + + stored = client.sessions["native-parts"][0] + assert stored["role"] == ("assistant" if role in {"system", "tool"} else role) + assert stored["parts"] == parts + + def test_create_openviking_tools_uses_peer_id_only_for_message_attribution(): client = InMemoryOpenVikingClient( {"viking://~/memories/profile.md": "The peer likes LangGraph agents."}