Skip to content

Commit 1090994

Browse files
refactor: address review feedback on tool custom parameters
- Extract _get_tool_custom_parameter() helper to deduplicate across 4 classes - Extract _serialize_tool_custom_parameters() helper for to_dict serialization - Add ToolCustomParametersMap type alias for legibility - Only store tools that actually have customParameters (skip bare tool entries) - Add docstring on to_dict explaining completion-config serialization-path fallback - Add comment about dict insertion order in serialization helper Co-Authored-By: traci@launchdarkly.com <traci@launchdarkly.com>
1 parent 869616b commit 1090994

5 files changed

Lines changed: 125 additions & 97 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+
ToolCustomParametersMap,
3132
)
3233
from ldai.providers import (
3334
AgentGraphResult,
@@ -68,6 +69,7 @@
6869
'LDMessage',
6970
'ModelConfig',
7071
'ProviderConfig',
72+
'ToolCustomParametersMap',
7173
'log',
7274
# Deprecated exports
7375
'AIConfig',

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
LDMessage,
2626
ModelConfig,
2727
ProviderConfig,
28+
ToolCustomParametersMap,
2829
)
2930
from ldai.providers import ToolRegistry
3031
from ldai.providers.runner_factory import RunnerFactory
@@ -756,7 +757,7 @@ def __evaluate(
756757
) -> Tuple[
757758
Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]],
758759
Optional[str], LDAIConfigTracker, bool, Optional[Any], Dict[str, Any],
759-
Optional[Dict[str, Dict[str, Any]]]
760+
Optional[ToolCustomParametersMap]
760761
]:
761762
"""
762763
Internal method to evaluate a configuration and extract components.
@@ -836,13 +837,22 @@ def __evaluate(
836837

837838
tool_custom_parameters = None
838839
model_raw = variation.get('model')
839-
params_raw = model_raw.get('parameters') if isinstance(model_raw, dict) else None
840-
tool_defs_raw = params_raw.get('tools') if isinstance(params_raw, dict) else None
840+
params_raw = (
841+
model_raw.get('parameters')
842+
if isinstance(model_raw, dict) else None
843+
)
844+
tool_defs_raw = (
845+
params_raw.get('tools')
846+
if isinstance(params_raw, dict) else None
847+
)
841848
if isinstance(tool_defs_raw, list):
842-
parsed: Dict[str, Dict[str, Any]] = {}
849+
parsed: ToolCustomParametersMap = {}
843850
for t in tool_defs_raw:
844-
if isinstance(t, dict) and t.get('name'):
845-
parsed[t['name']] = t.get('customParameters') or {}
851+
if not isinstance(t, dict) or not t.get('name'):
852+
continue
853+
cp = t.get('customParameters')
854+
if isinstance(cp, dict) and cp:
855+
parsed[t['name']] = cp
846856
if parsed:
847857
tool_custom_parameters = parsed
848858

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

Lines changed: 99 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,58 @@
44

55
from ldai.tracker import LDAIConfigTracker
66

7+
# Type alias for tool custom parameters: maps tool name -> custom params dict
8+
ToolCustomParametersMap = Dict[str, Dict[str, Any]]
9+
10+
11+
def _get_tool_custom_parameter(
12+
tool_custom_parameters: Optional['ToolCustomParametersMap'],
13+
tool_name: str,
14+
key: str,
15+
) -> Any:
16+
"""Retrieve a custom parameter for a specific tool.
17+
18+
:param tool_custom_parameters: The tool custom parameters map.
19+
:param tool_name: The name of the tool.
20+
:param key: The custom parameter key to look up.
21+
:return: The parameter value, or None if not found.
22+
"""
23+
if tool_custom_parameters is None:
24+
return None
25+
tool_params = tool_custom_parameters.get(tool_name)
26+
if tool_params is None:
27+
return None
28+
return tool_params.get(key)
29+
30+
31+
def _serialize_tool_custom_parameters(
32+
result: Dict[str, Any],
33+
tool_custom_parameters: Optional['ToolCustomParametersMap'],
34+
) -> None:
35+
"""Serialize tool_custom_parameters into the result dict.
36+
37+
Injects tools into ``result['model']['parameters']['tools']``.
38+
Iteration order follows dict insertion order (Python 3.7+); if the
39+
original flag payload order matters, the caller must ensure it is
40+
preserved at parse time.
41+
42+
:param result: The mutable dict being built by to_dict().
43+
:param tool_custom_parameters: The tool custom parameters map.
44+
"""
45+
if tool_custom_parameters is None:
46+
return
47+
model = result.get('model') or {}
48+
params = model.get('parameters') or {}
49+
tools_list = []
50+
for name, custom_params in tool_custom_parameters.items():
51+
tool_entry: Dict[str, Any] = {'name': name}
52+
if custom_params:
53+
tool_entry['customParameters'] = custom_params
54+
tools_list.append(tool_entry)
55+
params['tools'] = tools_list
56+
model['parameters'] = params
57+
result['model'] = model
58+
759

860
@dataclass
961
class LDMessage:
@@ -208,43 +260,33 @@ class AICompletionConfigDefault(AIConfigDefault):
208260
"""
209261
messages: Optional[List[LDMessage]] = None
210262
judge_configuration: Optional[JudgeConfiguration] = None
211-
tool_custom_parameters: Optional[Dict[str, Dict[str, Any]]] = None
263+
tool_custom_parameters: Optional[ToolCustomParametersMap] = None
212264

213-
def get_tool_custom_parameter(self, tool_name: str, key: str) -> Any:
214-
"""
215-
Retrieve a custom parameter for a specific tool.
216-
217-
:param tool_name: The name of the tool.
218-
:param key: The custom parameter key to look up.
219-
:return: The parameter value, or None if not found.
220-
"""
221-
if self.tool_custom_parameters is None:
222-
return None
223-
tool_params = self.tool_custom_parameters.get(tool_name)
224-
if tool_params is None:
225-
return None
226-
return tool_params.get(key)
265+
def get_tool_custom_parameter(
266+
self, tool_name: str, key: str,
267+
) -> Any:
268+
"""Retrieve a custom parameter for a specific tool."""
269+
return _get_tool_custom_parameter(
270+
self.tool_custom_parameters, tool_name, key,
271+
)
227272

228273
def to_dict(self) -> dict:
229-
"""
230-
Render the given default values as an AICompletionConfigDefault-compatible dictionary object.
274+
"""Render the given default values as an AICompletionConfigDefault-compatible dictionary object.
275+
276+
Note: tool_custom_parameters are serialized into
277+
``model.parameters.tools`` so that they are carried inside the
278+
variation dict used as the evaluation fallback. This is how
279+
completion-config defaults reach ``_completion_config`` — there
280+
is no separate ``or default.tool_custom_parameters`` fallback
281+
like agent configs have.
231282
"""
232283
result = self._base_to_dict()
233284
result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None
234285
if self.judge_configuration is not None:
235286
result['judgeConfiguration'] = self.judge_configuration.to_dict()
236-
if self.tool_custom_parameters is not None:
237-
model = result.get('model') or {}
238-
params = model.get('parameters') or {}
239-
tools_list = []
240-
for name, custom_params in self.tool_custom_parameters.items():
241-
tool_entry: Dict[str, Any] = {'name': name}
242-
if custom_params:
243-
tool_entry['customParameters'] = custom_params
244-
tools_list.append(tool_entry)
245-
params['tools'] = tools_list
246-
model['parameters'] = params
247-
result['model'] = model
287+
_serialize_tool_custom_parameters(
288+
result, self.tool_custom_parameters,
289+
)
248290
return result
249291

250292

@@ -255,22 +297,15 @@ class AICompletionConfig(AIConfig):
255297
"""
256298
messages: Optional[List[LDMessage]] = None
257299
judge_configuration: Optional[JudgeConfiguration] = None
258-
tool_custom_parameters: Optional[Dict[str, Dict[str, Any]]] = None
300+
tool_custom_parameters: Optional[ToolCustomParametersMap] = None
259301

260-
def get_tool_custom_parameter(self, tool_name: str, key: str) -> Any:
261-
"""
262-
Retrieve a custom parameter for a specific tool.
263-
264-
:param tool_name: The name of the tool.
265-
:param key: The custom parameter key to look up.
266-
:return: The parameter value, or None if not found.
267-
"""
268-
if self.tool_custom_parameters is None:
269-
return None
270-
tool_params = self.tool_custom_parameters.get(tool_name)
271-
if tool_params is None:
272-
return None
273-
return tool_params.get(key)
302+
def get_tool_custom_parameter(
303+
self, tool_name: str, key: str,
304+
) -> Any:
305+
"""Retrieve a custom parameter for a specific tool."""
306+
return _get_tool_custom_parameter(
307+
self.tool_custom_parameters, tool_name, key,
308+
)
274309

275310
def to_dict(self) -> dict:
276311
"""
@@ -294,22 +329,15 @@ class AIAgentConfigDefault(AIConfigDefault):
294329
"""
295330
instructions: Optional[str] = None
296331
judge_configuration: Optional[JudgeConfiguration] = None
297-
tool_custom_parameters: Optional[Dict[str, Dict[str, Any]]] = None
332+
tool_custom_parameters: Optional[ToolCustomParametersMap] = None
298333

299-
def get_tool_custom_parameter(self, tool_name: str, key: str) -> Any:
300-
"""
301-
Retrieve a custom parameter for a specific tool.
302-
303-
:param tool_name: The name of the tool.
304-
:param key: The custom parameter key to look up.
305-
:return: The parameter value, or None if not found.
306-
"""
307-
if self.tool_custom_parameters is None:
308-
return None
309-
tool_params = self.tool_custom_parameters.get(tool_name)
310-
if tool_params is None:
311-
return None
312-
return tool_params.get(key)
334+
def get_tool_custom_parameter(
335+
self, tool_name: str, key: str,
336+
) -> Any:
337+
"""Retrieve a custom parameter for a specific tool."""
338+
return _get_tool_custom_parameter(
339+
self.tool_custom_parameters, tool_name, key,
340+
)
313341

314342
def to_dict(self) -> Dict[str, Any]:
315343
"""
@@ -320,18 +348,9 @@ def to_dict(self) -> Dict[str, Any]:
320348
result['instructions'] = self.instructions
321349
if self.judge_configuration is not None:
322350
result['judgeConfiguration'] = self.judge_configuration.to_dict()
323-
if self.tool_custom_parameters is not None:
324-
model = result.get('model') or {}
325-
params = model.get('parameters') or {}
326-
tools_list = []
327-
for name, custom_params in self.tool_custom_parameters.items():
328-
tool_entry: Dict[str, Any] = {'name': name}
329-
if custom_params:
330-
tool_entry['customParameters'] = custom_params
331-
tools_list.append(tool_entry)
332-
params['tools'] = tools_list
333-
model['parameters'] = params
334-
result['model'] = model
351+
_serialize_tool_custom_parameters(
352+
result, self.tool_custom_parameters,
353+
)
335354
return result
336355

337356

@@ -342,22 +361,15 @@ class AIAgentConfig(AIConfig):
342361
"""
343362
instructions: Optional[str] = None
344363
judge_configuration: Optional[JudgeConfiguration] = None
345-
tool_custom_parameters: Optional[Dict[str, Dict[str, Any]]] = None
346-
347-
def get_tool_custom_parameter(self, tool_name: str, key: str) -> Any:
348-
"""
349-
Retrieve a custom parameter for a specific tool.
350-
351-
:param tool_name: The name of the tool.
352-
:param key: The custom parameter key to look up.
353-
:return: The parameter value, or None if not found.
354-
"""
355-
if self.tool_custom_parameters is None:
356-
return None
357-
tool_params = self.tool_custom_parameters.get(tool_name)
358-
if tool_params is None:
359-
return None
360-
return tool_params.get(key)
364+
tool_custom_parameters: Optional[ToolCustomParametersMap] = None
365+
366+
def get_tool_custom_parameter(
367+
self, tool_name: str, key: str,
368+
) -> Any:
369+
"""Retrieve a custom parameter for a specific tool."""
370+
return _get_tool_custom_parameter(
371+
self.tool_custom_parameters, tool_name, key,
372+
)
361373

362374
def to_dict(self) -> Dict[str, Any]:
363375
"""

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,15 @@ def test_agent_config_has_tools(ldai_client: LDAIClient):
397397

398398
assert agent.enabled is True
399399
assert agent.tool_custom_parameters is not None
400-
assert len(agent.tool_custom_parameters) == 3
400+
# Only tools with non-empty customParameters are included
401+
assert len(agent.tool_custom_parameters) == 2
401402

402403
assert agent.get_tool_custom_parameter('get-order', 'includeHistory') is True
403404
assert agent.get_tool_custom_parameter('get-order', 'maxItems') == 5
404405
assert agent.get_tool_custom_parameter('search-products', 'category') == 'electronics'
406+
# 'send-email' has no customParameters, so it is not in the map
407+
assert 'send-email' not in agent.tool_custom_parameters
405408
assert agent.get_tool_custom_parameter('send-email', 'anything') is None
406-
assert 'send-email' in agent.tool_custom_parameters
407409

408410

409411
def test_agent_config_tools_fallback_to_default(ldai_client: LDAIClient):

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,15 @@ def test_completion_config_has_tools(ldai_client: LDAIClient):
454454
config = ldai_client.completion_config('config-with-tools', context, default)
455455

456456
assert config.tool_custom_parameters is not None
457-
assert len(config.tool_custom_parameters) == 3
457+
# Only tools with non-empty customParameters are included
458+
assert len(config.tool_custom_parameters) == 2
458459

459460
assert config.get_tool_custom_parameter('web_search', 'maxResults') == 10
460461
assert config.get_tool_custom_parameter('web_search', 'region') == 'us'
461462
assert config.get_tool_custom_parameter('get_weather', 'units') == 'celsius'
463+
# 'calculator' has no customParameters, so it is not in the map
464+
assert 'calculator' not in config.tool_custom_parameters
462465
assert config.get_tool_custom_parameter('calculator', 'anything') is None
463-
assert 'calculator' in config.tool_custom_parameters
464466

465467

466468
def test_completion_config_no_tools(ldai_client: LDAIClient):

0 commit comments

Comments
 (0)