Skip to content

Commit d28c671

Browse files
jsonbaileyclaude
andcommitted
refactor: LangChainModelRunner and LangChainAgentRunner formally inherit Runner
- LangChainModelRunner: replaces invoke_model/invoke_structured_model with run(input, output_type=None); returns RunnerResult - LangChainAgentRunner: replaces AgentResult with RunnerResult; run() signature gains optional output_type parameter - Tests updated to call run() and assert result.content / result.parsed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ebad407 commit d28c671

3 files changed

Lines changed: 30 additions & 32 deletions

File tree

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, Optional
22

33
from ldai import log
4+
from ldai.providers.runner import Runner
45
from ldai.providers.types import LDAIMetrics, RunnerResult
56

67
from ldai_langchain.langchain_helper import (
78
extract_last_message_content,
8-
get_tool_calls_from_response,
99
sum_token_usage_from_messages,
1010
)
1111

1212

13-
class LangChainAgentRunner:
13+
class LangChainAgentRunner(Runner):
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-
Runner implementation for a single LangChain agent.
20+
Runner implementation for LangChain agents.
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).
2626
27-
Implements the unified :class:`~ldai.providers.runner.Runner` protocol.
27+
Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
28+
:meth:`run`.
2829
"""
2930

3031
def __init__(self, agent: Any):
@@ -36,7 +37,7 @@ async def run(
3637
output_type: Optional[Dict[str, Any]] = None,
3738
) -> RunnerResult:
3839
"""
39-
Run the agent with the given input.
40+
Run the agent with the given input string.
4041
4142
Delegates to the compiled LangChain agent, which handles
4243
the tool-calling loop internally.
@@ -45,21 +46,19 @@ async def run(
4546
:param output_type: Reserved for future structured output support;
4647
currently ignored.
4748
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and
48-
metrics including aggregated token usage and observed ``tool_calls``.
49+
aggregated metrics.
4950
"""
5051
try:
5152
result = await self._agent.ainvoke({
5253
"messages": [{"role": "user", "content": str(input)}]
5354
})
54-
messages: List[Any] = result.get("messages", [])
55-
content = extract_last_message_content(messages)
56-
tool_calls = self._extract_tool_calls(messages)
55+
messages = result.get("messages", [])
56+
output = extract_last_message_content(messages)
5757
return RunnerResult(
58-
content=content,
58+
content=output,
5959
metrics=LDAIMetrics(
6060
success=True,
6161
usage=sum_token_usage_from_messages(messages),
62-
tool_calls=tool_calls if tool_calls else None,
6362
),
6463
raw=result,
6564
)
@@ -70,14 +69,6 @@ async def run(
7069
metrics=LDAIMetrics(success=False, usage=None),
7170
)
7271

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-
8172
def get_agent(self) -> Any:
8273
"""Return the underlying compiled LangChain agent."""
8374
return self._agent

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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.runner import Runner
67
from ldai.providers.types import LDAIMetrics, RunnerResult
78

89
from ldai_langchain.langchain_helper import (
@@ -12,12 +13,12 @@
1213
)
1314

1415

15-
class LangChainModelRunner:
16+
class LangChainModelRunner(Runner):
1617
"""
1718
Runner implementation for LangChain chat models.
1819
1920
Holds a fully-configured BaseChatModel.
20-
Returned by ``LangChainRunnerFactory.create_model(config)``.
21+
Returned by LangChainRunnerFactory.create_model(config).
2122
2223
Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
2324
:meth:`run`.
@@ -45,11 +46,12 @@ async def run(
4546
:param input: A string prompt or a list of :class:`LDMessage` objects
4647
:param output_type: Optional JSON schema dict requesting structured output.
4748
When provided, ``parsed`` on the returned :class:`RunnerResult` is
48-
populated with the structured data.
49+
populated with the parsed JSON document.
4950
:return: :class:`RunnerResult` containing ``content``, ``metrics``,
5051
``raw`` and (when ``output_type`` is set) ``parsed``.
5152
"""
5253
messages = self._coerce_input(input)
54+
5355
if output_type is not None:
5456
return await self._run_structured(messages, output_type)
5557
return await self._run_completion(messages)
@@ -93,11 +95,13 @@ async def _run_completion(self, messages: List[LDMessage]) -> RunnerResult:
9395
)
9496

9597
async def _run_structured(
96-
self, messages: List[LDMessage], response_structure: Dict[str, Any]
98+
self,
99+
messages: List[LDMessage],
100+
output_type: Dict[str, Any],
97101
) -> RunnerResult:
98102
try:
99103
langchain_messages = convert_messages_to_langchain(messages)
100-
structured_llm = self._llm.with_structured_output(response_structure, include_raw=True)
104+
structured_llm = self._llm.with_structured_output(output_type, include_raw=True)
101105
response = await structured_llm.ainvoke(langchain_messages)
102106

103107
if not isinstance(response, dict):
@@ -108,8 +112,12 @@ async def _run_structured(
108112
)
109113

110114
raw_response = response.get('raw')
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 ''
115+
usage = None
116+
raw_content = ''
117+
if raw_response is not None:
118+
if hasattr(raw_response, 'content'):
119+
raw_content = raw_response.content or ''
120+
usage = get_ai_usage_from_response(raw_response)
113121

114122
if response.get('parsing_error'):
115123
log.warning('LangChain structured model invocation had a parsing error')
@@ -132,4 +140,3 @@ async def _run_structured(
132140
content='',
133141
metrics=LDAIMetrics(success=False, usage=None),
134142
)
135-

packages/ai-providers/server-ai-langchain/tests/test_langchain_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def test_returns_provider_name_unchanged_for_unmapped_providers(self):
220220

221221

222222
class TestRunCompletion:
223-
"""Tests for the unified run() method (chat-completion path)."""
223+
"""Tests for run() without structured output."""
224224

225225
@pytest.fixture
226226
def mock_llm(self):
@@ -268,7 +268,7 @@ async def test_returns_success_false_when_model_invocation_throws_error(self, mo
268268

269269

270270
class TestRunStructured:
271-
"""Tests for the unified run() method (structured-output path)."""
271+
"""Tests for run() with structured output."""
272272

273273
@pytest.fixture
274274
def mock_llm(self):
@@ -307,7 +307,7 @@ async def test_returns_success_false_when_structured_model_invocation_throws_err
307307

308308
assert result.metrics.success is False
309309
assert result.parsed is None
310-
assert result.content == ''
310+
assert result.raw is None
311311
assert result.metrics.usage is None
312312

313313

0 commit comments

Comments
 (0)