From 97aa9f4580ba53aaca40938ff4525c1fe54800e8 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 30 Jun 2025 15:54:36 -0400 Subject: [PATCH 01/11] feat: Added agent support to SDK --- ldai/client.py | 182 ++++++++++++++++++- ldai/testing/test_agents.py | 345 ++++++++++++++++++++++++++++++++++++ 2 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 ldai/testing/test_agents.py diff --git a/ldai/client.py b/ldai/client.py index e8b3c1f..a0f227f 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -125,6 +125,69 @@ def to_dict(self) -> dict: } +@dataclass(frozen=True) +class LDAIAgent: + """ + Represents an AI agent configuration with instructions and model settings. + + An agent is similar to an AIConfig but focuses on instructions rather than messages, + making it suitable for AI assistant/agent use cases. + """ + enabled: Optional[bool] = None + model: Optional[ModelConfig] = None + provider: Optional[ProviderConfig] = None + instructions: Optional[str] = None + tracker: Optional[LDAIConfigTracker] = None + + def to_dict(self) -> dict: + """ + Render the given agent as a dictionary object. + """ + result = { + '_ldMeta': { + 'enabled': self.enabled or False, + }, + 'model': self.model.to_dict() if self.model else None, + 'provider': self.provider.to_dict() if self.provider else None, + } + if self.instructions is not None: + result['instructions'] = self.instructions + return result + + +@dataclass(frozen=True) +class LDAIAgentDefaults: + """ + Default values for AI agent configurations. + + Similar to LDAIAgent but without tracker and with optional enabled field, + used as fallback values when agent configurations are not available. + """ + enabled: Optional[bool] = None + model: Optional[ModelConfig] = None + provider: Optional[ProviderConfig] = None + instructions: Optional[str] = None + + def to_dict(self) -> dict: + """ + Render the given agent defaults as a dictionary object. + """ + result = { + '_ldMeta': { + 'enabled': self.enabled or False, + }, + 'model': self.model.to_dict() if self.model else None, + 'provider': self.provider.to_dict() if self.provider else None, + } + if self.instructions is not None: + result['instructions'] = self.instructions + return result + + +# Type alias for multiple agents +LDAIAgents = Dict[str, LDAIAgent] + + class LDAIClient: """The LaunchDarkly AI SDK client object.""" @@ -147,13 +210,88 @@ def config( :param variables: Additional variables for the model configuration. :return: The value of the model configuration along with a tracker used for gathering metrics. """ - variation = self._client.variation(key, context, default_value.to_dict()) + model, provider, messages, tracker, enabled = self.__evaluate(key, context, default_value.to_dict(), variables) + + config = AIConfig( + enabled=bool(enabled), + model=model, + messages=messages, + provider=provider, + ) + + return config, tracker + + def agents( + self, + keys: List[str], + context: Context, + default_value: LDAIAgentDefaults, + variables: Optional[Dict[str, Any]] = None, + ) -> LDAIAgents: + """ + Get multiple AI agent configurations. + + This method allows you to retrieve multiple agent configurations in a single call, + with each agent having its instructions dynamically interpolated with the provided + variables and context data. + + Example: + ```python + agents = client.agents( + ['customer-support', 'sales-assistant'], + context, + LDAIAgentDefaults( + enabled=True, + model=ModelConfig('gpt-4'), + instructions="You are a helpful assistant." + ), + {'company_name': 'Acme Corp'} + ) + + support_agent = agents['customer-support'] + if support_agent.enabled: + print(support_agent.instructions) # Instructions with interpolated variables + # Use support_agent.tracker for metrics tracking + ``` + + :param keys: List of agent configuration keys to retrieve. + :param context: The context to evaluate the agent configurations in. + :param default_value: Default agent configuration values to use as fallback. + :param variables: Additional variables for template interpolation in instructions. + :return: Dictionary mapping agent keys to their LDAIAgent configurations. + """ + result: LDAIAgents = {} + + for key in keys: + agent = self.__evaluate_agent(key, context, default_value, variables) + result[key] = agent + + return result + + def __evaluate( + self, + key: str, + context: Context, + default_dict: Dict[str, Any], + variables: Optional[Dict[str, Any]] = None, + ) -> Tuple[Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]], Optional[str], LDAIConfigTracker]: + """ + Internal method to evaluate a configuration and extract components. + + :param key: The configuration key. + :param context: The evaluation context. + :param default_dict: Default configuration as dictionary. + :param variables: Variables for interpolation. + :return: Tuple of (model, provider, messages, instructions, tracker, enabled). + """ + variation = self._client.variation(key, context, default_dict) all_variables = {} if variables: all_variables.update(variables) all_variables['ldctx'] = context.to_dict() + # Extract messages messages = None if 'messages' in variation and isinstance(variation['messages'], list) and all( isinstance(entry, dict) for entry in variation['messages'] @@ -168,11 +306,18 @@ def config( for entry in variation['messages'] ] + # Extract instructions + instructions = None + if 'instructions' in variation and isinstance(variation['instructions'], str): + instructions = self.__interpolate_template(variation['instructions'], all_variables) + + # Extract provider config provider_config = None if 'provider' in variation and isinstance(variation['provider'], dict): provider = variation['provider'] provider_config = ProviderConfig(provider.get('name', '')) + # Extract model config model = None if 'model' in variation and isinstance(variation['model'], dict): parameters = variation['model'].get('parameters', None) @@ -183,6 +328,7 @@ def config( custom=custom ) + # Create tracker tracker = LDAIConfigTracker( self._client, variation.get('_ldMeta', {}).get('variationKey', ''), @@ -192,14 +338,36 @@ def config( ) enabled = variation.get('_ldMeta', {}).get('enabled', False) - config = AIConfig( - enabled=bool(enabled), - model=model, - messages=messages, - provider=provider_config, + + return model, provider_config, messages, instructions, tracker, enabled + + def __evaluate_agent( + self, + key: str, + context: Context, + default_value: LDAIAgentDefaults, + variables: Optional[Dict[str, Any]] = None, + ) -> LDAIAgent: + """ + Internal method to evaluate an agent configuration. + + :param key: The agent configuration key. + :param context: The evaluation context. + :param default_value: Default agent values. + :param variables: Variables for interpolation. + :return: Configured LDAIAgent instance. + """ + model, provider, instructions, tracker, enabled = self.__evaluate( + key, context, default_value.to_dict(), variables ) - return config, tracker + return LDAIAgent( + enabled=bool(enabled) if enabled is not None else None, + model=model or default_value.model, + provider=provider or default_value.provider, + instructions=instructions, + tracker=tracker, + ) def __interpolate_template(self, template: str, variables: Dict[str, Any]) -> str: """ diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py new file mode 100644 index 0000000..bbf26ec --- /dev/null +++ b/ldai/testing/test_agents.py @@ -0,0 +1,345 @@ +import pytest +from ldclient import Config, Context, LDClient +from ldclient.integrations.test_data import TestData + +from ldai.client import LDAIAgent, LDAIAgentDefaults, LDAIClient, ModelConfig, ProviderConfig + + +@pytest.fixture +def td() -> TestData: + td = TestData.data_source() + + # Single agent with instructions + td.update( + td.flag('customer-support-agent') + .variations( + { + 'model': {'name': 'gpt-4', 'parameters': {'temperature': 0.3, 'maxTokens': 2048}}, + 'provider': {'name': 'openai'}, + 'instructions': 'You are a helpful customer support agent for {{company_name}}. Always be polite and professional.', + '_ldMeta': {'enabled': True, 'variationKey': 'agent-v1', 'version': 1, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + + # Agent with context interpolation + td.update( + td.flag('personalized-agent') + .variations( + { + 'model': {'name': 'claude-3', 'parameters': {'temperature': 0.5}}, + 'instructions': 'Hello {{ldctx.name}}! I am your personal assistant. Your user key is {{ldctx.key}}.', + '_ldMeta': {'enabled': True, 'variationKey': 'personal-v1', 'version': 2, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + + # Agent with multi-context interpolation + td.update( + td.flag('multi-context-agent') + .variations( + { + 'model': {'name': 'gpt-3.5-turbo'}, + 'instructions': 'Welcome {{ldctx.user.name}} from {{ldctx.org.name}}! Your organization tier is {{ldctx.org.tier}}.', + '_ldMeta': {'enabled': True, 'variationKey': 'multi-v1', 'version': 1, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + + # Disabled agent + td.update( + td.flag('disabled-agent') + .variations( + { + 'model': {'name': 'gpt-4'}, + 'instructions': 'This agent is disabled.', + '_ldMeta': {'enabled': False, 'variationKey': 'disabled-v1', 'version': 1, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + + # Agent with minimal metadata + td.update( + td.flag('minimal-agent') + .variations( + { + 'instructions': 'Minimal agent configuration.', + '_ldMeta': {'enabled': True}, + } + ) + .variation_for_all(0) + ) + + # Sales assistant agent + td.update( + td.flag('sales-assistant') + .variations( + { + 'model': {'name': 'gpt-4', 'parameters': {'temperature': 0.7}}, + 'provider': {'name': 'openai'}, + 'instructions': 'You are a sales assistant for {{company_name}}. Help customers find the right products.', + '_ldMeta': {'enabled': True, 'variationKey': 'sales-v1', 'version': 1, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + + return td + + +@pytest.fixture +def client(td: TestData) -> LDClient: + config = Config('sdk-key', update_processor_class=td, send_events=False) + return LDClient(config=config) + + +@pytest.fixture +def ldai_client(client: LDClient) -> LDAIClient: + return LDAIClient(client) + + +def test_single_agent_basic_functionality(ldai_client: LDAIClient): + """Test basic agent retrieval and configuration.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults( + enabled=False, + model=ModelConfig('fallback-model'), + instructions="Default instructions" + ) + variables = {'company_name': 'Acme Corp'} + + agents = ldai_client.agents(['customer-support-agent'], context, defaults, variables) + + assert len(agents) == 1 + assert 'customer-support-agent' in agents + + agent = agents['customer-support-agent'] + assert agent.enabled is True + assert agent.model is not None + assert agent.model.name == 'gpt-4' + assert agent.model.get_parameter('temperature') == 0.3 + assert agent.model.get_parameter('maxTokens') == 2048 + assert agent.provider is not None + assert agent.provider.name == 'openai' + assert agent.instructions == 'You are a helpful customer support agent for Acme Corp. Always be polite and professional.' + assert agent.tracker is not None + + +def test_agent_instructions_interpolation(ldai_client: LDAIClient): + """Test that agent instructions are properly interpolated with variables.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + variables = {'company_name': 'TechStart Inc'} + + agents = ldai_client.agents(['customer-support-agent'], context, defaults, variables) + agent = agents['customer-support-agent'] + + expected_instructions = 'You are a helpful customer support agent for TechStart Inc. Always be polite and professional.' + assert agent.instructions == expected_instructions + + +def test_agent_context_interpolation(ldai_client: LDAIClient): + """Test that agent instructions can access context data via ldctx.""" + context = Context.builder('user-123').name('Alice').build() + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents(['personalized-agent'], context, defaults) + agent = agents['personalized-agent'] + + expected_instructions = 'Hello Alice! I am your personal assistant. Your user key is user-123.' + assert agent.instructions == expected_instructions + + +def test_agent_multi_context_interpolation(ldai_client: LDAIClient): + """Test agent instructions with multi-context interpolation.""" + user_context = Context.builder('user-key').name('Bob').build() + org_context = Context.builder('org-key').kind('org').name('LaunchDarkly').set('tier', 'Enterprise').build() + context = Context.multi_builder().add(user_context).add(org_context).build() + + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents(['multi-context-agent'], context, defaults) + agent = agents['multi-context-agent'] + + expected_instructions = 'Welcome Bob from LaunchDarkly! Your organization tier is Enterprise.' + assert agent.instructions == expected_instructions + + +def test_multiple_agents_retrieval(ldai_client: LDAIClient): + """Test retrieving multiple agents in a single call.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults( + enabled=False, + model=ModelConfig('fallback'), + instructions="Default" + ) + variables = {'company_name': 'MultiCorp'} + + agents = ldai_client.agents( + ['customer-support-agent', 'sales-assistant'], + context, + defaults, + variables + ) + + assert len(agents) == 2 + assert 'customer-support-agent' in agents + assert 'sales-assistant' in agents + + support_agent = agents['customer-support-agent'] + assert support_agent.enabled is True + assert 'MultiCorp' in support_agent.instructions + + sales_agent = agents['sales-assistant'] + assert sales_agent.enabled is True + assert 'MultiCorp' in sales_agent.instructions + assert sales_agent.model.get_parameter('temperature') == 0.7 + + +def test_disabled_agent(ldai_client: LDAIClient): + """Test that disabled agents are properly handled.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents(['disabled-agent'], context, defaults) + agent = agents['disabled-agent'] + + assert agent.enabled is False + assert agent.instructions == 'This agent is disabled.' + + +def test_agent_with_missing_metadata(ldai_client: LDAIClient): + """Test agent handling when metadata is minimal or missing.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults( + enabled=False, + model=ModelConfig('default-model'), + instructions="Default instructions" + ) + + agents = ldai_client.agents(['minimal-agent'], context, defaults) + agent = agents['minimal-agent'] + + assert agent.enabled is True # From flag + assert agent.instructions == 'Minimal agent configuration.' + assert agent.model == defaults.model # Falls back to default + assert agent.tracker is not None + + +def test_agent_uses_defaults_on_missing_flag(ldai_client: LDAIClient): + """Test that default values are used when agent flag doesn't exist.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults( + enabled=True, + model=ModelConfig('default-gpt', parameters={'temp': 0.5}), + provider=ProviderConfig('default-provider'), + instructions="You are a default assistant." + ) + + agents = ldai_client.agents(['non-existent-agent'], context, defaults) + agent = agents['non-existent-agent'] + + assert agent.enabled == defaults.enabled + assert agent.model.name == 'default-gpt' + assert agent.model.get_parameter('temp') == 0.5 + assert agent.provider.name == 'default-provider' + assert agent.instructions == defaults.instructions + # Tracker should still be created for non-existent flags + assert agent.tracker is not None + + +def test_agent_error_handling(ldai_client: LDAIClient): + """Test that agent errors are handled gracefully.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults( + enabled=True, + model=ModelConfig('fallback-model'), + instructions="Fallback instructions" + ) + + # Test with a mix of valid and invalid keys + agents = ldai_client.agents( + ['customer-support-agent', 'invalid-flag'], + context, + defaults + ) + + assert len(agents) == 2 + + # Valid agent should work normally + valid_agent = agents['customer-support-agent'] + assert valid_agent.enabled is True + assert valid_agent.tracker is not None + + # Invalid agent should use defaults but still be created + invalid_agent = agents['invalid-flag'] + assert invalid_agent.enabled == defaults.enabled + assert invalid_agent.model.name == 'fallback-model' + assert invalid_agent.instructions == defaults.instructions + + +def test_agent_no_variables_interpolation(ldai_client: LDAIClient): + """Test agent instructions with no variables provided.""" + context = Context.builder('user-456').name('Charlie').build() + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents(['personalized-agent'], context, defaults) + agent = agents['personalized-agent'] + + # Should still interpolate context but not variables + expected_instructions = 'Hello Charlie! I am your personal assistant. Your user key is user-456.' + assert agent.instructions == expected_instructions + + +def test_agent_empty_agent_list(ldai_client: LDAIClient): + """Test agents method with empty agent list.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents([], context, defaults) + + assert len(agents) == 0 + assert agents == {} + + +def test_agent_tracker_functionality(ldai_client: LDAIClient): + """Test that agent tracker works correctly.""" + context = Context.create('user-key') + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + + agents = ldai_client.agents(['customer-support-agent'], context, defaults) + agent = agents['customer-support-agent'] + + assert agent.tracker is not None + assert hasattr(agent.tracker, 'track_success') + assert hasattr(agent.tracker, 'track_duration') + assert hasattr(agent.tracker, 'track_tokens') + + # Test that tracker has correct metadata + track_data = agent.tracker._LDAIConfigTracker__get_track_data() + assert track_data['variationKey'] == 'agent-v1' + assert track_data['configKey'] == 'customer-support-agent' + assert track_data['version'] == 1 + + +def test_agents_backwards_compatibility_with_config(ldai_client: LDAIClient): + """Test that the existing config method still works after agent additions.""" + from ldai.client import AIConfig, LDMessage + + context = Context.create('user-key') + default_value = AIConfig( + enabled=True, + model=ModelConfig('test-model'), + messages=[LDMessage(role='system', content='Test message')] + ) + + # This should still work as before + config, tracker = ldai_client.config('customer-support-agent', context, default_value) + + assert config.enabled is True + assert config.model is not None + assert tracker is not None From ffdea9fe9d6768f1510a27f7615cdf1767fd4681 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 30 Jun 2025 16:38:09 -0400 Subject: [PATCH 02/11] fix test and linting --- ldai/client.py | 36 ++++++++++----------- ldai/testing/test_agents.py | 63 +++++++++++++------------------------ 2 files changed, 40 insertions(+), 59 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index a0f227f..924d574 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -13,7 +13,7 @@ class LDMessage: role: Literal['system', 'user', 'assistant'] content: str - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given message as a dictionary object. """ @@ -69,7 +69,7 @@ def get_custom(self, key: str) -> Any: return self._custom.get(key) - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given model config as a dictionary object. """ @@ -95,7 +95,7 @@ def name(self) -> str: """ return self._name - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given provider config as a dictionary object. """ @@ -111,7 +111,7 @@ class AIConfig: messages: Optional[List[LDMessage]] = None provider: Optional[ProviderConfig] = None - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given default values as an AIConfig-compatible dictionary object. """ @@ -129,7 +129,7 @@ def to_dict(self) -> dict: class LDAIAgent: """ Represents an AI agent configuration with instructions and model settings. - + An agent is similar to an AIConfig but focuses on instructions rather than messages, making it suitable for AI assistant/agent use cases. """ @@ -139,11 +139,11 @@ class LDAIAgent: instructions: Optional[str] = None tracker: Optional[LDAIConfigTracker] = None - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given agent as a dictionary object. """ - result = { + result: Dict[str, Any] = { '_ldMeta': { 'enabled': self.enabled or False, }, @@ -159,7 +159,7 @@ def to_dict(self) -> dict: class LDAIAgentDefaults: """ Default values for AI agent configurations. - + Similar to LDAIAgent but without tracker and with optional enabled field, used as fallback values when agent configurations are not available. """ @@ -168,11 +168,11 @@ class LDAIAgentDefaults: provider: Optional[ProviderConfig] = None instructions: Optional[str] = None - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """ Render the given agent defaults as a dictionary object. """ - result = { + result: Dict[str, Any] = { '_ldMeta': { 'enabled': self.enabled or False, }, @@ -210,7 +210,7 @@ def config( :param variables: Additional variables for the model configuration. :return: The value of the model configuration along with a tracker used for gathering metrics. """ - model, provider, messages, tracker, enabled = self.__evaluate(key, context, default_value.to_dict(), variables) + model, provider, messages, instructions, tracker, enabled = self.__evaluate(key, context, default_value.to_dict(), variables) config = AIConfig( enabled=bool(enabled), @@ -247,7 +247,7 @@ def agents( ), {'company_name': 'Acme Corp'} ) - + support_agent = agents['customer-support'] if support_agent.enabled: print(support_agent.instructions) # Instructions with interpolated variables @@ -261,11 +261,11 @@ def agents( :return: Dictionary mapping agent keys to their LDAIAgent configurations. """ result: LDAIAgents = {} - + for key in keys: agent = self.__evaluate_agent(key, context, default_value, variables) result[key] = agent - + return result def __evaluate( @@ -274,10 +274,10 @@ def __evaluate( context: Context, default_dict: Dict[str, Any], variables: Optional[Dict[str, Any]] = None, - ) -> Tuple[Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]], Optional[str], LDAIConfigTracker]: + ) -> Tuple[Optional[ModelConfig], Optional[ProviderConfig], Optional[List[LDMessage]], Optional[str], LDAIConfigTracker, bool]: """ Internal method to evaluate a configuration and extract components. - + :param key: The configuration key. :param context: The evaluation context. :param default_dict: Default configuration as dictionary. @@ -350,14 +350,14 @@ def __evaluate_agent( ) -> LDAIAgent: """ Internal method to evaluate an agent configuration. - + :param key: The agent configuration key. :param context: The evaluation context. :param default_value: Default agent values. :param variables: Variables for interpolation. :return: Configured LDAIAgent instance. """ - model, provider, instructions, tracker, enabled = self.__evaluate( + model, provider, messages, instructions, tracker, enabled = self.__evaluate( key, context, default_value.to_dict(), variables ) diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index bbf26ec..761495f 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -2,13 +2,14 @@ from ldclient import Config, Context, LDClient from ldclient.integrations.test_data import TestData -from ldai.client import LDAIAgent, LDAIAgentDefaults, LDAIClient, ModelConfig, ProviderConfig +from ldai.client import (LDAIAgentDefaults, LDAIClient, ModelConfig, + ProviderConfig) @pytest.fixture def td() -> TestData: td = TestData.data_source() - + # Single agent with instructions td.update( td.flag('customer-support-agent') @@ -116,7 +117,7 @@ def test_single_agent_basic_functionality(ldai_client: LDAIClient): assert len(agents) == 1 assert 'customer-support-agent' in agents - + agent = agents['customer-support-agent'] assert agent.enabled is True assert agent.model is not None @@ -159,7 +160,7 @@ def test_agent_multi_context_interpolation(ldai_client: LDAIClient): user_context = Context.builder('user-key').name('Bob').build() org_context = Context.builder('org-key').kind('org').name('LaunchDarkly').set('tier', 'Enterprise').build() context = Context.multi_builder().add(user_context).add(org_context).build() - + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") agents = ldai_client.agents(['multi-context-agent'], context, defaults) @@ -180,9 +181,9 @@ def test_multiple_agents_retrieval(ldai_client: LDAIClient): variables = {'company_name': 'MultiCorp'} agents = ldai_client.agents( - ['customer-support-agent', 'sales-assistant'], - context, - defaults, + ['customer-support-agent', 'sales-assistant'], + context, + defaults, variables ) @@ -192,12 +193,12 @@ def test_multiple_agents_retrieval(ldai_client: LDAIClient): support_agent = agents['customer-support-agent'] assert support_agent.enabled is True - assert 'MultiCorp' in support_agent.instructions + assert support_agent.instructions is not None and 'MultiCorp' in support_agent.instructions sales_agent = agents['sales-assistant'] assert sales_agent.enabled is True - assert 'MultiCorp' in sales_agent.instructions - assert sales_agent.model.get_parameter('temperature') == 0.7 + assert sales_agent.instructions is not None and 'MultiCorp' in sales_agent.instructions + assert sales_agent.model is not None and sales_agent.model.get_parameter('temperature') == 0.7 def test_disabled_agent(ldai_client: LDAIClient): @@ -244,9 +245,9 @@ def test_agent_uses_defaults_on_missing_flag(ldai_client: LDAIClient): agent = agents['non-existent-agent'] assert agent.enabled == defaults.enabled - assert agent.model.name == 'default-gpt' - assert agent.model.get_parameter('temp') == 0.5 - assert agent.provider.name == 'default-provider' + assert agent.model is not None and agent.model.name == 'default-gpt' + assert agent.model is not None and agent.model.get_parameter('temp') == 0.5 + assert agent.provider is not None and agent.provider.name == 'default-provider' assert agent.instructions == defaults.instructions # Tracker should still be created for non-existent flags assert agent.tracker is not None @@ -263,13 +264,13 @@ def test_agent_error_handling(ldai_client: LDAIClient): # Test with a mix of valid and invalid keys agents = ldai_client.agents( - ['customer-support-agent', 'invalid-flag'], - context, + ['customer-support-agent', 'invalid-flag'], + context, defaults ) assert len(agents) == 2 - + # Valid agent should work normally valid_agent = agents['customer-support-agent'] assert valid_agent.enabled is True @@ -278,7 +279,7 @@ def test_agent_error_handling(ldai_client: LDAIClient): # Invalid agent should use defaults but still be created invalid_agent = agents['invalid-flag'] assert invalid_agent.enabled == defaults.enabled - assert invalid_agent.model.name == 'fallback-model' + assert invalid_agent.model is not None and invalid_agent.model.name == 'fallback-model' assert invalid_agent.instructions == defaults.instructions @@ -301,35 +302,15 @@ def test_agent_empty_agent_list(ldai_client: LDAIClient): defaults = LDAIAgentDefaults(enabled=True, instructions="Default") agents = ldai_client.agents([], context, defaults) - + assert len(agents) == 0 assert agents == {} -def test_agent_tracker_functionality(ldai_client: LDAIClient): - """Test that agent tracker works correctly.""" - context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - - agents = ldai_client.agents(['customer-support-agent'], context, defaults) - agent = agents['customer-support-agent'] - - assert agent.tracker is not None - assert hasattr(agent.tracker, 'track_success') - assert hasattr(agent.tracker, 'track_duration') - assert hasattr(agent.tracker, 'track_tokens') - - # Test that tracker has correct metadata - track_data = agent.tracker._LDAIConfigTracker__get_track_data() - assert track_data['variationKey'] == 'agent-v1' - assert track_data['configKey'] == 'customer-support-agent' - assert track_data['version'] == 1 - - def test_agents_backwards_compatibility_with_config(ldai_client: LDAIClient): """Test that the existing config method still works after agent additions.""" from ldai.client import AIConfig, LDMessage - + context = Context.create('user-key') default_value = AIConfig( enabled=True, @@ -339,7 +320,7 @@ def test_agents_backwards_compatibility_with_config(ldai_client: LDAIClient): # This should still work as before config, tracker = ldai_client.config('customer-support-agent', context, default_value) - + assert config.enabled is True assert config.model is not None - assert tracker is not None + assert tracker is not None From 52829410714adac78e6f066a33696367c0ddec6b Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 30 Jun 2025 16:44:46 -0400 Subject: [PATCH 03/11] removed unneeded changes --- ldai/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index 924d574..e8f8630 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -13,7 +13,7 @@ class LDMessage: role: Literal['system', 'user', 'assistant'] content: str - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict: """ Render the given message as a dictionary object. """ @@ -69,7 +69,7 @@ def get_custom(self, key: str) -> Any: return self._custom.get(key) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict: """ Render the given model config as a dictionary object. """ @@ -95,7 +95,7 @@ def name(self) -> str: """ return self._name - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict: """ Render the given provider config as a dictionary object. """ @@ -111,7 +111,7 @@ class AIConfig: messages: Optional[List[LDMessage]] = None provider: Optional[ProviderConfig] = None - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict: """ Render the given default values as an AIConfig-compatible dictionary object. """ From 8940ea35311beeeb5df77c73ed7bdfed5a1cc749 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 30 Jun 2025 16:48:32 -0400 Subject: [PATCH 04/11] fixed indentation --- ldai/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index e8f8630..500578b 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -235,8 +235,8 @@ def agents( with each agent having its instructions dynamically interpolated with the provided variables and context data. - Example: - ```python + Example:: + agents = client.agents( ['customer-support', 'sales-assistant'], context, @@ -252,7 +252,6 @@ def agents( if support_agent.enabled: print(support_agent.instructions) # Instructions with interpolated variables # Use support_agent.tracker for metrics tracking - ``` :param keys: List of agent configuration keys to retrieve. :param context: The context to evaluate the agent configurations in. From 7b111f389cc246b6e58809e48f3d3775ee2253bc Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 14 Jul 2025 15:45:23 -0500 Subject: [PATCH 05/11] Updated with .agent method from spec --- ldai/client.py | 127 ++++++++++--- ldai/testing/test_agents.py | 344 +++++++++++++++++++++++------------- ldai/tracker.py | 10 +- 3 files changed, 330 insertions(+), 151 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index 500578b..0f323b3 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -184,6 +184,18 @@ def to_dict(self) -> Dict[str, Any]: return result +@dataclass +class LDAIAgentConfig: + """ + Configuration for individual agent in batch requests. + + Combines agent key with its specific default configuration and variables. + """ + agent_key: str + default_config: LDAIAgentDefaults + variables: Optional[Dict[str, Any]] = None + + # Type alias for multiple agents LDAIAgents = Dict[str, LDAIAgent] @@ -221,49 +233,111 @@ def config( return config, tracker - def agents( + def agent( self, - keys: List[str], + key: str, context: Context, default_value: LDAIAgentDefaults, variables: Optional[Dict[str, Any]] = None, - ) -> LDAIAgents: + ) -> LDAIAgent: """ - Get multiple AI agent configurations. + Retrieve a single AI Config agent. - This method allows you to retrieve multiple agent configurations in a single call, - with each agent having its instructions dynamically interpolated with the provided - variables and context data. + This method retrieves a single agent configuration with instructions + dynamically interpolated using the provided variables and context data. Example:: - agents = client.agents( - ['customer-support', 'sales-assistant'], + agent = client.agent( + 'research_agent', context, LDAIAgentDefaults( enabled=True, model=ModelConfig('gpt-4'), - instructions="You are a helpful assistant." + instructions="You are a research assistant specializing in {{topic}}." ), - {'company_name': 'Acme Corp'} + {'topic': 'climate change'} ) - support_agent = agents['customer-support'] - if support_agent.enabled: - print(support_agent.instructions) # Instructions with interpolated variables - # Use support_agent.tracker for metrics tracking + if agent.enabled: + research_result = agent.instructions # Interpolated instructions + agent.tracker.track_success() - :param keys: List of agent configuration keys to retrieve. - :param context: The context to evaluate the agent configurations in. + :param key: The agent configuration key to retrieve. + :param context: The context to evaluate the agent configuration in. :param default_value: Default agent configuration values to use as fallback. :param variables: Additional variables for template interpolation in instructions. + :return: Configured LDAIAgent instance. + """ + # Track single agent usage + self._client.track( + "$ld:ai:agent:function:single", + context, + key, + 1 + ) + + return self.__evaluate_agent(key, context, default_value, variables) + + def agents( + self, + agent_configs: List[LDAIAgentConfig], + context: Context, + ) -> LDAIAgents: + """ + Retrieve multiple AI agent configurations. + + This method allows you to retrieve multiple agent configurations in a single call, + with each agent having its own default configuration and variables for instruction + interpolation. + + Example:: + + agents = client.agents([ + LDAIAgentConfig( + agent_key='research_agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions='You are a research assistant.' + ), + variables={'topic': 'climate change'} + ), + LDAIAgentConfig( + agent_key='writing_agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions='You are a writing assistant.' + ), + variables={'style': 'academic'} + ) + ], context) + + research_result = agents["research_agent"].instructions + agents["research_agent"].tracker.track_success() + + :param agent_configs: List of agent configurations to retrieve. + :param context: The context to evaluate the agent configurations in. :return: Dictionary mapping agent keys to their LDAIAgent configurations. """ + # Track multiple agents usage + agent_count = len(agent_configs) + self._client.track( + "$ld:ai:agent:function:multiple", + context, + agent_count, + agent_count + ) + result: LDAIAgents = {} - for key in keys: - agent = self.__evaluate_agent(key, context, default_value, variables) - result[key] = agent + for config in agent_configs: + agent = self.__evaluate_agent( + config.agent_key, + context, + config.default_config, + config.variables + ) + result[config.agent_key] = agent return result @@ -360,20 +434,23 @@ def __evaluate_agent( key, context, default_value.to_dict(), variables ) + # For agents, prioritize instructions over messages + final_instructions = instructions if instructions is not None else default_value.instructions + return LDAIAgent( - enabled=bool(enabled) if enabled is not None else None, + enabled=bool(enabled) if enabled is not None else default_value.enabled, model=model or default_value.model, provider=provider or default_value.provider, - instructions=instructions, + instructions=final_instructions, tracker=tracker, ) def __interpolate_template(self, template: str, variables: Dict[str, Any]) -> str: """ - Interpolate the template with the given variables. + Interpolate the template with the given variables using Mustache format. - :template: The template string. - :variables: The variables to interpolate into the template. + :param template: The template string. + :param variables: The variables to interpolate into the template. :return: The interpolated string. """ return chevron.render(template, variables) diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index 761495f..a3e6a8e 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -2,8 +2,8 @@ from ldclient import Config, Context, LDClient from ldclient.integrations.test_data import TestData -from ldai.client import (LDAIAgentDefaults, LDAIClient, ModelConfig, - ProviderConfig) +from ldai.client import (LDAIAgentConfig, LDAIAgentDefaults, LDAIClient, + ModelConfig, ProviderConfig) @pytest.fixture @@ -89,6 +89,20 @@ def td() -> TestData: .variation_for_all(0) ) + # Research agent for testing single agent method + td.update( + td.flag('research-agent') + .variations( + { + 'model': {'name': 'gpt-4', 'parameters': {'temperature': 0.2, 'maxTokens': 3000}}, + 'provider': {'name': 'openai'}, + 'instructions': 'You are a research assistant specializing in {{topic}}. Your expertise level should match {{ldctx.expertise}}.', + '_ldMeta': {'enabled': True, 'variationKey': 'research-v1', 'version': 1, 'mode': 'agent'}, + } + ) + .variation_for_all(0) + ) + return td @@ -103,110 +117,171 @@ def ldai_client(client: LDClient) -> LDAIClient: return LDAIClient(client) -def test_single_agent_basic_functionality(ldai_client: LDAIClient): - """Test basic agent retrieval and configuration.""" - context = Context.create('user-key') +def test_single_agent_method(ldai_client: LDAIClient): + """Test the single agent() method functionality.""" + context = Context.builder('user-key').set('expertise', 'advanced').build() defaults = LDAIAgentDefaults( enabled=False, model=ModelConfig('fallback-model'), instructions="Default instructions" ) - variables = {'company_name': 'Acme Corp'} + variables = {'topic': 'quantum computing'} - agents = ldai_client.agents(['customer-support-agent'], context, defaults, variables) + agent = ldai_client.agent('research-agent', context, defaults, variables) - assert len(agents) == 1 - assert 'customer-support-agent' in agents - - agent = agents['customer-support-agent'] assert agent.enabled is True assert agent.model is not None assert agent.model.name == 'gpt-4' - assert agent.model.get_parameter('temperature') == 0.3 - assert agent.model.get_parameter('maxTokens') == 2048 + assert agent.model.get_parameter('temperature') == 0.2 + assert agent.model.get_parameter('maxTokens') == 3000 assert agent.provider is not None assert agent.provider.name == 'openai' - assert agent.instructions == 'You are a helpful customer support agent for Acme Corp. Always be polite and professional.' + assert agent.instructions == 'You are a research assistant specializing in quantum computing. Your expertise level should match advanced.' assert agent.tracker is not None -def test_agent_instructions_interpolation(ldai_client: LDAIClient): - """Test that agent instructions are properly interpolated with variables.""" +def test_single_agent_with_defaults(ldai_client: LDAIClient): + """Test single agent method with non-existent flag using defaults.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - variables = {'company_name': 'TechStart Inc'} + defaults = LDAIAgentDefaults( + enabled=True, + model=ModelConfig('default-model', parameters={'temp': 0.8}), + provider=ProviderConfig('default-provider'), + instructions="You are a default assistant for {{task}}." + ) + variables = {'task': 'general assistance'} - agents = ldai_client.agents(['customer-support-agent'], context, defaults, variables) - agent = agents['customer-support-agent'] + agent = ldai_client.agent('non-existent-agent', context, defaults, variables) - expected_instructions = 'You are a helpful customer support agent for TechStart Inc. Always be polite and professional.' - assert agent.instructions == expected_instructions + assert agent.enabled is True + assert agent.model is not None and agent.model.name == 'default-model' + assert agent.model is not None and agent.model.get_parameter('temp') == 0.8 + assert agent.provider is not None and agent.provider.name == 'default-provider' + assert agent.instructions == "You are a default assistant for general assistance." + assert agent.tracker is not None -def test_agent_context_interpolation(ldai_client: LDAIClient): - """Test that agent instructions can access context data via ldctx.""" - context = Context.builder('user-123').name('Alice').build() - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") +def test_agents_method_with_configs(ldai_client: LDAIClient): + """Test the new agents() method with LDAIAgentConfig objects.""" + context = Context.create('user-key') - agents = ldai_client.agents(['personalized-agent'], context, defaults) - agent = agents['personalized-agent'] + agent_configs = [ + LDAIAgentConfig( + agent_key='customer-support-agent', + default_config=LDAIAgentDefaults( + enabled=False, + model=ModelConfig('fallback-model'), + instructions="Default support" + ), + variables={'company_name': 'Acme Corp'} + ), + LDAIAgentConfig( + agent_key='sales-assistant', + default_config=LDAIAgentDefaults( + enabled=False, + model=ModelConfig('fallback-model'), + instructions="Default sales" + ), + variables={'company_name': 'Acme Corp'} + ) + ] - expected_instructions = 'Hello Alice! I am your personal assistant. Your user key is user-123.' - assert agent.instructions == expected_instructions + agents = ldai_client.agents(agent_configs, context) + assert len(agents) == 2 + assert 'customer-support-agent' in agents + assert 'sales-assistant' in agents + + support_agent = agents['customer-support-agent'] + assert support_agent.enabled is True + assert support_agent.instructions is not None and 'Acme Corp' in support_agent.instructions + + sales_agent = agents['sales-assistant'] + assert sales_agent.enabled is True + assert sales_agent.instructions is not None and 'Acme Corp' in sales_agent.instructions + assert sales_agent.model is not None and sales_agent.model.get_parameter('temperature') == 0.7 + + +def test_agents_method_different_variables_per_agent(ldai_client: LDAIClient): + """Test agents method with different variables for each agent.""" + context = Context.builder('user-key').name('Alice').build() + + agent_configs = [ + LDAIAgentConfig( + agent_key='personalized-agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions="Default personal" + ), + variables={} # Will use context only + ), + LDAIAgentConfig( + agent_key='customer-support-agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions="Default support" + ), + variables={'company_name': 'TechStart Inc'} + ) + ] -def test_agent_multi_context_interpolation(ldai_client: LDAIClient): - """Test agent instructions with multi-context interpolation.""" + agents = ldai_client.agents(agent_configs, context) + + personal_agent = agents['personalized-agent'] + assert personal_agent.instructions == 'Hello Alice! I am your personal assistant. Your user key is user-key.' + + support_agent = agents['customer-support-agent'] + assert support_agent.instructions == 'You are a helpful customer support agent for TechStart Inc. Always be polite and professional.' + + +def test_agents_with_multi_context_interpolation(ldai_client: LDAIClient): + """Test agents method with multi-context interpolation.""" user_context = Context.builder('user-key').name('Bob').build() org_context = Context.builder('org-key').kind('org').name('LaunchDarkly').set('tier', 'Enterprise').build() context = Context.multi_builder().add(user_context).add(org_context).build() - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + agent_configs = [ + LDAIAgentConfig( + agent_key='multi-context-agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions="Default multi-context" + ), + variables={} + ) + ] - agents = ldai_client.agents(['multi-context-agent'], context, defaults) + agents = ldai_client.agents(agent_configs, context) agent = agents['multi-context-agent'] expected_instructions = 'Welcome Bob from LaunchDarkly! Your organization tier is Enterprise.' assert agent.instructions == expected_instructions -def test_multiple_agents_retrieval(ldai_client: LDAIClient): - """Test retrieving multiple agents in a single call.""" +def test_disabled_agent_single_method(ldai_client: LDAIClient): + """Test that disabled agents are properly handled in single agent method.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults( - enabled=False, - model=ModelConfig('fallback'), - instructions="Default" - ) - variables = {'company_name': 'MultiCorp'} - - agents = ldai_client.agents( - ['customer-support-agent', 'sales-assistant'], - context, - defaults, - variables - ) - - assert len(agents) == 2 - assert 'customer-support-agent' in agents - assert 'sales-assistant' in agents + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - support_agent = agents['customer-support-agent'] - assert support_agent.enabled is True - assert support_agent.instructions is not None and 'MultiCorp' in support_agent.instructions + agent = ldai_client.agent('disabled-agent', context, defaults) - sales_agent = agents['sales-assistant'] - assert sales_agent.enabled is True - assert sales_agent.instructions is not None and 'MultiCorp' in sales_agent.instructions - assert sales_agent.model is not None and sales_agent.model.get_parameter('temperature') == 0.7 + assert agent.enabled is False + assert agent.instructions == 'This agent is disabled.' -def test_disabled_agent(ldai_client: LDAIClient): - """Test that disabled agents are properly handled.""" +def test_disabled_agent_multiple_method(ldai_client: LDAIClient): + """Test that disabled agents are properly handled in multiple agents method.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - agents = ldai_client.agents(['disabled-agent'], context, defaults) + agent_configs = [ + LDAIAgentConfig( + agent_key='disabled-agent', + default_config=LDAIAgentDefaults(enabled=True, instructions="Default"), + variables={} + ) + ] + + agents = ldai_client.agents(agent_configs, context) agent = agents['disabled-agent'] assert agent.enabled is False @@ -222,8 +297,7 @@ def test_agent_with_missing_metadata(ldai_client: LDAIClient): instructions="Default instructions" ) - agents = ldai_client.agents(['minimal-agent'], context, defaults) - agent = agents['minimal-agent'] + agent = ldai_client.agent('minimal-agent', context, defaults) assert agent.enabled is True # From flag assert agent.instructions == 'Minimal agent configuration.' @@ -231,83 +305,77 @@ def test_agent_with_missing_metadata(ldai_client: LDAIClient): assert agent.tracker is not None -def test_agent_uses_defaults_on_missing_flag(ldai_client: LDAIClient): - """Test that default values are used when agent flag doesn't exist.""" +def test_empty_agents_list(ldai_client: LDAIClient): + """Test agents method with empty agent configs list.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults( - enabled=True, - model=ModelConfig('default-gpt', parameters={'temp': 0.5}), - provider=ProviderConfig('default-provider'), - instructions="You are a default assistant." - ) - agents = ldai_client.agents(['non-existent-agent'], context, defaults) - agent = agents['non-existent-agent'] + agents = ldai_client.agents([], context) - assert agent.enabled == defaults.enabled - assert agent.model is not None and agent.model.name == 'default-gpt' - assert agent.model is not None and agent.model.get_parameter('temp') == 0.5 - assert agent.provider is not None and agent.provider.name == 'default-provider' - assert agent.instructions == defaults.instructions - # Tracker should still be created for non-existent flags - assert agent.tracker is not None + assert len(agents) == 0 + assert agents == {} -def test_agent_error_handling(ldai_client: LDAIClient): - """Test that agent errors are handled gracefully.""" +def test_agent_tracker_functionality(ldai_client: LDAIClient): + """Test that agent tracker works correctly.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults( - enabled=True, - model=ModelConfig('fallback-model'), - instructions="Fallback instructions" - ) - - # Test with a mix of valid and invalid keys - agents = ldai_client.agents( - ['customer-support-agent', 'invalid-flag'], - context, - defaults - ) - - assert len(agents) == 2 - - # Valid agent should work normally - valid_agent = agents['customer-support-agent'] - assert valid_agent.enabled is True - assert valid_agent.tracker is not None - - # Invalid agent should use defaults but still be created - invalid_agent = agents['invalid-flag'] - assert invalid_agent.enabled == defaults.enabled - assert invalid_agent.model is not None and invalid_agent.model.name == 'fallback-model' - assert invalid_agent.instructions == defaults.instructions + defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + agent = ldai_client.agent('customer-support-agent', context, defaults) -def test_agent_no_variables_interpolation(ldai_client: LDAIClient): - """Test agent instructions with no variables provided.""" - context = Context.builder('user-456').name('Charlie').build() - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + assert agent.tracker is not None + assert hasattr(agent.tracker, 'track_success') + assert hasattr(agent.tracker, 'track_duration') + assert hasattr(agent.tracker, 'track_tokens') - agents = ldai_client.agents(['personalized-agent'], context, defaults) - agent = agents['personalized-agent'] + # Test that tracker has correct metadata + track_data = agent.tracker.get_track_data() + assert track_data['variationKey'] == 'agent-v1' + assert track_data['configKey'] == 'customer-support-agent' + assert track_data['version'] == 1 - # Should still interpolate context but not variables - expected_instructions = 'Hello Charlie! I am your personal assistant. Your user key is user-456.' - assert agent.instructions == expected_instructions +def test_agent_tracking_calls(ldai_client: LDAIClient): + """Test that tracking calls are made for agent usage.""" + from unittest.mock import MagicMock, patch -def test_agent_empty_agent_list(ldai_client: LDAIClient): - """Test agents method with empty agent list.""" context = Context.create('user-key') defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - agents = ldai_client.agents([], context, defaults) + # Test single agent tracking + with patch.object(ldai_client._client, 'track') as mock_track: + ldai_client.agent('customer-support-agent', context, defaults) + mock_track.assert_called_with( + "$ld:ai:agent:single", + context, + 'customer-support-agent', + 1 + ) - assert len(agents) == 0 - assert agents == {} + # Test multiple agents tracking + agent_configs = [ + LDAIAgentConfig( + agent_key='customer-support-agent', + default_config=defaults, + variables={} + ), + LDAIAgentConfig( + agent_key='sales-assistant', + default_config=defaults, + variables={} + ) + ] + + with patch.object(ldai_client._client, 'track') as mock_track: + ldai_client.agents(agent_configs, context) + mock_track.assert_called_with( + "$ld:ai:agent:multiple", + context, + 2, + 2 + ) -def test_agents_backwards_compatibility_with_config(ldai_client: LDAIClient): +def test_backwards_compatibility_with_config(ldai_client: LDAIClient): """Test that the existing config method still works after agent additions.""" from ldai.client import AIConfig, LDMessage @@ -324,3 +392,29 @@ def test_agents_backwards_compatibility_with_config(ldai_client: LDAIClient): assert config.enabled is True assert config.model is not None assert tracker is not None + + +def test_agent_config_dataclass(): + """Test the LDAIAgentConfig dataclass functionality.""" + config = LDAIAgentConfig( + agent_key='test-agent', + default_config=LDAIAgentDefaults( + enabled=True, + instructions="Test instructions" + ), + variables={'key': 'value'} + ) + + assert config.agent_key == 'test-agent' + assert config.default_config.enabled is True + assert config.default_config.instructions == "Test instructions" + assert config.variables == {'key': 'value'} + + # Test with no variables + config_no_vars = LDAIAgentConfig( + agent_key='test-agent-2', + default_config=LDAIAgentDefaults(enabled=False) + ) + + assert config_no_vars.agent_key == 'test-agent-2' + assert config_no_vars.variables is None diff --git a/ldai/tracker.py b/ldai/tracker.py index ef10bcb..8968c06 100644 --- a/ldai/tracker.py +++ b/ldai/tracker.py @@ -1,7 +1,7 @@ import time from dataclasses import dataclass from enum import Enum -from typing import Dict, Optional +from typing import Any, Dict, Optional from ldclient import Context, LDClient @@ -282,6 +282,14 @@ def get_summary(self) -> LDAIMetricSummary: """ return self._summary + def get_track_data(self) -> Dict[str, Any]: + """ + Get tracking data for events. + + :return: Dictionary containing variation and config keys. + """ + return self.__get_track_data() + def _bedrock_to_token_usage(data: dict) -> TokenUsage: """ From 13baea890a8aa2c9dad48fa7542a319453a85d02 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 14 Jul 2025 15:52:13 -0500 Subject: [PATCH 06/11] Fixing tracking key --- ldai/testing/test_agents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index a3e6a8e..1c2e3c7 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -345,7 +345,7 @@ def test_agent_tracking_calls(ldai_client: LDAIClient): with patch.object(ldai_client._client, 'track') as mock_track: ldai_client.agent('customer-support-agent', context, defaults) mock_track.assert_called_with( - "$ld:ai:agent:single", + "$ld:ai:agent:function:single", context, 'customer-support-agent', 1 @@ -368,7 +368,7 @@ def test_agent_tracking_calls(ldai_client: LDAIClient): with patch.object(ldai_client._client, 'track') as mock_track: ldai_client.agents(agent_configs, context) mock_track.assert_called_with( - "$ld:ai:agent:multiple", + "$ld:ai:agent:function:multiple", context, 2, 2 From 68fdeade8027ee1323ce465e8c32c6e31ede822e Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 15 Jul 2025 09:30:41 -0500 Subject: [PATCH 07/11] Added default values and updated prop names --- ldai/client.py | 33 +++++++++---- ldai/testing/test_agents.py | 94 +++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index 0f323b3..718f50b 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -191,10 +191,15 @@ class LDAIAgentConfig: Combines agent key with its specific default configuration and variables. """ - agent_key: str - default_config: LDAIAgentDefaults + key: str + default_value: Optional[LDAIAgentDefaults] = None variables: Optional[Dict[str, Any]] = None + def __post_init__(self): + """Set default value if not provided.""" + if self.default_value is None: + self.default_value = LDAIAgentDefaults(enabled=False) + # Type alias for multiple agents LDAIAgents = Dict[str, LDAIAgent] @@ -237,7 +242,7 @@ def agent( self, key: str, context: Context, - default_value: LDAIAgentDefaults, + default_value: Optional[LDAIAgentDefaults] = None, variables: Optional[Dict[str, Any]] = None, ) -> LDAIAgent: """ @@ -248,6 +253,7 @@ def agent( Example:: + # With explicit default configuration agent = client.agent( 'research_agent', context, @@ -259,6 +265,9 @@ def agent( {'topic': 'climate change'} ) + # Or with optional default (defaults to {enabled: False}) + agent = client.agent('research_agent', context, variables={'topic': 'climate change'}) + if agent.enabled: research_result = agent.instructions # Interpolated instructions agent.tracker.track_success() @@ -269,6 +278,10 @@ def agent( :param variables: Additional variables for template interpolation in instructions. :return: Configured LDAIAgent instance. """ + # Set default value if not provided + if default_value is None: + default_value = LDAIAgentDefaults(enabled=False) + # Track single agent usage self._client.track( "$ld:ai:agent:function:single", @@ -295,16 +308,16 @@ def agents( agents = client.agents([ LDAIAgentConfig( - agent_key='research_agent', - default_config=LDAIAgentDefaults( + key='research_agent', + default_value=LDAIAgentDefaults( enabled=True, instructions='You are a research assistant.' ), variables={'topic': 'climate change'} ), LDAIAgentConfig( - agent_key='writing_agent', - default_config=LDAIAgentDefaults( + key='writing_agent', + default_value=LDAIAgentDefaults( enabled=True, instructions='You are a writing assistant.' ), @@ -332,12 +345,12 @@ def agents( for config in agent_configs: agent = self.__evaluate_agent( - config.agent_key, + config.key, context, - config.default_config, + config.default_value, config.variables ) - result[config.agent_key] = agent + result[config.key] = agent return result diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index 1c2e3c7..946af91 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -167,8 +167,8 @@ def test_agents_method_with_configs(ldai_client: LDAIClient): agent_configs = [ LDAIAgentConfig( - agent_key='customer-support-agent', - default_config=LDAIAgentDefaults( + key='customer-support-agent', + default_value=LDAIAgentDefaults( enabled=False, model=ModelConfig('fallback-model'), instructions="Default support" @@ -176,8 +176,8 @@ def test_agents_method_with_configs(ldai_client: LDAIClient): variables={'company_name': 'Acme Corp'} ), LDAIAgentConfig( - agent_key='sales-assistant', - default_config=LDAIAgentDefaults( + key='sales-assistant', + default_value=LDAIAgentDefaults( enabled=False, model=ModelConfig('fallback-model'), instructions="Default sales" @@ -208,16 +208,16 @@ def test_agents_method_different_variables_per_agent(ldai_client: LDAIClient): agent_configs = [ LDAIAgentConfig( - agent_key='personalized-agent', - default_config=LDAIAgentDefaults( + key='personalized-agent', + default_value=LDAIAgentDefaults( enabled=True, instructions="Default personal" ), variables={} # Will use context only ), LDAIAgentConfig( - agent_key='customer-support-agent', - default_config=LDAIAgentDefaults( + key='customer-support-agent', + default_value=LDAIAgentDefaults( enabled=True, instructions="Default support" ), @@ -242,8 +242,8 @@ def test_agents_with_multi_context_interpolation(ldai_client: LDAIClient): agent_configs = [ LDAIAgentConfig( - agent_key='multi-context-agent', - default_config=LDAIAgentDefaults( + key='multi-context-agent', + default_value=LDAIAgentDefaults( enabled=True, instructions="Default multi-context" ), @@ -275,8 +275,8 @@ def test_disabled_agent_multiple_method(ldai_client: LDAIClient): agent_configs = [ LDAIAgentConfig( - agent_key='disabled-agent', - default_config=LDAIAgentDefaults(enabled=True, instructions="Default"), + key='disabled-agent', + default_value=LDAIAgentDefaults(enabled=True, instructions="Default"), variables={} ) ] @@ -354,13 +354,13 @@ def test_agent_tracking_calls(ldai_client: LDAIClient): # Test multiple agents tracking agent_configs = [ LDAIAgentConfig( - agent_key='customer-support-agent', - default_config=defaults, + key='customer-support-agent', + default_value=defaults, variables={} ), LDAIAgentConfig( - agent_key='sales-assistant', - default_config=defaults, + key='sales-assistant', + default_value=defaults, variables={} ) ] @@ -397,24 +397,70 @@ def test_backwards_compatibility_with_config(ldai_client: LDAIClient): def test_agent_config_dataclass(): """Test the LDAIAgentConfig dataclass functionality.""" config = LDAIAgentConfig( - agent_key='test-agent', - default_config=LDAIAgentDefaults( + key='test-agent', + default_value=LDAIAgentDefaults( enabled=True, instructions="Test instructions" ), variables={'key': 'value'} ) - assert config.agent_key == 'test-agent' - assert config.default_config.enabled is True - assert config.default_config.instructions == "Test instructions" + assert config.key == 'test-agent' + assert config.default_value.enabled is True + assert config.default_value.instructions == "Test instructions" assert config.variables == {'key': 'value'} # Test with no variables config_no_vars = LDAIAgentConfig( - agent_key='test-agent-2', - default_config=LDAIAgentDefaults(enabled=False) + key='test-agent-2', + default_value=LDAIAgentDefaults(enabled=False) ) - assert config_no_vars.agent_key == 'test-agent-2' + assert config_no_vars.key == 'test-agent-2' assert config_no_vars.variables is None + + +def test_agent_config_optional_default_value(): + """Test that LDAIAgentConfig defaults to {enabled: False} when default_value is not provided.""" + config = LDAIAgentConfig(key='test-agent') + + assert config.key == 'test-agent' + assert config.default_value is not None + assert config.default_value.enabled is False + assert config.variables is None + + +def test_single_agent_optional_default_value(ldai_client: LDAIClient): + """Test the single agent() method with optional default_value.""" + context = Context.create('user-key') + + # Should work with no default_value provided (defaults to {enabled: False}) + agent = ldai_client.agent('non-existent-agent', context) + + assert agent.enabled is False # Should default to False + assert agent.tracker is not None + + +def test_agents_method_with_optional_defaults(ldai_client: LDAIClient): + """Test agents method with optional default_value configurations.""" + context = Context.create('user-key') + + agent_configs = [ + LDAIAgentConfig(key='customer-support-agent'), # No default_value + LDAIAgentConfig( + key='sales-assistant', + default_value=LDAIAgentDefaults(enabled=True, instructions="Custom sales assistant") + ) + ] + + agents = ldai_client.agents(agent_configs, context) + + assert len(agents) == 2 + + # First agent should use default {enabled: False} from auto-generated default_value + support_agent = agents['customer-support-agent'] + assert support_agent.enabled is True # From flag configuration + + # Second agent should use custom default + sales_agent = agents['sales-assistant'] + assert sales_agent.enabled is True From 88f5e795d86ed6cfe0aebd8509a94793034de7f6 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 15 Jul 2025 09:54:29 -0500 Subject: [PATCH 08/11] use default value for agents func when none is passed --- ldai/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ldai/client.py b/ldai/client.py index 718f50b..88e2be1 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -344,10 +344,13 @@ def agents( result: LDAIAgents = {} for config in agent_configs: + # Ensure default_value is set (should be handled by __post_init__, but satisfy type checker) + default_value = config.default_value or LDAIAgentDefaults(enabled=False) + agent = self.__evaluate_agent( config.key, context, - config.default_value, + default_value, config.variables ) result[config.key] = agent From 48a97b4e01a8325ac1f90a607d635c6e08038d5d Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 15 Jul 2025 10:03:34 -0500 Subject: [PATCH 09/11] removed unneeded function and fixed lint errors --- ldai/client.py | 2 +- ldai/testing/test_agents.py | 16 +++++----------- ldai/tracker.py | 8 -------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index 88e2be1..6f27759 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -346,7 +346,7 @@ def agents( for config in agent_configs: # Ensure default_value is set (should be handled by __post_init__, but satisfy type checker) default_value = config.default_value or LDAIAgentDefaults(enabled=False) - + agent = self.__evaluate_agent( config.key, context, diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index 946af91..de8473a 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -327,12 +327,6 @@ def test_agent_tracker_functionality(ldai_client: LDAIClient): assert hasattr(agent.tracker, 'track_duration') assert hasattr(agent.tracker, 'track_tokens') - # Test that tracker has correct metadata - track_data = agent.tracker.get_track_data() - assert track_data['variationKey'] == 'agent-v1' - assert track_data['configKey'] == 'customer-support-agent' - assert track_data['version'] == 1 - def test_agent_tracking_calls(ldai_client: LDAIClient): """Test that tracking calls are made for agent usage.""" @@ -423,7 +417,7 @@ def test_agent_config_dataclass(): def test_agent_config_optional_default_value(): """Test that LDAIAgentConfig defaults to {enabled: False} when default_value is not provided.""" config = LDAIAgentConfig(key='test-agent') - + assert config.key == 'test-agent' assert config.default_value is not None assert config.default_value.enabled is False @@ -433,10 +427,10 @@ def test_agent_config_optional_default_value(): def test_single_agent_optional_default_value(ldai_client: LDAIClient): """Test the single agent() method with optional default_value.""" context = Context.create('user-key') - + # Should work with no default_value provided (defaults to {enabled: False}) agent = ldai_client.agent('non-existent-agent', context) - + assert agent.enabled is False # Should default to False assert agent.tracker is not None @@ -456,11 +450,11 @@ def test_agents_method_with_optional_defaults(ldai_client: LDAIClient): agents = ldai_client.agents(agent_configs, context) assert len(agents) == 2 - + # First agent should use default {enabled: False} from auto-generated default_value support_agent = agents['customer-support-agent'] assert support_agent.enabled is True # From flag configuration - + # Second agent should use custom default sales_agent = agents['sales-assistant'] assert sales_agent.enabled is True diff --git a/ldai/tracker.py b/ldai/tracker.py index 8968c06..957ae05 100644 --- a/ldai/tracker.py +++ b/ldai/tracker.py @@ -282,14 +282,6 @@ def get_summary(self) -> LDAIMetricSummary: """ return self._summary - def get_track_data(self) -> Dict[str, Any]: - """ - Get tracking data for events. - - :return: Dictionary containing variation and config keys. - """ - return self.__get_track_data() - def _bedrock_to_token_usage(data: dict) -> TokenUsage: """ From 768ed2595063a07947907581b1ff0e110b7a6d78 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Mon, 21 Jul 2025 23:03:04 -0500 Subject: [PATCH 10/11] remove optional default and made small tweaks per pr feedback --- ldai/client.py | 45 +++----- ldai/testing/test_agents.py | 200 ++++++++---------------------------- ldai/tracker.py | 2 +- 3 files changed, 54 insertions(+), 193 deletions(-) diff --git a/ldai/client.py b/ldai/client.py index 6f27759..fabe770 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -155,7 +155,7 @@ def to_dict(self) -> Dict[str, Any]: return result -@dataclass(frozen=True) +@dataclass class LDAIAgentDefaults: """ Default values for AI agent configurations. @@ -192,14 +192,9 @@ class LDAIAgentConfig: Combines agent key with its specific default configuration and variables. """ key: str - default_value: Optional[LDAIAgentDefaults] = None + default_value: LDAIAgentDefaults variables: Optional[Dict[str, Any]] = None - def __post_init__(self): - """Set default value if not provided.""" - if self.default_value is None: - self.default_value = LDAIAgentDefaults(enabled=False) - # Type alias for multiple agents LDAIAgents = Dict[str, LDAIAgent] @@ -240,10 +235,8 @@ def config( def agent( self, - key: str, + config: LDAIAgentConfig, context: Context, - default_value: Optional[LDAIAgentDefaults] = None, - variables: Optional[Dict[str, Any]] = None, ) -> LDAIAgent: """ Retrieve a single AI Config agent. @@ -253,44 +246,33 @@ def agent( Example:: - # With explicit default configuration - agent = client.agent( - 'research_agent', - context, - LDAIAgentDefaults( + agent = client.agent(LDAIAgentConfig( + key='research_agent', + default_value=LDAIAgentDefaults( enabled=True, model=ModelConfig('gpt-4'), instructions="You are a research assistant specializing in {{topic}}." ), - {'topic': 'climate change'} - ) - - # Or with optional default (defaults to {enabled: False}) - agent = client.agent('research_agent', context, variables={'topic': 'climate change'}) + variables={'topic': 'climate change'} + ), context) if agent.enabled: research_result = agent.instructions # Interpolated instructions agent.tracker.track_success() - :param key: The agent configuration key to retrieve. + :param config: The agent configuration to use. :param context: The context to evaluate the agent configuration in. - :param default_value: Default agent configuration values to use as fallback. - :param variables: Additional variables for template interpolation in instructions. :return: Configured LDAIAgent instance. """ - # Set default value if not provided - if default_value is None: - default_value = LDAIAgentDefaults(enabled=False) - # Track single agent usage self._client.track( "$ld:ai:agent:function:single", context, - key, + config.key, 1 ) - return self.__evaluate_agent(key, context, default_value, variables) + return self.__evaluate_agent(config.key, context, config.default_value, config.variables) def agents( self, @@ -344,13 +326,10 @@ def agents( result: LDAIAgents = {} for config in agent_configs: - # Ensure default_value is set (should be handled by __post_init__, but satisfy type checker) - default_value = config.default_value or LDAIAgentDefaults(enabled=False) - agent = self.__evaluate_agent( config.key, context, - default_value, + config.default_value, config.variables ) result[config.key] = agent diff --git a/ldai/testing/test_agents.py b/ldai/testing/test_agents.py index de8473a..b2e80c0 100644 --- a/ldai/testing/test_agents.py +++ b/ldai/testing/test_agents.py @@ -120,14 +120,17 @@ def ldai_client(client: LDClient) -> LDAIClient: def test_single_agent_method(ldai_client: LDAIClient): """Test the single agent() method functionality.""" context = Context.builder('user-key').set('expertise', 'advanced').build() - defaults = LDAIAgentDefaults( - enabled=False, - model=ModelConfig('fallback-model'), - instructions="Default instructions" + config = LDAIAgentConfig( + key='research-agent', + default_value=LDAIAgentDefaults( + enabled=False, + model=ModelConfig('fallback-model'), + instructions="Default instructions" + ), + variables={'topic': 'quantum computing'} ) - variables = {'topic': 'quantum computing'} - agent = ldai_client.agent('research-agent', context, defaults, variables) + agent = ldai_client.agent(config, context) assert agent.enabled is True assert agent.model is not None @@ -143,15 +146,18 @@ def test_single_agent_method(ldai_client: LDAIClient): def test_single_agent_with_defaults(ldai_client: LDAIClient): """Test single agent method with non-existent flag using defaults.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults( - enabled=True, - model=ModelConfig('default-model', parameters={'temp': 0.8}), - provider=ProviderConfig('default-provider'), - instructions="You are a default assistant for {{task}}." + config = LDAIAgentConfig( + key='non-existent-agent', + default_value=LDAIAgentDefaults( + enabled=True, + model=ModelConfig('default-model', parameters={'temp': 0.8}), + provider=ProviderConfig('default-provider'), + instructions="You are a default assistant for {{task}}." + ), + variables={'task': 'general assistance'} ) - variables = {'task': 'general assistance'} - agent = ldai_client.agent('non-existent-agent', context, defaults, variables) + agent = ldai_client.agent(config, context) assert agent.enabled is True assert agent.model is not None and agent.model.name == 'default-model' @@ -236,7 +242,7 @@ def test_agents_method_different_variables_per_agent(ldai_client: LDAIClient): def test_agents_with_multi_context_interpolation(ldai_client: LDAIClient): """Test agents method with multi-context interpolation.""" - user_context = Context.builder('user-key').name('Bob').build() + user_context = Context.builder('user-key').name('Alice').build() org_context = Context.builder('org-key').kind('org').name('LaunchDarkly').set('tier', 'Enterprise').build() context = Context.multi_builder().add(user_context).add(org_context).build() @@ -252,21 +258,24 @@ def test_agents_with_multi_context_interpolation(ldai_client: LDAIClient): ] agents = ldai_client.agents(agent_configs, context) - agent = agents['multi-context-agent'] - expected_instructions = 'Welcome Bob from LaunchDarkly! Your organization tier is Enterprise.' - assert agent.instructions == expected_instructions + agent = agents['multi-context-agent'] + assert agent.instructions == 'Welcome Alice from LaunchDarkly! Your organization tier is Enterprise.' def test_disabled_agent_single_method(ldai_client: LDAIClient): """Test that disabled agents are properly handled in single agent method.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") + config = LDAIAgentConfig( + key='disabled-agent', + default_value=LDAIAgentDefaults(enabled=False), + variables={} + ) - agent = ldai_client.agent('disabled-agent', context, defaults) + agent = ldai_client.agent(config, context) assert agent.enabled is False - assert agent.instructions == 'This agent is disabled.' + assert agent.tracker is not None def test_disabled_agent_multiple_method(ldai_client: LDAIClient): @@ -276,116 +285,35 @@ def test_disabled_agent_multiple_method(ldai_client: LDAIClient): agent_configs = [ LDAIAgentConfig( key='disabled-agent', - default_value=LDAIAgentDefaults(enabled=True, instructions="Default"), + default_value=LDAIAgentDefaults(enabled=False), variables={} ) ] agents = ldai_client.agents(agent_configs, context) - agent = agents['disabled-agent'] - assert agent.enabled is False - assert agent.instructions == 'This agent is disabled.' + assert len(agents) == 1 + assert agents['disabled-agent'].enabled is False def test_agent_with_missing_metadata(ldai_client: LDAIClient): """Test agent handling when metadata is minimal or missing.""" context = Context.create('user-key') - defaults = LDAIAgentDefaults( - enabled=False, - model=ModelConfig('default-model'), - instructions="Default instructions" + config = LDAIAgentConfig( + key='minimal-agent', + default_value=LDAIAgentDefaults( + enabled=False, + model=ModelConfig('default-model'), + instructions="Default instructions" + ) ) - agent = ldai_client.agent('minimal-agent', context, defaults) + agent = ldai_client.agent(config, context) assert agent.enabled is True # From flag assert agent.instructions == 'Minimal agent configuration.' - assert agent.model == defaults.model # Falls back to default - assert agent.tracker is not None - - -def test_empty_agents_list(ldai_client: LDAIClient): - """Test agents method with empty agent configs list.""" - context = Context.create('user-key') - - agents = ldai_client.agents([], context) - - assert len(agents) == 0 - assert agents == {} - - -def test_agent_tracker_functionality(ldai_client: LDAIClient): - """Test that agent tracker works correctly.""" - context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - - agent = ldai_client.agent('customer-support-agent', context, defaults) - + assert agent.model == config.default_value.model # Falls back to default assert agent.tracker is not None - assert hasattr(agent.tracker, 'track_success') - assert hasattr(agent.tracker, 'track_duration') - assert hasattr(agent.tracker, 'track_tokens') - - -def test_agent_tracking_calls(ldai_client: LDAIClient): - """Test that tracking calls are made for agent usage.""" - from unittest.mock import MagicMock, patch - - context = Context.create('user-key') - defaults = LDAIAgentDefaults(enabled=True, instructions="Default") - - # Test single agent tracking - with patch.object(ldai_client._client, 'track') as mock_track: - ldai_client.agent('customer-support-agent', context, defaults) - mock_track.assert_called_with( - "$ld:ai:agent:function:single", - context, - 'customer-support-agent', - 1 - ) - - # Test multiple agents tracking - agent_configs = [ - LDAIAgentConfig( - key='customer-support-agent', - default_value=defaults, - variables={} - ), - LDAIAgentConfig( - key='sales-assistant', - default_value=defaults, - variables={} - ) - ] - - with patch.object(ldai_client._client, 'track') as mock_track: - ldai_client.agents(agent_configs, context) - mock_track.assert_called_with( - "$ld:ai:agent:function:multiple", - context, - 2, - 2 - ) - - -def test_backwards_compatibility_with_config(ldai_client: LDAIClient): - """Test that the existing config method still works after agent additions.""" - from ldai.client import AIConfig, LDMessage - - context = Context.create('user-key') - default_value = AIConfig( - enabled=True, - model=ModelConfig('test-model'), - messages=[LDMessage(role='system', content='Test message')] - ) - - # This should still work as before - config, tracker = ldai_client.config('customer-support-agent', context, default_value) - - assert config.enabled is True - assert config.model is not None - assert tracker is not None def test_agent_config_dataclass(): @@ -412,49 +340,3 @@ def test_agent_config_dataclass(): assert config_no_vars.key == 'test-agent-2' assert config_no_vars.variables is None - - -def test_agent_config_optional_default_value(): - """Test that LDAIAgentConfig defaults to {enabled: False} when default_value is not provided.""" - config = LDAIAgentConfig(key='test-agent') - - assert config.key == 'test-agent' - assert config.default_value is not None - assert config.default_value.enabled is False - assert config.variables is None - - -def test_single_agent_optional_default_value(ldai_client: LDAIClient): - """Test the single agent() method with optional default_value.""" - context = Context.create('user-key') - - # Should work with no default_value provided (defaults to {enabled: False}) - agent = ldai_client.agent('non-existent-agent', context) - - assert agent.enabled is False # Should default to False - assert agent.tracker is not None - - -def test_agents_method_with_optional_defaults(ldai_client: LDAIClient): - """Test agents method with optional default_value configurations.""" - context = Context.create('user-key') - - agent_configs = [ - LDAIAgentConfig(key='customer-support-agent'), # No default_value - LDAIAgentConfig( - key='sales-assistant', - default_value=LDAIAgentDefaults(enabled=True, instructions="Custom sales assistant") - ) - ] - - agents = ldai_client.agents(agent_configs, context) - - assert len(agents) == 2 - - # First agent should use default {enabled: False} from auto-generated default_value - support_agent = agents['customer-support-agent'] - assert support_agent.enabled is True # From flag configuration - - # Second agent should use custom default - sales_agent = agents['sales-assistant'] - assert sales_agent.enabled is True diff --git a/ldai/tracker.py b/ldai/tracker.py index 957ae05..ef10bcb 100644 --- a/ldai/tracker.py +++ b/ldai/tracker.py @@ -1,7 +1,7 @@ import time from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Optional +from typing import Dict, Optional from ldclient import Context, LDClient From 3c128473d9bca989fd2da50a8af60cf55dd7b95f Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 22 Jul 2025 12:33:42 -0500 Subject: [PATCH 11/11] added frozen attr to LD Agent config --- ldai/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldai/client.py b/ldai/client.py index fabe770..8854b4b 100644 --- a/ldai/client.py +++ b/ldai/client.py @@ -155,7 +155,7 @@ def to_dict(self) -> Dict[str, Any]: return result -@dataclass +@dataclass(frozen=True) class LDAIAgentDefaults: """ Default values for AI agent configurations.