1- from typing import Any , Dict , List
1+ from typing import Any , Dict , List , Optional
22
33from langchain_core .language_models .chat_models import BaseChatModel
44from langchain_core .messages import BaseMessage
55from ldai import LDMessage , log
66from ldai .providers .model_runner import ModelRunner
7- from ldai .providers .types import LDAIMetrics , ModelResponse , StructuredResponse
7+ from ldai .providers .types import LDAIMetrics , ModelResponse , RunnerResult , StructuredResponse
88
99from ldai_langchain .langchain_helper import (
1010 convert_messages_to_langchain ,
1515
1616class LangChainModelRunner (ModelRunner ):
1717 """
18- ModelRunner implementation for LangChain.
18+ Runner implementation for LangChain chat models .
1919
2020 Holds a fully-configured BaseChatModel.
21- Returned by LangChainConnector.create_model(config).
21+ Returned by ``LangChainRunnerFactory.create_model(config)``.
22+
23+ Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
24+ :meth:`run`. The legacy :meth:`invoke_model` and :meth:`invoke_structured_model`
25+ methods are preserved for backward compatibility.
2226 """
2327
2428 def __init__ (self , llm : BaseChatModel ):
@@ -32,13 +36,37 @@ def get_llm(self) -> BaseChatModel:
3236 """
3337 return self ._llm
3438
35- async def invoke_model (self , messages : List [LDMessage ]) -> ModelResponse :
39+ async def run (
40+ self ,
41+ input : Any ,
42+ output_type : Optional [Dict [str , Any ]] = None ,
43+ ) -> RunnerResult :
3644 """
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
45+ Run the LangChain model with the given input.
46+
47+ :param input: A string prompt or a list of :class:`LDMessage` objects
48+ :param output_type: Optional JSON schema dict requesting structured output.
49+ When provided, ``parsed`` on the returned :class:`RunnerResult` is
50+ populated with the structured data.
51+ :return: :class:`RunnerResult` containing ``content``, ``metrics``,
52+ ``raw`` and (when ``output_type`` is set) ``parsed``.
4153 """
54+ messages = self ._coerce_input (input )
55+ if output_type is not None :
56+ return await self ._run_structured (messages , output_type )
57+ return await self ._run_completion (messages )
58+
59+ @staticmethod
60+ def _coerce_input (input : Any ) -> List [LDMessage ]:
61+ if isinstance (input , str ):
62+ return [LDMessage (role = 'user' , content = input )]
63+ if isinstance (input , list ):
64+ return input
65+ raise TypeError (
66+ f"Unsupported input type for LangChainModelRunner.run: { type (input ).__name__ } "
67+ )
68+
69+ async def _run_completion (self , messages : List [LDMessage ]) -> RunnerResult :
4270 try :
4371 langchain_messages = convert_messages_to_langchain (messages )
4472 response : BaseMessage = await self ._llm .ainvoke (langchain_messages )
@@ -52,58 +80,91 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
5280 f'Multimodal response not supported, expecting a string. '
5381 f'Content type: { type (response .content )} , Content: { response .content } '
5482 )
55- metrics = LDAIMetrics (success = False , usage = metrics .usage )
83+ return RunnerResult (
84+ content = '' ,
85+ metrics = LDAIMetrics (success = False , usage = metrics .usage ),
86+ raw = response ,
87+ )
5688
57- return ModelResponse (
58- message = LDMessage (role = 'assistant' , content = content ),
59- metrics = metrics ,
60- )
89+ return RunnerResult (content = content , metrics = metrics , raw = response )
6190 except Exception as error :
6291 log .warning (f'LangChain model invocation failed: { error } ' )
63- return ModelResponse (
64- message = LDMessage ( role = 'assistant' , content = '' ) ,
92+ return RunnerResult (
93+ content = '' ,
6594 metrics = LDAIMetrics (success = False , usage = None ),
6695 )
6796
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- )
97+ async def _run_structured (
98+ self , messages : List [LDMessage ], response_structure : Dict [str , Any ]
99+ ) -> RunnerResult :
85100 try :
86101 langchain_messages = convert_messages_to_langchain (messages )
87102 structured_llm = self ._llm .with_structured_output (response_structure , include_raw = True )
88103 response = await structured_llm .ainvoke (langchain_messages )
89104
90105 if not isinstance (response , dict ):
91106 log .warning (f'Structured output did not return a dict. Got: { type (response )} ' )
92- return structured_response
107+ return RunnerResult (
108+ content = '' ,
109+ metrics = LDAIMetrics (success = False , usage = None ),
110+ )
93111
94112 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 )
113+ usage = get_ai_usage_from_response (raw_response ) if raw_response is not None else None
114+ raw_content = raw_response .content if raw_response is not None and hasattr (raw_response , 'content' ) else ''
99115
100116 if response .get ('parsing_error' ):
101117 log .warning ('LangChain structured model invocation had a parsing error' )
102- return structured_response
118+ return RunnerResult (
119+ content = raw_content ,
120+ metrics = LDAIMetrics (success = False , usage = usage ),
121+ raw = raw_response ,
122+ )
103123
104- structured_response .metrics .success = True
105- structured_response .data = response .get ('parsed' ) or {}
106- return structured_response
124+ parsed = response .get ('parsed' ) or {}
125+ return RunnerResult (
126+ content = raw_content ,
127+ metrics = LDAIMetrics (success = True , usage = usage ),
128+ raw = raw_response ,
129+ parsed = parsed ,
130+ )
107131 except Exception as error :
108132 log .warning (f'LangChain structured model invocation failed: { error } ' )
109- return structured_response
133+ return RunnerResult (
134+ content = '' ,
135+ metrics = LDAIMetrics (success = False , usage = None ),
136+ )
137+
138+ async def invoke_model (self , messages : List [LDMessage ]) -> ModelResponse :
139+ """
140+ Invoke the LangChain model with an array of messages.
141+
142+ .. deprecated::
143+ Use :meth:`run` instead. This method delegates to :meth:`run` and
144+ adapts the result to the legacy :class:`ModelResponse` shape.
145+ """
146+ result = await self ._run_completion (messages )
147+ return ModelResponse (
148+ message = LDMessage (role = 'assistant' , content = result .content ),
149+ metrics = result .metrics ,
150+ )
151+
152+ async def invoke_structured_model (
153+ self ,
154+ messages : List [LDMessage ],
155+ response_structure : Dict [str , Any ],
156+ ) -> StructuredResponse :
157+ """
158+ Invoke the LangChain model with structured output support.
159+
160+ .. deprecated::
161+ Use :meth:`run` with the ``output_type`` argument instead. This
162+ method delegates to :meth:`run` and adapts the result to the
163+ legacy :class:`StructuredResponse` shape.
164+ """
165+ result = await self ._run_structured (messages , response_structure )
166+ return StructuredResponse (
167+ data = result .parsed or {},
168+ raw_response = result .content ,
169+ metrics = result .metrics ,
170+ )
0 commit comments