Skip to content

Commit e10be52

Browse files
committed
feat: Support additional create methods for agent and agent_graph
feat!: Rename AIProviderFactory → RunnerFactory feat!: Rename OpenAIProvider → OpenAIRunnerFactory import from ldai_openai.openai_runner_factory feat!: Rename LangChainProvider to LangChainRunnerFactory import from ldai_langchain.langchain_runner_factory feat: Add create_model(), create_agent(), create_agent_graph() to AIProvider ABC (non-abstract, default warns)
1 parent dee9255 commit e10be52

11 files changed

Lines changed: 366 additions & 277 deletions

File tree

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
"""LaunchDarkly AI SDK - LangChain Provider.
1+
"""LaunchDarkly AI SDK - LangChain Connector."""
22

3-
This package provides LangChain integration for the LaunchDarkly Server-Side AI SDK,
4-
"""
5-
6-
from ldai_langchain.langchain_provider import LangChainProvider
3+
from ldai_langchain.langchain_runner_factory import LangChainRunnerFactory
74

85
__version__ = "0.1.0"
96

107
__all__ = [
118
'__version__',
12-
'LangChainProvider',
9+
'LangChainRunnerFactory',
1310
]

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_provider.py renamed to packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_runner_factory.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""LangChain implementation of AIProvider for LaunchDarkly AI SDK."""
1+
"""LangChain connector for LaunchDarkly AI SDK."""
22

33
from typing import Any, Dict, List, Optional, Union
44

@@ -11,31 +11,46 @@
1111
from ldai.tracker import TokenUsage
1212

1313

14-
class LangChainProvider(AIProvider):
14+
class LangChainRunnerFactory(AIProvider):
1515
"""
16-
LangChain implementation of AIProvider.
17-
18-
This provider integrates LangChain models with LaunchDarkly's tracking capabilities.
16+
LangChain connector for the LaunchDarkly AI SDK.
17+
18+
Can be used in two ways:
19+
- Transparently via ExecutorFactory (pass ``default_ai_provider='langchain'`` to
20+
``create_model()`` / ``create_chat()``).
21+
- Directly for full control: instantiate with a ``BaseChatModel``, then call
22+
``invoke_model()`` yourself and use the static convenience methods
23+
(``get_ai_metrics_from_response``, ``convert_messages_to_langchain``,
24+
``map_provider``, ``create_langchain_model``).
1925
"""
2026

21-
def __init__(self, llm: BaseChatModel):
27+
def __init__(self, llm: Optional[BaseChatModel] = None):
2228
"""
23-
Initialize the LangChain provider.
29+
Initialize the LangChain connector.
30+
31+
When called with no arguments the connector acts as a per-provider factory
32+
— call ``create_model(config)`` to obtain a configured instance.
2433
25-
:param llm: A LangChain BaseChatModel instance
34+
When called with an explicit ``llm`` the connector is ready to invoke
35+
the model immediately.
36+
37+
:param llm: A LangChain BaseChatModel instance (optional)
2638
"""
2739
self._llm = llm
2840

29-
@staticmethod
30-
async def create(ai_config: AIConfigKind) -> 'LangChainProvider':
41+
# --- AIProvider factory methods ---
42+
43+
def create_model(self, config: AIConfigKind) -> 'LangChainRunnerFactory':
3144
"""
32-
Static factory method to create a LangChain AIProvider from an AI configuration.
45+
Create a configured LangChain model connector for the given AI config.
3346
34-
:param ai_config: The LaunchDarkly AI configuration
35-
:return: Configured LangChainProvider instance
47+
:param config: The LaunchDarkly AI configuration
48+
:return: Configured LangChainRunnerFactory ready to invoke the model
3649
"""
37-
llm = LangChainProvider.create_langchain_model(ai_config)
38-
return LangChainProvider(llm)
50+
llm = LangChainRunnerFactory.create_langchain_model(config)
51+
return LangChainRunnerFactory(llm)
52+
53+
# --- Model invocation ---
3954

4055
async def invoke_model(self, messages: List[LDMessage]) -> ChatResponse:
4156
"""
@@ -45,9 +60,9 @@ async def invoke_model(self, messages: List[LDMessage]) -> ChatResponse:
4560
:return: ChatResponse containing the model's response and metrics
4661
"""
4762
try:
48-
langchain_messages = LangChainProvider.convert_messages_to_langchain(messages)
63+
langchain_messages = LangChainRunnerFactory.convert_messages_to_langchain(messages)
4964
response: BaseMessage = await self._llm.ainvoke(langchain_messages)
50-
metrics = LangChainProvider.get_ai_metrics_from_response(response)
65+
metrics = LangChainRunnerFactory.get_ai_metrics_from_response(response)
5166

5267
content: str = ''
5368
if isinstance(response.content, str):
@@ -89,7 +104,7 @@ async def invoke_structured_model(
89104
metrics=LDAIMetrics(success=False, usage=None),
90105
)
91106
try:
92-
langchain_messages = LangChainProvider.convert_messages_to_langchain(messages)
107+
langchain_messages = LangChainRunnerFactory.convert_messages_to_langchain(messages)
93108
structured_llm = self._llm.with_structured_output(response_structure, include_raw=True)
94109
response = await structured_llm.ainvoke(langchain_messages)
95110

@@ -104,7 +119,7 @@ async def invoke_structured_model(
104119
if raw_response is not None:
105120
if hasattr(raw_response, 'content'):
106121
structured_response.raw_response = raw_response.content
107-
structured_response.metrics.usage = LangChainProvider.get_ai_usage_from_response(raw_response)
122+
structured_response.metrics.usage = LangChainRunnerFactory.get_ai_usage_from_response(raw_response)
108123

109124
if response.get('parsing_error'):
110125
log.warning(f'LangChain structured model invocation had a parsing error')
@@ -117,11 +132,13 @@ async def invoke_structured_model(
117132
log.warning(f'LangChain structured model invocation failed: {error}')
118133
return structured_response
119134

120-
def get_chat_model(self) -> BaseChatModel:
135+
# --- Convenience accessors ---
136+
137+
def get_chat_model(self) -> Optional[BaseChatModel]:
121138
"""
122139
Get the underlying LangChain model instance.
123140
124-
:return: The underlying BaseChatModel
141+
:return: The underlying BaseChatModel, or None if not yet configured
125142
"""
126143
return self._llm
127144

@@ -174,23 +191,19 @@ def get_ai_usage_from_response(response: BaseMessage) -> TokenUsage:
174191
@staticmethod
175192
def get_ai_metrics_from_response(response: BaseMessage) -> LDAIMetrics:
176193
"""
177-
Get AI metrics from a LangChain provider response.
178-
179-
This method extracts token usage information and success status from LangChain responses
180-
and returns a LaunchDarkly AIMetrics object.
194+
Extract LaunchDarkly AI metrics from a LangChain response.
181195
182196
:param response: The response from the LangChain model
183197
:return: LDAIMetrics with success status and token usage
184198
185-
Example:
186-
# Use with tracker.track_metrics_of for automatic tracking
199+
Example::
200+
187201
response = await tracker.track_metrics_of(
188202
lambda: llm.ainvoke(messages),
189-
LangChainProvider.get_ai_metrics_from_response
203+
LangChainRunnerFactory.get_ai_metrics_from_response
190204
)
191205
"""
192-
# Extract token usage if available
193-
usage = LangChainProvider.get_ai_usage_from_response(response)
206+
usage = LangChainRunnerFactory.get_ai_usage_from_response(response)
194207

195208
return LDAIMetrics(success=True, usage=usage)
196209

@@ -201,9 +214,6 @@ def convert_messages_to_langchain(
201214
"""
202215
Convert LaunchDarkly messages to LangChain messages.
203216
204-
This helper method enables developers to work directly with LangChain message types
205-
while maintaining compatibility with LaunchDarkly's standardized message format.
206-
207217
:param messages: List of LDMessage objects
208218
:return: List of LangChain message objects
209219
:raises ValueError: If an unsupported message role is encountered
@@ -225,10 +235,7 @@ def convert_messages_to_langchain(
225235
@staticmethod
226236
def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
227237
"""
228-
Create a LangChain model from an AI configuration.
229-
230-
This public helper method enables developers to initialize their own LangChain models
231-
using LaunchDarkly AI configurations.
238+
Create a LangChain model from a LaunchDarkly AI configuration.
232239
233240
:param ai_config: The LaunchDarkly AI configuration
234241
:return: A configured LangChain BaseChatModel
@@ -242,7 +249,7 @@ def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
242249
model_name = model_dict.get('name', '')
243250
provider = provider_dict.get('name', '')
244251
parameters = dict(model_dict.get('parameters') or {})
245-
mapped_provider = LangChainProvider.map_provider(provider)
252+
mapped_provider = LangChainRunnerFactory.map_provider(provider)
246253

247254
# Bedrock requires the foundation provider (e.g. Bedrock:Anthropic) passed in
248255
# parameters separately from model_provider, which is used for LangChain routing.
@@ -253,3 +260,4 @@ def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
253260
model_provider=mapped_provider,
254261
**parameters,
255262
)
263+

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

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ldai import LDMessage
99

10-
from ldai_langchain import LangChainProvider
10+
from ldai_langchain import LangChainRunnerFactory
1111

1212

1313
class TestConvertMessagesToLangchain:
@@ -16,7 +16,7 @@ class TestConvertMessagesToLangchain:
1616
def test_converts_system_messages_to_system_message(self):
1717
"""Should convert system messages to SystemMessage."""
1818
messages = [LDMessage(role='system', content='You are a helpful assistant.')]
19-
result = LangChainProvider.convert_messages_to_langchain(messages)
19+
result = LangChainRunnerFactory.convert_messages_to_langchain(messages)
2020

2121
assert len(result) == 1
2222
assert isinstance(result[0], SystemMessage)
@@ -25,7 +25,7 @@ def test_converts_system_messages_to_system_message(self):
2525
def test_converts_user_messages_to_human_message(self):
2626
"""Should convert user messages to HumanMessage."""
2727
messages = [LDMessage(role='user', content='Hello, how are you?')]
28-
result = LangChainProvider.convert_messages_to_langchain(messages)
28+
result = LangChainRunnerFactory.convert_messages_to_langchain(messages)
2929

3030
assert len(result) == 1
3131
assert isinstance(result[0], HumanMessage)
@@ -34,7 +34,7 @@ def test_converts_user_messages_to_human_message(self):
3434
def test_converts_assistant_messages_to_ai_message(self):
3535
"""Should convert assistant messages to AIMessage."""
3636
messages = [LDMessage(role='assistant', content='I am doing well, thank you!')]
37-
result = LangChainProvider.convert_messages_to_langchain(messages)
37+
result = LangChainRunnerFactory.convert_messages_to_langchain(messages)
3838

3939
assert len(result) == 1
4040
assert isinstance(result[0], AIMessage)
@@ -47,7 +47,7 @@ def test_converts_multiple_messages_in_order(self):
4747
LDMessage(role='user', content='What is the weather like?'),
4848
LDMessage(role='assistant', content='I cannot check the weather.'),
4949
]
50-
result = LangChainProvider.convert_messages_to_langchain(messages)
50+
result = LangChainRunnerFactory.convert_messages_to_langchain(messages)
5151

5252
assert len(result) == 3
5353
assert isinstance(result[0], SystemMessage)
@@ -62,11 +62,11 @@ class MockMessage:
6262
content = 'Test message'
6363

6464
with pytest.raises(ValueError, match='Unsupported message role: unknown'):
65-
LangChainProvider.convert_messages_to_langchain([MockMessage()]) # type: ignore
65+
LangChainRunnerFactory.convert_messages_to_langchain([MockMessage()]) # type: ignore
6666

6767
def test_handles_empty_message_array(self):
6868
"""Should handle empty message array."""
69-
result = LangChainProvider.convert_messages_to_langchain([])
69+
result = LangChainRunnerFactory.convert_messages_to_langchain([])
7070
assert len(result) == 0
7171

7272

@@ -84,7 +84,7 @@ def test_creates_metrics_with_success_true_and_token_usage(self):
8484
},
8585
}
8686

87-
result = LangChainProvider.get_ai_metrics_from_response(mock_response)
87+
result = LangChainRunnerFactory.get_ai_metrics_from_response(mock_response)
8888

8989
assert result.success is True
9090
assert result.usage is not None
@@ -103,7 +103,7 @@ def test_creates_metrics_with_snake_case_token_usage(self):
103103
},
104104
}
105105

106-
result = LangChainProvider.get_ai_metrics_from_response(mock_response)
106+
result = LangChainRunnerFactory.get_ai_metrics_from_response(mock_response)
107107

108108
assert result.success is True
109109
assert result.usage is not None
@@ -115,7 +115,7 @@ def test_creates_metrics_with_success_true_and_no_usage_when_metadata_missing(se
115115
"""Should create metrics with success=True and no usage when metadata is missing."""
116116
mock_response = AIMessage(content='Test response')
117117

118-
result = LangChainProvider.get_ai_metrics_from_response(mock_response)
118+
result = LangChainRunnerFactory.get_ai_metrics_from_response(mock_response)
119119

120120
assert result.success is True
121121
assert result.usage is None
@@ -126,23 +126,23 @@ class TestMapProvider:
126126

127127
def test_maps_gemini_to_google_genai(self):
128128
"""Should map gemini to google-genai."""
129-
assert LangChainProvider.map_provider('gemini') == 'google-genai'
130-
assert LangChainProvider.map_provider('Gemini') == 'google-genai'
131-
assert LangChainProvider.map_provider('GEMINI') == 'google-genai'
129+
assert LangChainRunnerFactory.map_provider('gemini') == 'google-genai'
130+
assert LangChainRunnerFactory.map_provider('Gemini') == 'google-genai'
131+
assert LangChainRunnerFactory.map_provider('GEMINI') == 'google-genai'
132132

133133
def test_maps_bedrock_and_model_families_to_bedrock_converse(self):
134134
"""Should map bedrock and bedrock:model_family to bedrock_converse."""
135-
assert LangChainProvider.map_provider('bedrock') == 'bedrock_converse'
136-
assert LangChainProvider.map_provider('Bedrock:Anthropic') == 'bedrock_converse'
137-
assert LangChainProvider.map_provider('bedrock:anthropic') == 'bedrock_converse'
138-
assert LangChainProvider.map_provider('bedrock:amazon') == 'bedrock_converse'
139-
assert LangChainProvider.map_provider('bedrock:cohere') == 'bedrock_converse'
135+
assert LangChainRunnerFactory.map_provider('bedrock') == 'bedrock_converse'
136+
assert LangChainRunnerFactory.map_provider('Bedrock:Anthropic') == 'bedrock_converse'
137+
assert LangChainRunnerFactory.map_provider('bedrock:anthropic') == 'bedrock_converse'
138+
assert LangChainRunnerFactory.map_provider('bedrock:amazon') == 'bedrock_converse'
139+
assert LangChainRunnerFactory.map_provider('bedrock:cohere') == 'bedrock_converse'
140140

141141
def test_returns_provider_name_unchanged_for_unmapped_providers(self):
142142
"""Should return provider name unchanged for unmapped providers."""
143-
assert LangChainProvider.map_provider('openai') == 'openai'
144-
assert LangChainProvider.map_provider('anthropic') == 'anthropic'
145-
assert LangChainProvider.map_provider('unknown') == 'unknown'
143+
assert LangChainRunnerFactory.map_provider('openai') == 'openai'
144+
assert LangChainRunnerFactory.map_provider('anthropic') == 'anthropic'
145+
assert LangChainRunnerFactory.map_provider('unknown') == 'unknown'
146146

147147

148148
class TestInvokeModel:
@@ -158,7 +158,7 @@ async def test_returns_success_true_for_string_content(self, mock_llm):
158158
"""Should return success=True for string content."""
159159
mock_response = AIMessage(content='Test response')
160160
mock_llm.ainvoke = AsyncMock(return_value=mock_response)
161-
provider = LangChainProvider(mock_llm)
161+
provider = LangChainRunnerFactory(mock_llm)
162162

163163
messages = [LDMessage(role='user', content='Hello')]
164164
result = await provider.invoke_model(messages)
@@ -171,7 +171,7 @@ async def test_returns_success_false_for_non_string_content_and_logs_warning(sel
171171
"""Should return success=False for non-string content and log warning."""
172172
mock_response = AIMessage(content=[{'type': 'image', 'data': 'base64data'}])
173173
mock_llm.ainvoke = AsyncMock(return_value=mock_response)
174-
provider = LangChainProvider(mock_llm)
174+
provider = LangChainRunnerFactory(mock_llm)
175175

176176
messages = [LDMessage(role='user', content='Hello')]
177177
result = await provider.invoke_model(messages)
@@ -184,7 +184,7 @@ async def test_returns_success_false_when_model_invocation_throws_error(self, mo
184184
"""Should return success=False when model invocation throws an error."""
185185
error = Exception('Model invocation failed')
186186
mock_llm.ainvoke = AsyncMock(side_effect=error)
187-
provider = LangChainProvider(mock_llm)
187+
provider = LangChainRunnerFactory(mock_llm)
188188

189189
messages = [LDMessage(role='user', content='Hello')]
190190
result = await provider.invoke_model(messages)
@@ -210,7 +210,7 @@ async def test_returns_success_true_for_successful_invocation(self, mock_llm):
210210
mock_structured_llm = MagicMock()
211211
mock_structured_llm.ainvoke = AsyncMock(return_value=mock_response)
212212
mock_llm.with_structured_output = MagicMock(return_value=mock_structured_llm)
213-
provider = LangChainProvider(mock_llm)
213+
provider = LangChainRunnerFactory(mock_llm)
214214

215215
messages = [LDMessage(role='user', content='Hello')]
216216
response_structure = {'type': 'object', 'properties': {}}
@@ -226,7 +226,7 @@ async def test_returns_success_false_when_structured_model_invocation_throws_err
226226
mock_structured_llm = MagicMock()
227227
mock_structured_llm.ainvoke = AsyncMock(side_effect=error)
228228
mock_llm.with_structured_output = MagicMock(return_value=mock_structured_llm)
229-
provider = LangChainProvider(mock_llm)
229+
provider = LangChainRunnerFactory(mock_llm)
230230

231231
messages = [LDMessage(role='user', content='Hello')]
232232
response_structure = {'type': 'object', 'properties': {}}
@@ -244,7 +244,7 @@ class TestGetChatModel:
244244
def test_returns_underlying_llm(self):
245245
"""Should return the underlying LLM."""
246246
mock_llm = MagicMock()
247-
provider = LangChainProvider(mock_llm)
247+
provider = LangChainRunnerFactory(mock_llm)
248248

249249
assert provider.get_chat_model() is mock_llm
250250

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
"""LaunchDarkly AI SDK OpenAI Provider."""
1+
"""LaunchDarkly AI SDK OpenAI Connector."""
22

3-
from ldai_openai.openai_provider import OpenAIProvider
3+
from ldai_openai.openai_runner_factory import OpenAIProvider
44

5-
__all__ = ['OpenAIProvider']
5+
__all__ = [
6+
'OpenAIProvider',
7+
]

0 commit comments

Comments
 (0)