Skip to content

Commit df8cc20

Browse files
authored
feat: Added agent support to SDK (#54)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** Provide links to any issues in this repository or elsewhere relating to this pull request. **Describe the solution you've provided** Provide a clear and concise description of what you expect to happen. **Describe alternatives you've considered** Provide a clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the pull request here.
2 parents 32eaf5a + 3c12847 commit df8cc20

File tree

2 files changed

+591
-10
lines changed

2 files changed

+591
-10
lines changed

ldai/client.py

Lines changed: 249 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,81 @@ def to_dict(self) -> dict:
125125
}
126126

127127

128+
@dataclass(frozen=True)
129+
class LDAIAgent:
130+
"""
131+
Represents an AI agent configuration with instructions and model settings.
132+
133+
An agent is similar to an AIConfig but focuses on instructions rather than messages,
134+
making it suitable for AI assistant/agent use cases.
135+
"""
136+
enabled: Optional[bool] = None
137+
model: Optional[ModelConfig] = None
138+
provider: Optional[ProviderConfig] = None
139+
instructions: Optional[str] = None
140+
tracker: Optional[LDAIConfigTracker] = None
141+
142+
def to_dict(self) -> Dict[str, Any]:
143+
"""
144+
Render the given agent as a dictionary object.
145+
"""
146+
result: Dict[str, Any] = {
147+
'_ldMeta': {
148+
'enabled': self.enabled or False,
149+
},
150+
'model': self.model.to_dict() if self.model else None,
151+
'provider': self.provider.to_dict() if self.provider else None,
152+
}
153+
if self.instructions is not None:
154+
result['instructions'] = self.instructions
155+
return result
156+
157+
158+
@dataclass(frozen=True)
159+
class LDAIAgentDefaults:
160+
"""
161+
Default values for AI agent configurations.
162+
163+
Similar to LDAIAgent but without tracker and with optional enabled field,
164+
used as fallback values when agent configurations are not available.
165+
"""
166+
enabled: Optional[bool] = None
167+
model: Optional[ModelConfig] = None
168+
provider: Optional[ProviderConfig] = None
169+
instructions: Optional[str] = None
170+
171+
def to_dict(self) -> Dict[str, Any]:
172+
"""
173+
Render the given agent defaults as a dictionary object.
174+
"""
175+
result: Dict[str, Any] = {
176+
'_ldMeta': {
177+
'enabled': self.enabled or False,
178+
},
179+
'model': self.model.to_dict() if self.model else None,
180+
'provider': self.provider.to_dict() if self.provider else None,
181+
}
182+
if self.instructions is not None:
183+
result['instructions'] = self.instructions
184+
return result
185+
186+
187+
@dataclass
188+
class LDAIAgentConfig:
189+
"""
190+
Configuration for individual agent in batch requests.
191+
192+
Combines agent key with its specific default configuration and variables.
193+
"""
194+
key: str
195+
default_value: LDAIAgentDefaults
196+
variables: Optional[Dict[str, Any]] = None
197+
198+
199+
# Type alias for multiple agents
200+
LDAIAgents = Dict[str, LDAIAgent]
201+
202+
128203
class LDAIClient:
129204
"""The LaunchDarkly AI SDK client object."""
130205

@@ -147,13 +222,144 @@ def config(
147222
:param variables: Additional variables for the model configuration.
148223
:return: The value of the model configuration along with a tracker used for gathering metrics.
149224
"""
150-
variation = self._client.variation(key, context, default_value.to_dict())
225+
model, provider, messages, instructions, tracker, enabled = self.__evaluate(key, context, default_value.to_dict(), variables)
226+
227+
config = AIConfig(
228+
enabled=bool(enabled),
229+
model=model,
230+
messages=messages,
231+
provider=provider,
232+
)
233+
234+
return config, tracker
235+
236+
def agent(
237+
self,
238+
config: LDAIAgentConfig,
239+
context: Context,
240+
) -> LDAIAgent:
241+
"""
242+
Retrieve a single AI Config agent.
243+
244+
This method retrieves a single agent configuration with instructions
245+
dynamically interpolated using the provided variables and context data.
246+
247+
Example::
248+
249+
agent = client.agent(LDAIAgentConfig(
250+
key='research_agent',
251+
default_value=LDAIAgentDefaults(
252+
enabled=True,
253+
model=ModelConfig('gpt-4'),
254+
instructions="You are a research assistant specializing in {{topic}}."
255+
),
256+
variables={'topic': 'climate change'}
257+
), context)
258+
259+
if agent.enabled:
260+
research_result = agent.instructions # Interpolated instructions
261+
agent.tracker.track_success()
262+
263+
:param config: The agent configuration to use.
264+
:param context: The context to evaluate the agent configuration in.
265+
:return: Configured LDAIAgent instance.
266+
"""
267+
# Track single agent usage
268+
self._client.track(
269+
"$ld:ai:agent:function:single",
270+
context,
271+
config.key,
272+
1
273+
)
274+
275+
return self.__evaluate_agent(config.key, context, config.default_value, config.variables)
276+
277+
def agents(
278+
self,
279+
agent_configs: List[LDAIAgentConfig],
280+
context: Context,
281+
) -> LDAIAgents:
282+
"""
283+
Retrieve multiple AI agent configurations.
284+
285+
This method allows you to retrieve multiple agent configurations in a single call,
286+
with each agent having its own default configuration and variables for instruction
287+
interpolation.
288+
289+
Example::
290+
291+
agents = client.agents([
292+
LDAIAgentConfig(
293+
key='research_agent',
294+
default_value=LDAIAgentDefaults(
295+
enabled=True,
296+
instructions='You are a research assistant.'
297+
),
298+
variables={'topic': 'climate change'}
299+
),
300+
LDAIAgentConfig(
301+
key='writing_agent',
302+
default_value=LDAIAgentDefaults(
303+
enabled=True,
304+
instructions='You are a writing assistant.'
305+
),
306+
variables={'style': 'academic'}
307+
)
308+
], context)
309+
310+
research_result = agents["research_agent"].instructions
311+
agents["research_agent"].tracker.track_success()
312+
313+
:param agent_configs: List of agent configurations to retrieve.
314+
:param context: The context to evaluate the agent configurations in.
315+
:return: Dictionary mapping agent keys to their LDAIAgent configurations.
316+
"""
317+
# Track multiple agents usage
318+
agent_count = len(agent_configs)
319+
self._client.track(
320+
"$ld:ai:agent:function:multiple",
321+
context,
322+
agent_count,
323+
agent_count
324+
)
325+
326+
result: LDAIAgents = {}
327+
328+
for config in agent_configs:
329+
agent = self.__evaluate_agent(
330+
config.key,
331+
context,
332+
config.default_value,
333+
config.variables
334+
)
335+
result[config.key] = agent
336+
337+
return result
338+
339+
def __evaluate(
340+
self,
341+
key: str,
342+
context: Context,
343+
default_dict: Dict[str, Any],
344+
variables: Optional[Dict[str, Any]] = None,
345+
) -> Tuple[Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]], Optional[str], LDAIConfigTracker, bool]:
346+
"""
347+
Internal method to evaluate a configuration and extract components.
348+
349+
:param key: The configuration key.
350+
:param context: The evaluation context.
351+
:param default_dict: Default configuration as dictionary.
352+
:param variables: Variables for interpolation.
353+
:return: Tuple of (model, provider, messages, instructions, tracker, enabled).
354+
"""
355+
variation = self._client.variation(key, context, default_dict)
151356

152357
all_variables = {}
153358
if variables:
154359
all_variables.update(variables)
155360
all_variables['ldctx'] = context.to_dict()
156361

362+
# Extract messages
157363
messages = None
158364
if 'messages' in variation and isinstance(variation['messages'], list) and all(
159365
isinstance(entry, dict) for entry in variation['messages']
@@ -168,11 +374,18 @@ def config(
168374
for entry in variation['messages']
169375
]
170376

377+
# Extract instructions
378+
instructions = None
379+
if 'instructions' in variation and isinstance(variation['instructions'], str):
380+
instructions = self.__interpolate_template(variation['instructions'], all_variables)
381+
382+
# Extract provider config
171383
provider_config = None
172384
if 'provider' in variation and isinstance(variation['provider'], dict):
173385
provider = variation['provider']
174386
provider_config = ProviderConfig(provider.get('name', ''))
175387

388+
# Extract model config
176389
model = None
177390
if 'model' in variation and isinstance(variation['model'], dict):
178391
parameters = variation['model'].get('parameters', None)
@@ -183,6 +396,7 @@ def config(
183396
custom=custom
184397
)
185398

399+
# Create tracker
186400
tracker = LDAIConfigTracker(
187401
self._client,
188402
variation.get('_ldMeta', {}).get('variationKey', ''),
@@ -192,21 +406,46 @@ def config(
192406
)
193407

194408
enabled = variation.get('_ldMeta', {}).get('enabled', False)
195-
config = AIConfig(
196-
enabled=bool(enabled),
197-
model=model,
198-
messages=messages,
199-
provider=provider_config,
409+
410+
return model, provider_config, messages, instructions, tracker, enabled
411+
412+
def __evaluate_agent(
413+
self,
414+
key: str,
415+
context: Context,
416+
default_value: LDAIAgentDefaults,
417+
variables: Optional[Dict[str, Any]] = None,
418+
) -> LDAIAgent:
419+
"""
420+
Internal method to evaluate an agent configuration.
421+
422+
:param key: The agent configuration key.
423+
:param context: The evaluation context.
424+
:param default_value: Default agent values.
425+
:param variables: Variables for interpolation.
426+
:return: Configured LDAIAgent instance.
427+
"""
428+
model, provider, messages, instructions, tracker, enabled = self.__evaluate(
429+
key, context, default_value.to_dict(), variables
200430
)
201431

202-
return config, tracker
432+
# For agents, prioritize instructions over messages
433+
final_instructions = instructions if instructions is not None else default_value.instructions
434+
435+
return LDAIAgent(
436+
enabled=bool(enabled) if enabled is not None else default_value.enabled,
437+
model=model or default_value.model,
438+
provider=provider or default_value.provider,
439+
instructions=final_instructions,
440+
tracker=tracker,
441+
)
203442

204443
def __interpolate_template(self, template: str, variables: Dict[str, Any]) -> str:
205444
"""
206-
Interpolate the template with the given variables.
445+
Interpolate the template with the given variables using Mustache format.
207446
208-
:template: The template string.
209-
:variables: The variables to interpolate into the template.
447+
:param template: The template string.
448+
:param variables: The variables to interpolate into the template.
210449
:return: The interpolated string.
211450
"""
212451
return chevron.render(template, variables)

0 commit comments

Comments
 (0)