Skip to content

Commit a770b1f

Browse files
jsonbaileyclaude
andcommitted
feat: Update LangChain runners to implement Runner protocol returning RunnerResult
- LangChainModelRunner.run() implements the unified Runner protocol; returns RunnerResult with content, metrics (LDAIMetrics), raw, and parsed fields. Structured output is supported via the output_type parameter. - LangChainAgentRunner.run() updated to return RunnerResult; populates tool_calls in LDAIMetrics from observed tool_calls in message responses. - Legacy invoke_model() and invoke_structured_model() retained as deprecated adapters that delegate to run() and wrap results into ModelResponse / StructuredResponse for backward compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b5f15b7 commit a770b1f

4 files changed

Lines changed: 130 additions & 86 deletions

File tree

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,83 @@
1-
from typing import Any
1+
from typing import Any, Dict, List, Optional
22

33
from ldai import log
4-
from ldai.providers import AgentResult, AgentRunner
5-
from ldai.providers.types import LDAIMetrics
4+
from ldai.providers.types import LDAIMetrics, RunnerResult
65

76
from ldai_langchain.langchain_helper import (
87
extract_last_message_content,
8+
get_tool_calls_from_response,
99
sum_token_usage_from_messages,
1010
)
1111

1212

13-
class LangChainAgentRunner(AgentRunner):
13+
class LangChainAgentRunner:
1414
"""
1515
CAUTION:
1616
This feature is experimental and should NOT be considered ready for production use.
1717
It may change or be removed without notice and is not subject to backwards
1818
compatibility guarantees.
1919
20-
AgentRunner implementation for LangChain.
20+
Runner implementation for a single LangChain agent.
2121
2222
Wraps a compiled LangChain agent graph (from ``langchain.agents.create_agent``)
2323
and delegates execution to it. Tool calling and loop management are handled
2424
internally by the graph.
25-
Returned by LangChainRunnerFactory.create_agent(config, tools).
25+
Returned by ``LangChainRunnerFactory.create_agent(config, tools)``.
26+
27+
Implements the unified :class:`~ldai.providers.runner.Runner` protocol.
2628
"""
2729

2830
def __init__(self, agent: Any):
2931
self._agent = agent
3032

31-
async def run(self, input: Any) -> AgentResult:
33+
async def run(
34+
self,
35+
input: Any,
36+
output_type: Optional[Dict[str, Any]] = None,
37+
) -> RunnerResult:
3238
"""
33-
Run the agent with the given input string.
39+
Run the agent with the given input.
3440
3541
Delegates to the compiled LangChain agent, which handles
3642
the tool-calling loop internally.
3743
3844
:param input: The user prompt or input to the agent
39-
:return: AgentResult with output, raw response, and aggregated metrics
45+
:param output_type: Reserved for future structured output support;
46+
currently ignored.
47+
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and
48+
metrics including aggregated token usage and observed ``tool_calls``.
4049
"""
4150
try:
4251
result = await self._agent.ainvoke({
4352
"messages": [{"role": "user", "content": str(input)}]
4453
})
45-
messages = result.get("messages", [])
46-
output = extract_last_message_content(messages)
47-
return AgentResult(
48-
output=output,
49-
raw=result,
54+
messages: List[Any] = result.get("messages", [])
55+
content = extract_last_message_content(messages)
56+
tool_calls = self._extract_tool_calls(messages)
57+
return RunnerResult(
58+
content=content,
5059
metrics=LDAIMetrics(
5160
success=True,
5261
usage=sum_token_usage_from_messages(messages),
62+
tool_calls=tool_calls if tool_calls else None,
5363
),
64+
raw=result,
5465
)
5566
except Exception as error:
5667
log.warning(f"LangChain agent run failed: {error}")
57-
return AgentResult(
58-
output="",
59-
raw=None,
68+
return RunnerResult(
69+
content="",
6070
metrics=LDAIMetrics(success=False, usage=None),
6171
)
6272

73+
@staticmethod
74+
def _extract_tool_calls(messages: List[Any]) -> List[str]:
75+
"""Collect tool call names from all messages in the agent output."""
76+
names: List[str] = []
77+
for msg in messages:
78+
names.extend(get_tool_calls_from_response(msg))
79+
return names
80+
6381
def get_agent(self) -> Any:
6482
"""Return the underlying compiled LangChain agent."""
6583
return self._agent
Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from typing import Any, Dict, List
1+
from typing import Any, Dict, List, Optional
22

33
from langchain_core.language_models.chat_models import BaseChatModel
44
from langchain_core.messages import BaseMessage
55
from ldai import LDMessage, log
6-
from ldai.providers.model_runner import ModelRunner
7-
from ldai.providers.types import LDAIMetrics, ModelResponse, StructuredResponse
6+
from ldai.providers.types import LDAIMetrics, RunnerResult
87

98
from ldai_langchain.langchain_helper import (
109
convert_messages_to_langchain,
@@ -13,12 +12,15 @@
1312
)
1413

1514

16-
class LangChainModelRunner(ModelRunner):
15+
class LangChainModelRunner:
1716
"""
18-
ModelRunner implementation for LangChain.
17+
Runner implementation for LangChain chat models.
1918
2019
Holds a fully-configured BaseChatModel.
21-
Returned by LangChainConnector.create_model(config).
20+
Returned by ``LangChainRunnerFactory.create_model(config)``.
21+
22+
Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
23+
:meth:`run`.
2224
"""
2325

2426
def __init__(self, llm: BaseChatModel):
@@ -32,13 +34,37 @@ def get_llm(self) -> BaseChatModel:
3234
"""
3335
return self._llm
3436

35-
async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
37+
async def run(
38+
self,
39+
input: Any,
40+
output_type: Optional[Dict[str, Any]] = None,
41+
) -> RunnerResult:
3642
"""
37-
Invoke the LangChain model with an array of messages.
38-
39-
:param messages: Array of LDMessage objects representing the conversation
40-
:return: ModelResponse containing the model's response and metrics
43+
Run the LangChain model with the given input.
44+
45+
:param input: A string prompt or a list of :class:`LDMessage` objects
46+
:param output_type: Optional JSON schema dict requesting structured output.
47+
When provided, ``parsed`` on the returned :class:`RunnerResult` is
48+
populated with the structured data.
49+
:return: :class:`RunnerResult` containing ``content``, ``metrics``,
50+
``raw`` and (when ``output_type`` is set) ``parsed``.
4151
"""
52+
messages = self._coerce_input(input)
53+
if output_type is not None:
54+
return await self._run_structured(messages, output_type)
55+
return await self._run_completion(messages)
56+
57+
@staticmethod
58+
def _coerce_input(input: Any) -> List[LDMessage]:
59+
if isinstance(input, str):
60+
return [LDMessage(role='user', content=input)]
61+
if isinstance(input, list):
62+
return input
63+
raise TypeError(
64+
f"Unsupported input type for LangChainModelRunner.run: {type(input).__name__}"
65+
)
66+
67+
async def _run_completion(self, messages: List[LDMessage]) -> RunnerResult:
4268
try:
4369
langchain_messages = convert_messages_to_langchain(messages)
4470
response: BaseMessage = await self._llm.ainvoke(langchain_messages)
@@ -52,58 +78,58 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
5278
f'Multimodal response not supported, expecting a string. '
5379
f'Content type: {type(response.content)}, Content: {response.content}'
5480
)
55-
metrics = LDAIMetrics(success=False, usage=metrics.usage)
81+
return RunnerResult(
82+
content='',
83+
metrics=LDAIMetrics(success=False, usage=metrics.usage),
84+
raw=response,
85+
)
5686

57-
return ModelResponse(
58-
message=LDMessage(role='assistant', content=content),
59-
metrics=metrics,
60-
)
87+
return RunnerResult(content=content, metrics=metrics, raw=response)
6188
except Exception as error:
6289
log.warning(f'LangChain model invocation failed: {error}')
63-
return ModelResponse(
64-
message=LDMessage(role='assistant', content=''),
90+
return RunnerResult(
91+
content='',
6592
metrics=LDAIMetrics(success=False, usage=None),
6693
)
6794

68-
async def invoke_structured_model(
69-
self,
70-
messages: List[LDMessage],
71-
response_structure: Dict[str, Any],
72-
) -> StructuredResponse:
73-
"""
74-
Invoke the LangChain model with structured output support.
75-
76-
:param messages: Array of LDMessage objects representing the conversation
77-
:param response_structure: Dictionary defining the output structure
78-
:return: StructuredResponse containing the structured data
79-
"""
80-
structured_response = StructuredResponse(
81-
data={},
82-
raw_response='',
83-
metrics=LDAIMetrics(success=False, usage=None),
84-
)
95+
async def _run_structured(
96+
self, messages: List[LDMessage], response_structure: Dict[str, Any]
97+
) -> RunnerResult:
8598
try:
8699
langchain_messages = convert_messages_to_langchain(messages)
87100
structured_llm = self._llm.with_structured_output(response_structure, include_raw=True)
88101
response = await structured_llm.ainvoke(langchain_messages)
89102

90103
if not isinstance(response, dict):
91104
log.warning(f'Structured output did not return a dict. Got: {type(response)}')
92-
return structured_response
105+
return RunnerResult(
106+
content='',
107+
metrics=LDAIMetrics(success=False, usage=None),
108+
)
93109

94110
raw_response = response.get('raw')
95-
if raw_response is not None:
96-
if hasattr(raw_response, 'content'):
97-
structured_response.raw_response = raw_response.content
98-
structured_response.metrics.usage = get_ai_usage_from_response(raw_response)
111+
usage = get_ai_usage_from_response(raw_response) if raw_response is not None else None
112+
raw_content = raw_response.content if raw_response is not None and hasattr(raw_response, 'content') else ''
99113

100114
if response.get('parsing_error'):
101115
log.warning('LangChain structured model invocation had a parsing error')
102-
return structured_response
116+
return RunnerResult(
117+
content=raw_content,
118+
metrics=LDAIMetrics(success=False, usage=usage),
119+
raw=raw_response,
120+
)
103121

104-
structured_response.metrics.success = True
105-
structured_response.data = response.get('parsed') or {}
106-
return structured_response
122+
parsed = response.get('parsed') or {}
123+
return RunnerResult(
124+
content=raw_content,
125+
metrics=LDAIMetrics(success=True, usage=usage),
126+
raw=raw_response,
127+
parsed=parsed,
128+
)
107129
except Exception as error:
108130
log.warning(f'LangChain structured model invocation failed: {error}')
109-
return structured_response
131+
return RunnerResult(
132+
content='',
133+
metrics=LDAIMetrics(success=False, usage=None),
134+
)
135+

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,10 @@ async def run(self, input: Any) -> AgentGraphResult:
329329
messages = result.get('messages', [])
330330
output = extract_last_message_content(messages)
331331

332-
# Flush per-node metrics to LD trackers
333-
all_eval_results = await handler.flush(self._graph, pending_eval_tasks)
332+
# Flush per-node metrics to LD trackers; eval results are tracked
333+
# internally and intentionally not exposed on AgentGraphResult here
334+
# — judge dispatch is the managed layer's responsibility.
335+
await handler.flush(self._graph, pending_eval_tasks)
334336

335337
tracker.track_path(handler.path)
336338
tracker.track_duration(duration)
@@ -341,7 +343,6 @@ async def run(self, input: Any) -> AgentGraphResult:
341343
output=output,
342344
raw=result,
343345
metrics=LDAIMetrics(success=True),
344-
evaluations=all_eval_results,
345346
)
346347

347348
except Exception as exc:

0 commit comments

Comments
 (0)