From 6661d5f00d839f08f7e44a483514fb5ed5b2624c Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 20 Apr 2026 12:53:04 -0500 Subject: [PATCH 1/8] feat: add root-level tools map with customParameters to AI Config types Adds AITool dataclass and tools map (keyed by tool name) to completion and agent config types. The root-level tools map is distinct from model.parameters.tools[] which remains passable to LLM providers as-is. Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 1 + packages/sdk/server-ai/src/ldai/client.py | 29 ++++- packages/sdk/server-ai/src/ldai/models.py | 34 +++++ packages/sdk/server-ai/tests/test_tools.py | 137 ++++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/server-ai/tests/test_tools.py diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index a3557d8..e2bbabc 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -20,6 +20,7 @@ AIConfig, AIJudgeConfig, AIJudgeConfigDefault, + AITool, Edge, JudgeConfiguration, LDAIAgent, diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index ac13469..38baa62 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -21,6 +21,7 @@ AICompletionConfigDefault, AIJudgeConfig, AIJudgeConfigDefault, + AITool, Edge, JudgeConfiguration, LDMessage, @@ -50,6 +51,22 @@ _DISABLED_JUDGE_DEFAULT = AIJudgeConfigDefault.disabled() +def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, AITool]]: + """Parse the root-level tools map from a flag variation dict.""" + if not tools_data or not isinstance(tools_data, dict): + return None + result = {} + for tool_name, tool_dict in tools_data.items(): + if isinstance(tool_dict, dict): + result[tool_name] = AITool( + name=tool_dict.get('name', tool_name), + type=tool_dict.get('type'), + parameters=tool_dict.get('parameters'), + custom_parameters=tool_dict.get('customParameters'), + ) + return result or None + + class LDAIClient: """The LaunchDarkly AI SDK client object.""" @@ -89,10 +106,13 @@ def _completion_config( variables: Optional[Dict[str, Any]] = None, ) -> AICompletionConfig: (model, provider, messages, instructions, - tracker_factory, enabled, judge_configuration, _) = self.__evaluate( + tracker_factory, enabled, judge_configuration, variation) = self.__evaluate( key, context, default.to_dict(), variables ) + tools_data = variation.get('tools') + tools = _parse_tools(tools_data) if tools_data is not None else default.tools + config = AICompletionConfig( key=key, enabled=bool(enabled), @@ -101,6 +121,7 @@ def _completion_config( provider=provider, create_tracker=tracker_factory, judge_configuration=judge_configuration, + tools=tools, ) return config @@ -891,13 +912,16 @@ def __evaluate_agent( :return: Configured AIAgentConfig instance. """ (model, provider, messages, instructions, - tracker_factory, enabled, judge_configuration, _) = self.__evaluate( + tracker_factory, enabled, judge_configuration, variation) = self.__evaluate( key, context, default.to_dict(), variables, graph_key=graph_key ) # For agents, prioritize instructions over messages final_instructions = instructions if instructions is not None else default.instructions + tools_data = variation.get('tools') + tools = _parse_tools(tools_data) if tools_data is not None else default.tools + return AIAgentConfig( key=key, enabled=bool(enabled) if enabled is not None else (default.enabled or False), @@ -906,6 +930,7 @@ def __evaluate_agent( instructions=final_instructions, create_tracker=tracker_factory, judge_configuration=judge_configuration or default.judge_configuration, + tools=tools, ) def __interpolate_template(self, template: str, variables: Dict[str, Any]) -> str: diff --git a/packages/sdk/server-ai/src/ldai/models.py b/packages/sdk/server-ai/src/ldai/models.py index cf5da1f..d1310ae 100644 --- a/packages/sdk/server-ai/src/ldai/models.py +++ b/packages/sdk/server-ai/src/ldai/models.py @@ -5,6 +5,28 @@ from typing_extensions import Self +@dataclass(frozen=True) +class AITool: + """ + A single tool entry from the root-level tools map in an AI Config flag variation. + Distinct from model.parameters.tools[] which is the raw array passed to LLM providers. + """ + name: str + type: Optional[str] = None + parameters: Optional[Dict[str, Any]] = None + custom_parameters: Optional[Dict[str, Any]] = None + + def to_dict(self) -> dict: + result: Dict[str, Any] = {'name': self.name} + if self.type is not None: + result['type'] = self.type + if self.parameters is not None: + result['parameters'] = self.parameters + if self.custom_parameters is not None: + result['customParameters'] = self.custom_parameters # camelCase in wire format + return result + + @dataclass class LDMessage: role: Literal['system', 'user', 'assistant'] @@ -208,6 +230,7 @@ class AICompletionConfigDefault(AIConfigDefault): """ messages: Optional[List[LDMessage]] = None judge_configuration: Optional[JudgeConfiguration] = None + tools: Optional[Dict[str, 'AITool']] = None def to_dict(self) -> dict: """ @@ -217,6 +240,8 @@ def to_dict(self) -> dict: result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None if self.judge_configuration is not None: result['judgeConfiguration'] = self.judge_configuration.to_dict() + if self.tools is not None: + result['tools'] = {k: v.to_dict() for k, v in self.tools.items()} return result @@ -227,6 +252,7 @@ class AICompletionConfig(AIConfig): """ messages: Optional[List[LDMessage]] = None judge_configuration: Optional[JudgeConfiguration] = None + tools: Optional[Dict[str, 'AITool']] = None def to_dict(self) -> dict: """ @@ -236,6 +262,8 @@ def to_dict(self) -> dict: result['messages'] = [message.to_dict() for message in self.messages] if self.messages else None if self.judge_configuration is not None: result['judgeConfiguration'] = self.judge_configuration.to_dict() + if self.tools is not None: + result['tools'] = {k: v.to_dict() for k, v in self.tools.items()} return result @@ -250,6 +278,7 @@ class AIAgentConfigDefault(AIConfigDefault): """ instructions: Optional[str] = None judge_configuration: Optional[JudgeConfiguration] = None + tools: Optional[Dict[str, 'AITool']] = None def to_dict(self) -> Dict[str, Any]: """ @@ -260,6 +289,8 @@ def to_dict(self) -> Dict[str, Any]: result['instructions'] = self.instructions if self.judge_configuration is not None: result['judgeConfiguration'] = self.judge_configuration.to_dict() + if self.tools is not None: + result['tools'] = {k: v.to_dict() for k, v in self.tools.items()} return result @@ -270,6 +301,7 @@ class AIAgentConfig(AIConfig): """ instructions: Optional[str] = None judge_configuration: Optional[JudgeConfiguration] = None + tools: Optional[Dict[str, 'AITool']] = None def to_dict(self) -> Dict[str, Any]: """ @@ -280,6 +312,8 @@ def to_dict(self) -> Dict[str, Any]: result['instructions'] = self.instructions if self.judge_configuration is not None: result['judgeConfiguration'] = self.judge_configuration.to_dict() + if self.tools is not None: + result['tools'] = {k: v.to_dict() for k, v in self.tools.items()} return result diff --git a/packages/sdk/server-ai/tests/test_tools.py b/packages/sdk/server-ai/tests/test_tools.py new file mode 100644 index 0000000..fc23afe --- /dev/null +++ b/packages/sdk/server-ai/tests/test_tools.py @@ -0,0 +1,137 @@ +import pytest +from ldclient import Config, Context, LDClient +from ldclient.integrations.test_data import TestData + +from ldai import AITool, LDAIClient +from ldai.models import AIAgentConfigDefault, AICompletionConfigDefault + + +@pytest.fixture +def td() -> TestData: + td = TestData.data_source() + td.update( + td.flag('completion-with-tools') + .variations( + { + 'model': {'name': 'gpt-5', 'parameters': {'temperature': 0.7}}, + 'messages': [{'role': 'user', 'content': 'Hello'}], + 'tools': { + 'web-search-tool': { + 'name': 'web-search-tool', + 'type': 'function', + 'parameters': {'type': 'object', 'properties': {}, 'required': []}, + 'customParameters': {'some-custom-parameter': 'some-custom-value'}, + } + }, + '_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1}, + }, + ) + .variation_for_all(0) + ) + + td.update( + td.flag('completion-no-tools') + .variations( + { + 'model': {'name': 'gpt-5'}, + 'messages': [{'role': 'user', 'content': 'Hello'}], + '_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1}, + }, + ) + .variation_for_all(0) + ) + + td.update( + td.flag('agent-with-tools') + .variations( + { + 'model': {'name': 'gpt-5'}, + 'instructions': 'You are a helpful agent.', + 'tools': { + 'search-tool': { + 'name': 'search-tool', + 'type': 'function', + 'customParameters': {'maxResults': 10}, + } + }, + '_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1, 'mode': 'agent'}, + }, + ) + .variation_for_all(0) + ) + + return td + + +@pytest.fixture +def client(td) -> LDAIClient: + config = Config('fake-sdk-key', update_processor_class=td, send_events=False) + ld_client = LDClient(config=config) + return LDAIClient(ld_client) + + +@pytest.fixture +def context() -> Context: + return Context.builder('test-user').name('Test User').build() + + +def test_completion_config_includes_tools_from_variation(client, context): + result = client.completion_config('completion-with-tools', context, AICompletionConfigDefault()) + + assert result.tools is not None + assert 'web-search-tool' in result.tools + tool = result.tools['web-search-tool'] + assert tool.name == 'web-search-tool' + assert tool.type == 'function' + assert tool.custom_parameters == {'some-custom-parameter': 'some-custom-value'} + + +def test_completion_config_tools_none_when_not_in_variation(client, context): + result = client.completion_config('completion-no-tools', context, AICompletionConfigDefault()) + + assert result.tools is None + + +def test_completion_config_falls_back_to_default_tools(client, context): + default_tool = AITool(name='default-tool', type='function', custom_parameters={'priority': 'high'}) + default = AICompletionConfigDefault(tools={'default-tool': default_tool}) + + result = client.completion_config('completion-no-tools', context, default) + + assert result.tools is not None + assert 'default-tool' in result.tools + assert result.tools['default-tool'].custom_parameters == {'priority': 'high'} + + +def test_agent_config_includes_tools_from_variation(client, context): + result = client.agent_config('agent-with-tools', context, AIAgentConfigDefault()) + + assert result.tools is not None + assert 'search-tool' in result.tools + tool = result.tools['search-tool'] + assert tool.name == 'search-tool' + assert tool.custom_parameters == {'maxResults': 10} + + +def test_aitool_to_dict_serializes_custom_parameters_as_camel_case(): + tool = AITool( + name='my-tool', + type='function', + parameters={'type': 'object'}, + custom_parameters={'someKey': 'someValue'}, + ) + d = tool.to_dict() + + assert d['name'] == 'my-tool' + assert d['type'] == 'function' + assert d['parameters'] == {'type': 'object'} + assert 'customParameters' in d + assert d['customParameters'] == {'someKey': 'someValue'} + assert 'custom_parameters' not in d + + +def test_aitool_to_dict_omits_none_fields(): + tool = AITool(name='bare-tool') + d = tool.to_dict() + + assert d == {'name': 'bare-tool'} From 36c6af77ac3c01422a7ca57e1e9d132e51d4af13 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Fri, 24 Apr 2026 17:26:36 -0500 Subject: [PATCH 2/8] fix: add AITool to __all__ in __init__.py Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index e2bbabc..8ecdf36 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -65,6 +65,7 @@ 'Judge', 'JudgeConfiguration', 'JudgeResult', + 'AITool', 'LDMessage', 'ModelConfig', 'ProviderConfig', From 3080987947048d08558d6dc0c19ec8c744bceaed Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 09:09:12 -0500 Subject: [PATCH 3/8] refactor: rename AITool to LDTool for cross-SDK naming consistency Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 4 ++-- packages/sdk/server-ai/src/ldai/client.py | 6 +++--- packages/sdk/server-ai/src/ldai/models.py | 10 +++++----- packages/sdk/server-ai/tests/test_tools.py | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index 8ecdf36..e55b6e4 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -20,7 +20,7 @@ AIConfig, AIJudgeConfig, AIJudgeConfigDefault, - AITool, + LDTool, Edge, JudgeConfiguration, LDAIAgent, @@ -65,7 +65,7 @@ 'Judge', 'JudgeConfiguration', 'JudgeResult', - 'AITool', + 'LDTool', 'LDMessage', 'ModelConfig', 'ProviderConfig', diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 38baa62..1fe1281 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -21,7 +21,7 @@ AICompletionConfigDefault, AIJudgeConfig, AIJudgeConfigDefault, - AITool, + LDTool, Edge, JudgeConfiguration, LDMessage, @@ -51,14 +51,14 @@ _DISABLED_JUDGE_DEFAULT = AIJudgeConfigDefault.disabled() -def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, AITool]]: +def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDTool]]: """Parse the root-level tools map from a flag variation dict.""" if not tools_data or not isinstance(tools_data, dict): return None result = {} for tool_name, tool_dict in tools_data.items(): if isinstance(tool_dict, dict): - result[tool_name] = AITool( + result[tool_name] = LDTool( name=tool_dict.get('name', tool_name), type=tool_dict.get('type'), parameters=tool_dict.get('parameters'), diff --git a/packages/sdk/server-ai/src/ldai/models.py b/packages/sdk/server-ai/src/ldai/models.py index d1310ae..afc0215 100644 --- a/packages/sdk/server-ai/src/ldai/models.py +++ b/packages/sdk/server-ai/src/ldai/models.py @@ -6,7 +6,7 @@ @dataclass(frozen=True) -class AITool: +class LDTool: """ A single tool entry from the root-level tools map in an AI Config flag variation. Distinct from model.parameters.tools[] which is the raw array passed to LLM providers. @@ -230,7 +230,7 @@ class AICompletionConfigDefault(AIConfigDefault): """ messages: Optional[List[LDMessage]] = None judge_configuration: Optional[JudgeConfiguration] = None - tools: Optional[Dict[str, 'AITool']] = None + tools: Optional[Dict[str, 'LDTool']] = None def to_dict(self) -> dict: """ @@ -252,7 +252,7 @@ class AICompletionConfig(AIConfig): """ messages: Optional[List[LDMessage]] = None judge_configuration: Optional[JudgeConfiguration] = None - tools: Optional[Dict[str, 'AITool']] = None + tools: Optional[Dict[str, 'LDTool']] = None def to_dict(self) -> dict: """ @@ -278,7 +278,7 @@ class AIAgentConfigDefault(AIConfigDefault): """ instructions: Optional[str] = None judge_configuration: Optional[JudgeConfiguration] = None - tools: Optional[Dict[str, 'AITool']] = None + tools: Optional[Dict[str, 'LDTool']] = None def to_dict(self) -> Dict[str, Any]: """ @@ -301,7 +301,7 @@ class AIAgentConfig(AIConfig): """ instructions: Optional[str] = None judge_configuration: Optional[JudgeConfiguration] = None - tools: Optional[Dict[str, 'AITool']] = None + tools: Optional[Dict[str, 'LDTool']] = None def to_dict(self) -> Dict[str, Any]: """ diff --git a/packages/sdk/server-ai/tests/test_tools.py b/packages/sdk/server-ai/tests/test_tools.py index fc23afe..36a7dfc 100644 --- a/packages/sdk/server-ai/tests/test_tools.py +++ b/packages/sdk/server-ai/tests/test_tools.py @@ -2,7 +2,7 @@ from ldclient import Config, Context, LDClient from ldclient.integrations.test_data import TestData -from ldai import AITool, LDAIClient +from ldai import LDTool, LDAIClient from ldai.models import AIAgentConfigDefault, AICompletionConfigDefault @@ -93,7 +93,7 @@ def test_completion_config_tools_none_when_not_in_variation(client, context): def test_completion_config_falls_back_to_default_tools(client, context): - default_tool = AITool(name='default-tool', type='function', custom_parameters={'priority': 'high'}) + default_tool = LDTool(name='default-tool', type='function', custom_parameters={'priority': 'high'}) default = AICompletionConfigDefault(tools={'default-tool': default_tool}) result = client.completion_config('completion-no-tools', context, default) @@ -114,7 +114,7 @@ def test_agent_config_includes_tools_from_variation(client, context): def test_aitool_to_dict_serializes_custom_parameters_as_camel_case(): - tool = AITool( + tool = LDTool( name='my-tool', type='function', parameters={'type': 'object'}, @@ -131,7 +131,7 @@ def test_aitool_to_dict_serializes_custom_parameters_as_camel_case(): def test_aitool_to_dict_omits_none_fields(): - tool = AITool(name='bare-tool') + tool = LDTool(name='bare-tool') d = tool.to_dict() assert d == {'name': 'bare-tool'} From 4ba1e930c4f7af72d090c356ba4f96ff180e000c Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 09:13:39 -0500 Subject: [PATCH 4/8] fix: simplify _parse_tools guard and warn on malformed tool entries Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 1fe1281..1a9c452 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -53,7 +53,7 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDTool]]: """Parse the root-level tools map from a flag variation dict.""" - if not tools_data or not isinstance(tools_data, dict): + if not isinstance(tools_data, dict): return None result = {} for tool_name, tool_dict in tools_data.items(): @@ -64,6 +64,8 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDT parameters=tool_dict.get('parameters'), custom_parameters=tool_dict.get('customParameters'), ) + else: + log.warning('Skipping tool "%s": expected a dict, got %s', tool_name, type(tool_dict).__name__) return result or None From 5a9fd22b542cbc285c02be092d0556fdf2c8b953 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 09:15:58 -0500 Subject: [PATCH 5/8] refactor: use early continue in _parse_tools loop Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 1a9c452..519b313 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -57,15 +57,15 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDT return None result = {} for tool_name, tool_dict in tools_data.items(): - if isinstance(tool_dict, dict): - result[tool_name] = LDTool( - name=tool_dict.get('name', tool_name), - type=tool_dict.get('type'), - parameters=tool_dict.get('parameters'), - custom_parameters=tool_dict.get('customParameters'), - ) - else: + if not isinstance(tool_dict, dict): log.warning('Skipping tool "%s": expected a dict, got %s', tool_name, type(tool_dict).__name__) + continue + result[tool_name] = LDTool( + name=tool_dict.get('name', tool_name), + type=tool_dict.get('type'), + parameters=tool_dict.get('parameters'), + custom_parameters=tool_dict.get('customParameters'), + ) return result or None From ccc744ecaa5004c3a270fb9bee29b3a436d5af55 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 09:26:00 -0500 Subject: [PATCH 6/8] fix: sort LDTool import alphabetically to satisfy isort Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 2 +- packages/sdk/server-ai/src/ldai/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index e55b6e4..b7d8752 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -20,13 +20,13 @@ AIConfig, AIJudgeConfig, AIJudgeConfigDefault, - LDTool, Edge, JudgeConfiguration, LDAIAgent, LDAIAgentConfig, LDAIAgentDefaults, LDMessage, + LDTool, ModelConfig, ProviderConfig, ) diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 519b313..2525d9c 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -21,10 +21,10 @@ AICompletionConfigDefault, AIJudgeConfig, AIJudgeConfigDefault, - LDTool, Edge, JudgeConfiguration, LDMessage, + LDTool, ModelConfig, ProviderConfig, ) From 19a2b2ce1e5f074ccc57d7e0f744f56dde37ac96 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 09:29:59 -0500 Subject: [PATCH 7/8] fix: remove incorrect default.tools fallback from _parse_tools call sites The variation call already incorporates the default via default_dict; applying a second-level default here is incorrect. Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/client.py | 6 ++---- packages/sdk/server-ai/tests/test_tools.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 2525d9c..ec7cad4 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -112,8 +112,7 @@ def _completion_config( key, context, default.to_dict(), variables ) - tools_data = variation.get('tools') - tools = _parse_tools(tools_data) if tools_data is not None else default.tools + tools = _parse_tools(variation.get('tools')) config = AICompletionConfig( key=key, @@ -921,8 +920,7 @@ def __evaluate_agent( # For agents, prioritize instructions over messages final_instructions = instructions if instructions is not None else default.instructions - tools_data = variation.get('tools') - tools = _parse_tools(tools_data) if tools_data is not None else default.tools + tools = _parse_tools(variation.get('tools')) return AIAgentConfig( key=key, diff --git a/packages/sdk/server-ai/tests/test_tools.py b/packages/sdk/server-ai/tests/test_tools.py index 36a7dfc..1c74215 100644 --- a/packages/sdk/server-ai/tests/test_tools.py +++ b/packages/sdk/server-ai/tests/test_tools.py @@ -92,15 +92,13 @@ def test_completion_config_tools_none_when_not_in_variation(client, context): assert result.tools is None -def test_completion_config_falls_back_to_default_tools(client, context): +def test_completion_config_tools_none_when_variation_has_no_tools(client, context): default_tool = LDTool(name='default-tool', type='function', custom_parameters={'priority': 'high'}) default = AICompletionConfigDefault(tools={'default-tool': default_tool}) result = client.completion_config('completion-no-tools', context, default) - assert result.tools is not None - assert 'default-tool' in result.tools - assert result.tools['default-tool'].custom_parameters == {'priority': 'high'} + assert result.tools is None def test_agent_config_includes_tools_from_variation(client, context): From 7c522aeb95efba68a357e5108ccd433321484321 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 27 Apr 2026 10:02:30 -0500 Subject: [PATCH 8/8] feat: add description field to LDTool Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/client.py | 1 + packages/sdk/server-ai/src/ldai/models.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index ec7cad4..4023574 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -62,6 +62,7 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDT continue result[tool_name] = LDTool( name=tool_dict.get('name', tool_name), + description=tool_dict.get('description'), type=tool_dict.get('type'), parameters=tool_dict.get('parameters'), custom_parameters=tool_dict.get('customParameters'), diff --git a/packages/sdk/server-ai/src/ldai/models.py b/packages/sdk/server-ai/src/ldai/models.py index afc0215..28d8cef 100644 --- a/packages/sdk/server-ai/src/ldai/models.py +++ b/packages/sdk/server-ai/src/ldai/models.py @@ -12,12 +12,15 @@ class LDTool: Distinct from model.parameters.tools[] which is the raw array passed to LLM providers. """ name: str + description: Optional[str] = None type: Optional[str] = None parameters: Optional[Dict[str, Any]] = None custom_parameters: Optional[Dict[str, Any]] = None def to_dict(self) -> dict: result: Dict[str, Any] = {'name': self.name} + if self.description is not None: + result['description'] = self.description if self.type is not None: result['type'] = self.type if self.parameters is not None: