Skip to content

Commit b6eef1c

Browse files
authored
Fix attribute error for raw tuples in langchain (#261)
* Fix attribute error for raw tuples in langchain * Add CHANGELOG * Address feedback * Cleanup test file
1 parent d83930e commit b6eef1c

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support raw LangChain message inputs (`("role", content)` tuples, dicts, and strings) in input messages, so the prompt is recorded and duplicate `invoke_agent` spans are avoided.

instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/utils.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
AIMessage,
1313
BaseMessage,
1414
ToolMessage,
15+
convert_to_messages,
1516
)
1617

1718
from opentelemetry.semconv._incubating.attributes import (
@@ -152,11 +153,19 @@ def _message_parts(message: BaseMessage) -> list[MessagePart]:
152153

153154

154155
def to_input_messages(
155-
messages: Iterable[BaseMessage],
156+
messages: Iterable[Any],
156157
) -> list[InputMessage]:
157158
"""Convert LangChain messages into spec-conformant ``InputMessage`` s."""
159+
try:
160+
normalized_messages: Iterable[BaseMessage] = convert_to_messages(
161+
list(messages)
162+
)
163+
except Exception: # pylint: disable=broad-except
164+
normalized_messages = [
165+
m for m in messages if isinstance(m, BaseMessage)
166+
]
158167
result: list[InputMessage] = []
159-
for message in messages:
168+
for message in normalized_messages:
160169
parts = _message_parts(message)
161170
if not parts:
162171
continue

instrumentation/opentelemetry-instrumentation-genai-langchain/tests/test_callback_handler.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import uuid
1212
from unittest import mock
1313

14+
import pytest
1415
from langchain_core.messages import AIMessage, HumanMessage
1516
from langchain_core.outputs import ChatGeneration, LLMResult
1617

@@ -22,6 +23,7 @@
2223
make_last_output_message,
2324
make_output_message,
2425
serialize,
26+
to_input_messages,
2527
to_output_messages,
2628
)
2729
from opentelemetry.util.genai.invocation import (
@@ -650,6 +652,64 @@ def test_messages_key_takes_priority_over_other_fields(self):
650652
assert len(result) == 1
651653
assert result[0].parts[0].content == "hello"
652654

655+
def test_messages_key_converts_raw_role_content_tuples(self):
656+
result = make_input_message(
657+
{"messages": [("human", "Hello"), HumanMessage(content="Hi")]}
658+
)
659+
660+
assert len(result) == 2
661+
assert result[0].role == "user"
662+
assert result[0].parts[0].content == "Hello"
663+
assert result[1].parts[0].content == "Hi"
664+
665+
def test_messages_key_converts_role_content_dicts(self):
666+
result = make_input_message(
667+
{"messages": [{"role": "user", "content": "Hello"}]}
668+
)
669+
670+
assert len(result) == 1
671+
assert result[0].role == "user"
672+
assert result[0].parts[0].content == "Hello"
673+
674+
@pytest.mark.parametrize(
675+
"bad_entry",
676+
[
677+
("human",), # too few items for (role, content)
678+
("human", "hi", "extra"), # too many items
679+
("not_a_real_role", "hi"), # unknown role string
680+
{"content": "no role key"}, # dict missing role
681+
{"role": "user"}, # dict missing content
682+
None, # not a message at all
683+
42, # not a message at all
684+
object(), # arbitrary object
685+
],
686+
)
687+
def test_messages_key_malformed_entry_does_not_raise(self, bad_entry):
688+
result = make_input_message(
689+
{"messages": [bad_entry, HumanMessage(content="Hi")]}
690+
)
691+
692+
assert len(result) == 1
693+
assert result[0].parts[0].content == "Hi"
694+
695+
def test_messages_key_all_malformed_returns_empty_without_raising(self):
696+
result = make_input_message(
697+
{"messages": [("human",), None, {"content": "x"}]}
698+
)
699+
700+
assert result == []
701+
702+
def test_to_input_messages_never_raises_on_mixed_bad_input(self):
703+
result = to_input_messages(
704+
[object(), ("bad", "role", "shape"), HumanMessage(content="ok")]
705+
)
706+
707+
assert len(result) == 1
708+
assert result[0].parts[0].content == "ok"
709+
710+
def test_to_input_messages_empty_iterable_returns_empty(self):
711+
assert to_input_messages([]) == []
712+
653713
def test_fallback_serializes_non_message_state_fields(self):
654714
result = make_input_message({"user_query": "what is 2+2?"})
655715

0 commit comments

Comments
 (0)