Skip to content

Commit 8271807

Browse files
committed
create the ai provider interface and factory
1 parent 0d933d2 commit 8271807

File tree

5 files changed

+302
-1
lines changed

5 files changed

+302
-1
lines changed

ldai/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
22
from dataclasses import dataclass
3-
from typing import Any, Dict, List, Literal, Optional
3+
from typing import Any, Dict, List, Literal, Optional, Union
44

55
from ldai.tracker import LDAIConfigTracker
66

@@ -334,6 +334,9 @@ class AIAgentConfigRequest:
334334
# Type alias for multiple agents
335335
AIAgents = Dict[str, AIAgentConfig]
336336

337+
# Type alias for all AI Config variants
338+
AIConfigKind = Union[AIAgentConfig, AICompletionConfig, AIJudgeConfig]
339+
337340

338341
# ============================================================================
339342
# Deprecated Type Aliases for Backward Compatibility

ldai/providers/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""AI Provider interfaces and factory for LaunchDarkly AI SDK."""
2+
3+
from ldai.providers.ai_provider import AIProvider
4+
from ldai.providers.ai_provider_factory import AIProviderFactory, SupportedAIProvider
5+
6+
__all__ = [
7+
'AIProvider',
8+
'AIProviderFactory',
9+
'SupportedAIProvider',
10+
]
11+

ldai/providers/ai_provider.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Abstract base class for AI providers."""
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Any, Dict, List, Optional, Union
5+
6+
from ldai.models import AIConfigKind, LDMessage
7+
from ldai.providers.types import ChatResponse, StructuredResponse
8+
9+
10+
class AIProvider(ABC):
11+
"""
12+
Abstract base class for AI providers that implement chat model functionality.
13+
14+
This class provides the contract that all provider implementations must follow
15+
to integrate with LaunchDarkly's tracking and configuration capabilities.
16+
17+
Following the AICHAT spec recommendation to use base classes with non-abstract methods
18+
for better extensibility and backwards compatibility.
19+
"""
20+
21+
def __init__(self, logger: Optional[Any] = None):
22+
"""
23+
Initialize the AI provider.
24+
25+
:param logger: Optional logger for logging provider operations.
26+
"""
27+
self.logger = logger
28+
29+
async def invoke_model(self, messages: List[LDMessage]) -> ChatResponse:
30+
"""
31+
Invoke the chat model with an array of messages.
32+
33+
This method should convert messages to provider format, invoke the model,
34+
and return a ChatResponse with the result and metrics.
35+
36+
Default implementation takes no action and returns a placeholder response.
37+
Provider implementations should override this method.
38+
39+
:param messages: Array of LDMessage objects representing the conversation
40+
:return: ChatResponse containing the model's response
41+
"""
42+
if self.logger:
43+
self.logger.warn('invokeModel not implemented by this provider')
44+
45+
from ldai.models import LDMessage
46+
from ldai.providers.types import LDAIMetrics
47+
48+
return ChatResponse(
49+
message=LDMessage(role='assistant', content=''),
50+
metrics=LDAIMetrics(success=False, usage=None),
51+
)
52+
53+
async def invoke_structured_model(
54+
self,
55+
messages: List[LDMessage],
56+
response_structure: Dict[str, Any],
57+
) -> StructuredResponse:
58+
"""
59+
Invoke the chat model with structured output support.
60+
61+
This method should convert messages to provider format, invoke the model with
62+
structured output configuration, and return a structured response.
63+
64+
Default implementation takes no action and returns a placeholder response.
65+
Provider implementations should override this method.
66+
67+
:param messages: Array of LDMessage objects representing the conversation
68+
:param response_structure: Dictionary of output configurations keyed by output name
69+
:return: StructuredResponse containing the structured data
70+
"""
71+
if self.logger:
72+
self.logger.warn('invokeStructuredModel not implemented by this provider')
73+
74+
from ldai.providers.types import LDAIMetrics
75+
76+
return StructuredResponse(
77+
data={},
78+
raw_response='',
79+
metrics=LDAIMetrics(success=False, usage=None),
80+
)
81+
82+
@staticmethod
83+
@abstractmethod
84+
async def create(ai_config: AIConfigKind, logger: Optional[Any] = None) -> 'AIProvider':
85+
"""
86+
Static method that constructs an instance of the provider.
87+
88+
Each provider implementation must provide their own static create method
89+
that accepts an AIConfigKind and returns a configured instance.
90+
91+
:param ai_config: The LaunchDarkly AI configuration
92+
:param logger: Optional logger for the provider
93+
:return: Configured provider instance
94+
"""
95+
raise NotImplementedError('Provider implementations must override the static create method')
96+
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Factory for creating AIProvider instances based on the provider configuration."""
2+
3+
import importlib
4+
from typing import Any, List, Literal, Optional, Type
5+
6+
from ldai.models import AIConfigKind
7+
from ldai.providers.ai_provider import AIProvider
8+
9+
10+
# List of supported AI providers
11+
SUPPORTED_AI_PROVIDERS = [
12+
# Multi-provider packages should be last in the list
13+
'langchain',
14+
]
15+
16+
# Type representing the supported AI providers
17+
SupportedAIProvider = Literal['langchain']
18+
19+
20+
class AIProviderFactory:
21+
"""
22+
Factory for creating AIProvider instances based on the provider configuration.
23+
"""
24+
25+
@staticmethod
26+
async def create(
27+
ai_config: AIConfigKind,
28+
logger: Optional[Any] = None,
29+
default_ai_provider: Optional[SupportedAIProvider] = None,
30+
) -> Optional[AIProvider]:
31+
"""
32+
Create an AIProvider instance based on the AI configuration.
33+
34+
This method attempts to load provider-specific implementations dynamically.
35+
Returns None if the provider is not supported.
36+
37+
:param ai_config: The AI configuration
38+
:param logger: Optional logger for logging provider initialization
39+
:param default_ai_provider: Optional default AI provider to use
40+
:return: AIProvider instance or None if not supported
41+
"""
42+
provider_name = ai_config.provider.name.lower() if ai_config.provider else None
43+
# Determine which providers to try based on default_ai_provider
44+
providers_to_try = AIProviderFactory._get_providers_to_try(default_ai_provider, provider_name)
45+
46+
# Try each provider in order
47+
for provider_type in providers_to_try:
48+
provider = await AIProviderFactory._try_create_provider(provider_type, ai_config, logger)
49+
if provider:
50+
return provider
51+
52+
# If no provider was successfully created, log a warning
53+
if logger:
54+
logger.warn(
55+
f"Provider is not supported or failed to initialize: {provider_name or 'unknown'}"
56+
)
57+
return None
58+
59+
@staticmethod
60+
def _get_providers_to_try(
61+
default_ai_provider: Optional[SupportedAIProvider],
62+
provider_name: Optional[str],
63+
) -> List[SupportedAIProvider]:
64+
"""
65+
Determine which providers to try based on default_ai_provider and provider_name.
66+
67+
:param default_ai_provider: Optional default provider to use
68+
:param provider_name: Optional provider name from config
69+
:return: List of providers to try in order
70+
"""
71+
# If default_ai_provider is set, only try that specific provider
72+
if default_ai_provider:
73+
return [default_ai_provider]
74+
75+
# If no default_ai_provider is set, try all providers in order
76+
provider_set = set()
77+
78+
# First try the specific provider if it's supported
79+
if provider_name and provider_name in SUPPORTED_AI_PROVIDERS:
80+
provider_set.add(provider_name) # type: ignore
81+
82+
# Then try multi-provider packages, but avoid duplicates
83+
multi_provider_packages: List[SupportedAIProvider] = ['langchain', 'vercel']
84+
for provider in multi_provider_packages:
85+
provider_set.add(provider)
86+
87+
return list(provider_set)
88+
89+
@staticmethod
90+
async def _try_create_provider(
91+
provider_type: SupportedAIProvider,
92+
ai_config: AIConfigKind,
93+
logger: Optional[Any] = None,
94+
) -> Optional[AIProvider]:
95+
"""
96+
Try to create a provider of the specified type.
97+
98+
:param provider_type: Type of provider to create
99+
:param ai_config: AI configuration
100+
:param logger: Optional logger
101+
:return: AIProvider instance or None if creation failed
102+
"""
103+
provider_mappings = {
104+
'openai': ('launchdarkly_server_sdk_ai_openai', 'OpenAIProvider'),
105+
'langchain': ('launchdarkly_server_sdk_ai_langchain', 'LangChainProvider'),
106+
'vercel': ('launchdarkly_server_sdk_ai_vercel', 'VercelProvider'),
107+
}
108+
109+
if provider_type not in provider_mappings:
110+
return None
111+
112+
package_name, provider_class_name = provider_mappings[provider_type]
113+
return await AIProviderFactory._create_provider(
114+
package_name, provider_class_name, ai_config, logger
115+
)
116+
117+
@staticmethod
118+
async def _create_provider(
119+
package_name: str,
120+
provider_class_name: str,
121+
ai_config: AIConfigKind,
122+
logger: Optional[Any] = None,
123+
) -> Optional[AIProvider]:
124+
"""
125+
Create a provider instance dynamically.
126+
127+
:param package_name: Name of the package containing the provider
128+
:param provider_class_name: Name of the provider class
129+
:param ai_config: AI configuration
130+
:param logger: Optional logger
131+
:return: AIProvider instance or None if creation failed
132+
"""
133+
try:
134+
# Try to dynamically import the provider
135+
# This will work if the package is installed
136+
module = importlib.import_module(package_name)
137+
provider_class: Type[AIProvider] = getattr(module, provider_class_name)
138+
139+
provider = await provider_class.create(ai_config, logger)
140+
if logger:
141+
logger.debug(
142+
f"Successfully created AIProvider for: {ai_config.provider.name if ai_config.provider else 'unknown'} "
143+
f"with package {package_name}"
144+
)
145+
return provider
146+
except (ImportError, AttributeError, Exception) as error:
147+
# If the provider is not available or creation fails, return None
148+
if logger:
149+
logger.warn(
150+
f"Error creating AIProvider for: {ai_config.provider.name if ai_config.provider else 'unknown'} "
151+
f"with package {package_name}: {error}"
152+
)
153+
return None
154+

ldai/providers/types.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Types for AI provider responses."""
2+
3+
from dataclasses import dataclass
4+
from typing import Any, Dict, List, Optional
5+
6+
from ldai.models import LDMessage
7+
from ldai.tracker import TokenUsage
8+
9+
10+
@dataclass
11+
class LDAIMetrics:
12+
"""
13+
Metrics information for AI operations that includes success status and token usage.
14+
"""
15+
success: bool
16+
usage: Optional[TokenUsage] = None
17+
18+
19+
@dataclass
20+
class ChatResponse:
21+
"""
22+
Chat response structure.
23+
"""
24+
message: LDMessage
25+
metrics: LDAIMetrics
26+
evaluations: Optional[List[Any]] = None # List of JudgeResponse, will be populated later
27+
28+
29+
@dataclass
30+
class StructuredResponse:
31+
"""
32+
Structured response from AI models.
33+
"""
34+
data: Dict[str, Any]
35+
raw_response: str
36+
metrics: LDAIMetrics
37+

0 commit comments

Comments
 (0)