Skip to content

Commit aabad93

Browse files
feat: add custom parameters support for tool definitions
Co-Authored-By: traci@launchdarkly.com <traci@launchdarkly.com>
1 parent a48b364 commit aabad93

5 files changed

Lines changed: 293 additions & 7 deletions

File tree

packages/sdk/server-ai/src/ldai/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
LDMessage,
2929
ModelConfig,
3030
ProviderConfig,
31+
ToolDefinition,
3132
)
3233
from ldai.providers import (
3334
AgentGraphResult,
@@ -68,6 +69,7 @@
6869
'LDMessage',
6970
'ModelConfig',
7071
'ProviderConfig',
72+
'ToolDefinition',
7173
'log',
7274
# Deprecated exports
7375
'AIConfig',

packages/sdk/server-ai/src/ldai/client.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
LDMessage,
2626
ModelConfig,
2727
ProviderConfig,
28+
ToolDefinition,
2829
)
2930
from ldai.providers import ToolRegistry
3031
from ldai.providers.runner_factory import RunnerFactory
@@ -68,7 +69,7 @@ def _completion_config(
6869
default: AICompletionConfigDefault,
6970
variables: Optional[Dict[str, Any]] = None,
7071
) -> AICompletionConfig:
71-
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
72+
model, provider, messages, instructions, tracker, enabled, judge_configuration, _, tools = self.__evaluate(
7273
key, context, default.to_dict(), variables
7374
)
7475

@@ -80,6 +81,7 @@ def _completion_config(
8081
provider=provider,
8182
tracker=tracker,
8283
judge_configuration=judge_configuration,
84+
tools=tools,
8385
)
8486

8587
return config
@@ -134,7 +136,9 @@ def _judge_config(
134136
default: AIJudgeConfigDefault,
135137
variables: Optional[Dict[str, Any]] = None,
136138
) -> AIJudgeConfig:
137-
model, provider, messages, instructions, tracker, enabled, judge_configuration, variation = self.__evaluate(
139+
(model, provider, messages, instructions,
140+
tracker, enabled, judge_configuration,
141+
variation, tools) = self.__evaluate(
138142
key, context, default.to_dict(), variables
139143
)
140144

@@ -750,7 +754,8 @@ def __evaluate(
750754
variables: Optional[Dict[str, Any]] = None,
751755
) -> Tuple[
752756
Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]],
753-
Optional[str], LDAIConfigTracker, bool, Optional[Any], Dict[str, Any]
757+
Optional[str], LDAIConfigTracker, bool, Optional[Any], Dict[str, Any],
758+
Optional[List[ToolDefinition]]
754759
]:
755760
"""
756761
Internal method to evaluate a configuration and extract components.
@@ -828,7 +833,23 @@ def __evaluate(
828833
if judges:
829834
judge_configuration = JudgeConfiguration(judges=judges)
830835

831-
return model, provider_config, messages, instructions, tracker, enabled, judge_configuration, variation
836+
tools = None
837+
model_raw = variation.get('model')
838+
params_raw = model_raw.get('parameters') if isinstance(model_raw, dict) else None
839+
tool_defs_raw = params_raw.get('tools') if isinstance(params_raw, dict) else None
840+
if isinstance(tool_defs_raw, list):
841+
tools = [
842+
ToolDefinition(
843+
name=t.get('name', ''),
844+
parameters=t.get('parameters', None)
845+
)
846+
for t in tool_defs_raw
847+
if isinstance(t, dict) and t.get('name')
848+
]
849+
if not tools:
850+
tools = None
851+
852+
return model, provider_config, messages, instructions, tracker, enabled, judge_configuration, variation, tools
832853

833854
def __evaluate_agent(
834855
self,
@@ -846,7 +867,7 @@ def __evaluate_agent(
846867
:param variables: Variables for interpolation.
847868
:return: Configured AIAgentConfig instance.
848869
"""
849-
model, provider, messages, instructions, tracker, enabled, judge_configuration, _ = self.__evaluate(
870+
model, provider, messages, instructions, tracker, enabled, judge_configuration, _, tools = self.__evaluate(
850871
key, context, default.to_dict(), variables
851872
)
852873

@@ -861,6 +882,7 @@ def __evaluate_agent(
861882
instructions=final_instructions,
862883
tracker=tracker,
863884
judge_configuration=judge_configuration or default.judge_configuration,
885+
tools=tools or default.tools,
864886
)
865887

866888
def __interpolate_template(self, template: str, variables: Dict[str, Any]) -> str:

packages/sdk/server-ai/src/ldai/models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,52 @@ def to_dict(self) -> dict:
7777
}
7878

7979

80+
class ToolDefinition:
81+
"""
82+
Definition of a tool available to an AI configuration.
83+
84+
Each tool has a name used to match against the tool registry, and
85+
optional custom parameters that can be configured via the LaunchDarkly
86+
dashboard.
87+
"""
88+
89+
def __init__(self, name: str, parameters: Optional[Dict[str, Any]] = None):
90+
"""
91+
:param name: The name of the tool.
92+
:param parameters: Optional custom parameters for the tool.
93+
"""
94+
self._name = name
95+
self._parameters = parameters
96+
97+
@property
98+
def name(self) -> str:
99+
"""
100+
The name of the tool.
101+
"""
102+
return self._name
103+
104+
def get_parameter(self, key: str) -> Any:
105+
"""
106+
Retrieve a custom parameter by key.
107+
108+
:param key: The parameter key to look up.
109+
:return: The parameter value, or None if not found.
110+
"""
111+
if self._parameters is None:
112+
return None
113+
114+
return self._parameters.get(key)
115+
116+
def to_dict(self) -> dict:
117+
"""
118+
Render the tool definition as a dictionary object.
119+
"""
120+
result: Dict[str, Any] = {'name': self._name}
121+
if self._parameters is not None:
122+
result['parameters'] = self._parameters
123+
return result
124+
125+
80126
class ProviderConfig:
81127
"""
82128
Configuration related to the provider.
@@ -208,6 +254,7 @@ class AICompletionConfigDefault(AIConfigDefault):
208254
"""
209255
messages: Optional[List[LDMessage]] = None
210256
judge_configuration: Optional[JudgeConfiguration] = None
257+
tools: Optional[List[ToolDefinition]] = None
211258

212259
def to_dict(self) -> dict:
213260
"""
@@ -217,6 +264,12 @@ def to_dict(self) -> dict:
217264
result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None
218265
if self.judge_configuration is not None:
219266
result['judgeConfiguration'] = self.judge_configuration.to_dict()
267+
if self.tools is not None:
268+
model = result.get('model') or {}
269+
params = model.get('parameters') or {}
270+
params['tools'] = [tool.to_dict() for tool in self.tools]
271+
model['parameters'] = params
272+
result['model'] = model
220273
return result
221274

222275

@@ -227,6 +280,7 @@ class AICompletionConfig(AIConfig):
227280
"""
228281
messages: Optional[List[LDMessage]] = None
229282
judge_configuration: Optional[JudgeConfiguration] = None
283+
tools: Optional[List[ToolDefinition]] = None
230284

231285
def to_dict(self) -> dict:
232286
"""
@@ -250,6 +304,7 @@ class AIAgentConfigDefault(AIConfigDefault):
250304
"""
251305
instructions: Optional[str] = None
252306
judge_configuration: Optional[JudgeConfiguration] = None
307+
tools: Optional[List[ToolDefinition]] = None
253308

254309
def to_dict(self) -> Dict[str, Any]:
255310
"""
@@ -260,6 +315,12 @@ def to_dict(self) -> Dict[str, Any]:
260315
result['instructions'] = self.instructions
261316
if self.judge_configuration is not None:
262317
result['judgeConfiguration'] = self.judge_configuration.to_dict()
318+
if self.tools is not None:
319+
model = result.get('model') or {}
320+
params = model.get('parameters') or {}
321+
params['tools'] = [tool.to_dict() for tool in self.tools]
322+
model['parameters'] = params
323+
result['model'] = model
263324
return result
264325

265326

@@ -270,6 +331,7 @@ class AIAgentConfig(AIConfig):
270331
"""
271332
instructions: Optional[str] = None
272333
judge_configuration: Optional[JudgeConfiguration] = None
334+
tools: Optional[List[ToolDefinition]] = None
273335

274336
def to_dict(self) -> Dict[str, Any]:
275337
"""

packages/sdk/server-ai/tests/test_agents.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ldclient.integrations.test_data import TestData
44

55
from ldai import (LDAIAgentConfig, LDAIAgentDefaults, LDAIClient, ModelConfig,
6-
ProviderConfig)
6+
ProviderConfig, ToolDefinition)
77

88

99
@pytest.fixture
@@ -103,6 +103,30 @@ def td() -> TestData:
103103
.variation_for_all(0)
104104
)
105105

106+
# Agent with tools and custom parameters
107+
td.update(
108+
td.flag('agent-with-tools')
109+
.variations(
110+
{
111+
'model': {
112+
'name': 'gpt-4',
113+
'parameters': {
114+
'temperature': 0.3,
115+
'tools': [
116+
{'name': 'get-order', 'parameters': {'includeHistory': True, 'maxItems': 5}},
117+
{'name': 'search-products', 'parameters': {'category': 'electronics'}},
118+
{'name': 'send-email'},
119+
],
120+
},
121+
},
122+
'provider': {'name': 'openai'},
123+
'instructions': 'You are a support agent with tools.',
124+
'_ldMeta': {'enabled': True, 'variationKey': 'tools-v1', 'version': 1, 'mode': 'agent'},
125+
}
126+
)
127+
.variation_for_all(0)
128+
)
129+
106130
return td
107131

108132

@@ -363,3 +387,58 @@ def test_agents_request_without_default_uses_disabled(ldai_client: LDAIClient):
363387

364388
assert 'missing-agent' in agents
365389
assert agents['missing-agent'].enabled is False
390+
391+
392+
def test_agent_config_has_tools(ldai_client: LDAIClient):
393+
"""Test that agent configs parse tools with custom parameters from flag variations."""
394+
context = Context.create('user-key')
395+
396+
agent = ldai_client.agent_config('agent-with-tools', context)
397+
398+
assert agent.enabled is True
399+
assert agent.tools is not None
400+
assert len(agent.tools) == 3
401+
402+
get_order = agent.tools[0]
403+
assert get_order.name == 'get-order'
404+
assert get_order.get_parameter('includeHistory') is True
405+
assert get_order.get_parameter('maxItems') == 5
406+
407+
search = agent.tools[1]
408+
assert search.name == 'search-products'
409+
assert search.get_parameter('category') == 'electronics'
410+
411+
send_email = agent.tools[2]
412+
assert send_email.name == 'send-email'
413+
assert send_email.get_parameter('anything') is None
414+
415+
416+
def test_agent_config_tools_fallback_to_default(ldai_client: LDAIClient):
417+
"""Test that agent config falls back to default tools when flag has no tools."""
418+
context = Context.create('user-key')
419+
default_tools = [ToolDefinition('default-tool', parameters={'timeout': 30})]
420+
default = LDAIAgentDefaults(
421+
enabled=False,
422+
model=ModelConfig('fallback-model'),
423+
instructions='Default instructions',
424+
tools=default_tools,
425+
)
426+
427+
agent = ldai_client.agent_config('customer-support-agent', context, default)
428+
429+
assert agent.enabled is True
430+
# customer-support-agent has no tools in the flag, so falls back to default
431+
assert agent.tools is not None
432+
assert len(agent.tools) == 1
433+
assert agent.tools[0].name == 'default-tool'
434+
assert agent.tools[0].get_parameter('timeout') == 30
435+
436+
437+
def test_agent_config_no_tools(ldai_client: LDAIClient):
438+
"""Test that agent tools is None when neither flag nor default has tools."""
439+
context = Context.create('user-key')
440+
441+
agent = ldai_client.agent_config('customer-support-agent', context)
442+
443+
assert agent.enabled is True
444+
assert agent.tools is None

0 commit comments

Comments
 (0)