Skip to content

Commit cbc377d

Browse files
committed
fix: add isinstance guard in structured output, add tool_calls extraction in LangChainAgentRunner, add _coerce_input comment
1 parent bdc779b commit cbc377d

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_agent_runner.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ async def run(
5454
})
5555
messages = result.get("messages", [])
5656
output = extract_last_message_content(messages)
57+
tool_calls = _extract_tool_calls(messages)
5758
return RunnerResult(
5859
content=output,
5960
metrics=LDAIMetrics(
6061
success=True,
6162
usage=sum_token_usage_from_messages(messages),
63+
tool_calls=tool_calls if tool_calls else None,
6264
),
6365
raw=result,
6466
)
@@ -72,3 +74,27 @@ async def run(
7274
def get_agent(self) -> Any:
7375
"""Return the underlying compiled LangChain agent."""
7476
return self._agent
77+
78+
79+
def _extract_tool_calls(messages: Any) -> list:
80+
"""
81+
Extract tool-call names from a LangChain agent's message list.
82+
83+
LangChain's ``AIMessage`` exposes ``.tool_calls`` as a list of dicts
84+
(``{'name': ..., 'args': ..., 'id': ...}``). Some providers emit
85+
OpenAI-style objects with ``.function.name`` instead; handle both shapes.
86+
"""
87+
tool_calls: list = []
88+
for msg in messages or []:
89+
msg_tool_calls = getattr(msg, 'tool_calls', None)
90+
if not msg_tool_calls:
91+
continue
92+
for tc in msg_tool_calls:
93+
if isinstance(tc, dict) and 'name' in tc:
94+
tool_calls.append(tc['name'])
95+
else:
96+
fn = getattr(tc, 'function', None)
97+
name = getattr(fn, 'name', None) if fn is not None else None
98+
if name:
99+
tool_calls.append(name)
100+
return tool_calls

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_model_runner.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ async def run(
5656
return await self._run_structured(messages, output_type)
5757
return await self._run_completion(messages)
5858

59+
# convert_messages_to_langchain only accepts List[LDMessage]; _coerce_input
60+
# normalizes a bare string to [LDMessage(role='user', ...)] before that step.
5961
@staticmethod
6062
def _coerce_input(input: Any) -> List[LDMessage]:
6163
if isinstance(input, str):
@@ -116,7 +118,13 @@ async def _run_structured(
116118
raw_content = ''
117119
if raw_response is not None:
118120
if hasattr(raw_response, 'content'):
119-
raw_content = raw_response.content or ''
121+
if isinstance(raw_response.content, str):
122+
raw_content = raw_response.content
123+
else:
124+
log.warning(
125+
f'Multimodal response not supported in structured mode. '
126+
f'Content type: {type(raw_response.content)}, Content: {raw_response.content}'
127+
)
120128
usage = get_ai_usage_from_response(raw_response)
121129

122130
if response.get('parsing_error'):

0 commit comments

Comments
 (0)