Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5c4181c
feat!: Add ManagedResult, RunnerResult, and Runner protocol; rename i…
jsonbailey Apr 28, 2026
4e28ae6
refactor: address review feedback on docstrings
jsonbailey Apr 29, 2026
2dd9329
fix: merge duplicate track_tool_calls methods in LDAIConfigTracker
jsonbailey Apr 29, 2026
b4d15df
fix: avoid double metrics extraction in track_metrics_of helpers
jsonbailey Apr 29, 2026
4fe7eb5
refactor: drop ModelRunner/AgentRunner compat from managed layer
jsonbailey Apr 29, 2026
ff871bf
fix: tighten _track_from_metrics_extractor checks
jsonbailey Apr 29, 2026
d75467f
refactor: remove deprecated ManagedModel.invoke()
jsonbailey Apr 29, 2026
89d0ad7
refactor: type RunnerFactory.create_model/agent returns as Optional[R…
jsonbailey Apr 29, 2026
7a52f24
refactor: handle metrics extraction failures gracefully
jsonbailey Apr 29, 2026
45845ed
refactor: update Judge to use Runner protocol and RunnerResult
jsonbailey Apr 30, 2026
56249a1
feat: Wire LDAIMetrics tool_calls and duration_ms into tracker
jsonbailey Apr 28, 2026
4d86c9c
chore: remove stale PR-10 section comment from test_tracker.py
jsonbailey Apr 30, 2026
cc792ec
refactor: type metrics_extractor as Callable[[Any], Optional[LDAIMetr…
jsonbailey Apr 30, 2026
194ad41
feat: Update OpenAI runners to implement Runner protocol returning Ru…
jsonbailey Apr 28, 2026
2878bda
refactor: OpenAIModelRunner and OpenAIAgentRunner formally inherit Ru…
jsonbailey Apr 30, 2026
0036188
feat: Update LangChain runners to implement Runner protocol returning…
jsonbailey Apr 28, 2026
a2db8cb
refactor: LangChainModelRunner and LangChainAgentRunner formally inhe…
jsonbailey Apr 30, 2026
8e60f79
feat: Add ManagedGraphResult, GraphMetricSummary, and AgentGraphRunne…
jsonbailey Apr 28, 2026
f016b0d
feat: Graph tracking refactor — ManagedAgentGraph drives tracking for…
jsonbailey Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,62 +1,71 @@
from typing import Any
from typing import Any, Dict, Optional

from ldai import log
from ldai.providers import AgentResult, AgentRunner
from ldai.providers.types import LDAIMetrics
from ldai.providers.runner import Runner
from ldai.providers.types import LDAIMetrics, RunnerResult

from ldai_langchain.langchain_helper import (
extract_last_message_content,
sum_token_usage_from_messages,
)


class LangChainAgentRunner(AgentRunner):
class LangChainAgentRunner(Runner):
"""
CAUTION:
This feature is experimental and should NOT be considered ready for production use.
It may change or be removed without notice and is not subject to backwards
compatibility guarantees.

AgentRunner implementation for LangChain.
Runner implementation for LangChain agents.

Wraps a compiled LangChain agent graph (from ``langchain.agents.create_agent``)
and delegates execution to it. Tool calling and loop management are handled
internally by the graph.
Returned by LangChainRunnerFactory.create_agent(config, tools).

Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
:meth:`run`.
"""

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

async def run(self, input: Any) -> AgentResult:
async def run(
self,
input: Any,
output_type: Optional[Dict[str, Any]] = None,
) -> RunnerResult:
"""
Run the agent with the given input string.

Delegates to the compiled LangChain agent, which handles
the tool-calling loop internally.

:param input: The user prompt or input to the agent
:return: AgentResult with output, raw response, and aggregated metrics
:param output_type: Reserved for future structured output support;
currently ignored.
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and
aggregated metrics.
"""
try:
result = await self._agent.ainvoke({
"messages": [{"role": "user", "content": str(input)}]
})
messages = result.get("messages", [])
output = extract_last_message_content(messages)
return AgentResult(
output=output,
raw=result,
return RunnerResult(
content=output,
metrics=LDAIMetrics(
success=True,
usage=sum_token_usage_from_messages(messages),
),
raw=result,
)
except Exception as error:
log.warning(f"LangChain agent run failed: {error}")
return AgentResult(
output="",
raw=None,
return RunnerResult(
content="",
metrics=LDAIMetrics(success=False, usage=None),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import BaseMessage
from ldai import LDMessage, log
from ldai.providers.model_runner import ModelRunner
from ldai.providers.types import LDAIMetrics, ModelResponse, StructuredResponse
from ldai.providers.runner import Runner
from ldai.providers.types import LDAIMetrics, RunnerResult

from ldai_langchain.langchain_helper import (
convert_messages_to_langchain,
Expand All @@ -13,12 +13,15 @@
)


class LangChainModelRunner(ModelRunner):
class LangChainModelRunner(Runner):
"""
ModelRunner implementation for LangChain.
Runner implementation for LangChain chat models.

Holds a fully-configured BaseChatModel.
Returned by LangChainConnector.create_model(config).
Returned by LangChainRunnerFactory.create_model(config).

Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
:meth:`run`.
"""

def __init__(self, llm: BaseChatModel):
Expand All @@ -32,13 +35,38 @@ def get_llm(self) -> BaseChatModel:
"""
return self._llm

async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
async def run(
self,
input: Any,
output_type: Optional[Dict[str, Any]] = None,
) -> RunnerResult:
"""
Invoke the LangChain model with an array of messages.

:param messages: Array of LDMessage objects representing the conversation
:return: ModelResponse containing the model's response and metrics
Run the LangChain model with the given input.

:param input: A string prompt or a list of :class:`LDMessage` objects
:param output_type: Optional JSON schema dict requesting structured output.
When provided, ``parsed`` on the returned :class:`RunnerResult` is
populated with the parsed JSON document.
:return: :class:`RunnerResult` containing ``content``, ``metrics``,
``raw`` and (when ``output_type`` is set) ``parsed``.
"""
messages = self._coerce_input(input)

if output_type is not None:
return await self._run_structured(messages, output_type)
return await self._run_completion(messages)

@staticmethod
def _coerce_input(input: Any) -> List[LDMessage]:
if isinstance(input, str):
return [LDMessage(role='user', content=input)]
if isinstance(input, list):
return input
raise TypeError(
f"Unsupported input type for LangChainModelRunner.run: {type(input).__name__}"
)

async def _run_completion(self, messages: List[LDMessage]) -> RunnerResult:
try:
langchain_messages = convert_messages_to_langchain(messages)
response: BaseMessage = await self._llm.ainvoke(langchain_messages)
Expand All @@ -52,58 +80,63 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
f'Multimodal response not supported, expecting a string. '
f'Content type: {type(response.content)}, Content: {response.content}'
)
metrics = LDAIMetrics(success=False, usage=metrics.usage)
return RunnerResult(
content='',
metrics=LDAIMetrics(success=False, usage=metrics.usage),
raw=response,
)

return ModelResponse(
message=LDMessage(role='assistant', content=content),
metrics=metrics,
)
return RunnerResult(content=content, metrics=metrics, raw=response)
except Exception as error:
log.warning(f'LangChain model invocation failed: {error}')
return ModelResponse(
message=LDMessage(role='assistant', content=''),
return RunnerResult(
content='',
metrics=LDAIMetrics(success=False, usage=None),
)

async def invoke_structured_model(
async def _run_structured(
self,
messages: List[LDMessage],
response_structure: Dict[str, Any],
) -> StructuredResponse:
"""
Invoke the LangChain model with structured output support.

:param messages: Array of LDMessage objects representing the conversation
:param response_structure: Dictionary defining the output structure
:return: StructuredResponse containing the structured data
"""
structured_response = StructuredResponse(
data={},
raw_response='',
metrics=LDAIMetrics(success=False, usage=None),
)
output_type: Dict[str, Any],
) -> RunnerResult:
try:
langchain_messages = convert_messages_to_langchain(messages)
structured_llm = self._llm.with_structured_output(response_structure, include_raw=True)
structured_llm = self._llm.with_structured_output(output_type, include_raw=True)
response = await structured_llm.ainvoke(langchain_messages)

if not isinstance(response, dict):
log.warning(f'Structured output did not return a dict. Got: {type(response)}')
return structured_response
return RunnerResult(
content='',
metrics=LDAIMetrics(success=False, usage=None),
)

raw_response = response.get('raw')
usage = None
raw_content = ''
if raw_response is not None:
if hasattr(raw_response, 'content'):
structured_response.raw_response = raw_response.content
structured_response.metrics.usage = get_ai_usage_from_response(raw_response)
raw_content = raw_response.content or ''
usage = get_ai_usage_from_response(raw_response)

if response.get('parsing_error'):
log.warning('LangChain structured model invocation had a parsing error')
return structured_response
return RunnerResult(
content=raw_content,
metrics=LDAIMetrics(success=False, usage=usage),
raw=raw_response,
)

structured_response.metrics.success = True
structured_response.data = response.get('parsed') or {}
return structured_response
parsed = response.get('parsed') or {}
return RunnerResult(
content=raw_content,
metrics=LDAIMetrics(success=True, usage=usage),
raw=raw_response,
parsed=parsed,
)
except Exception as error:
log.warning(f'LangChain structured model invocation failed: {error}')
return structured_response
return RunnerResult(
content='',
metrics=LDAIMetrics(success=False, usage=None),
)
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,10 @@ async def run(self, input: Any) -> AgentGraphResult:
messages = result.get('messages', [])
output = extract_last_message_content(messages)

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

tracker.track_path(handler.path)
tracker.track_duration(duration)
Expand All @@ -341,7 +343,6 @@ async def run(self, input: Any) -> AgentGraphResult:
output=output,
raw=result,
metrics=LDAIMetrics(success=True),
evaluations=all_eval_results,
)

except Exception as exc:
Expand Down
Loading
Loading