diff --git a/launchdarkly/ai_config_strands/.env.example b/launchdarkly/ai_config_strands/.env.example index 4d1359d..b0bb65e 100644 --- a/launchdarkly/ai_config_strands/.env.example +++ b/launchdarkly/ai_config_strands/.env.example @@ -1,12 +1,16 @@ # Shared LaunchDarkly Configuration -LD_SERVER_KEY='add your SDK server key' +LD_SERVER_KEY='add your LaunchDarkly SDK server key' + +# LaunchDarkly API Access Token (for management operations) +# Get this from Account Settings → Authorization in LaunchDarkly dashboard +LD_API_KEY='add your LaunchDarkly API Access token server key' # Shared AI Config identifier for multi-agent system # Change this value if you want to create a new AI config -LD_AI_CONFIG_ID='multi-agent-llm-prompt' +LD_AI_CONFIG_ID='multi-agent-strands' # LaunchDarkly project key -LD_PROJECT_KEY='add your project name' +LD_PROJECT_KEY='add your LaunchDarkly project name' # Agent-specific roles for context targeting TEACHER_ORCHESTRATOR_ROLE=teacher-orchestrator diff --git a/launchdarkly/ai_config_strands/.gitignore b/launchdarkly/ai_config_strands/.gitignore new file mode 100644 index 0000000..4fa8198 --- /dev/null +++ b/launchdarkly/ai_config_strands/.gitignore @@ -0,0 +1,25 @@ +# Environment variables +.env + +# Virtual environments +venv/ +bootstrap/venv/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +repl_state/ \ No newline at end of file diff --git a/launchdarkly/ai_config_strands/bootstrap/README.md b/launchdarkly/ai_config_strands/bootstrap/README.md index d3b592b..e39b338 100644 --- a/launchdarkly/ai_config_strands/bootstrap/README.md +++ b/launchdarkly/ai_config_strands/bootstrap/README.md @@ -19,8 +19,22 @@ export LD_PROJECT_KEY="your-project-key" ``` ### 3. Run the Script + +#### Recommended: Skip Existing Resources ```bash +python create_ai_config.py --skip-existing +``` + +#### Other Options +```bash +# Normal mode (may encounter conflicts) python create_ai_config.py + +# Force targeting updates (use with caution) +python create_ai_config.py --skip-existing --force-targeting + +# Show help +python create_ai_config.py --help ``` ## šŸ“‹ What the Script Does @@ -70,13 +84,28 @@ Example: ## šŸ”§ Troubleshooting -### Common Issues +### Fixed Issues (Latest Version) + +The script has been updated to handle common errors: + +1. **Resources already exist**: Gracefully handles existing segments, AI model configs, AI configs, and variations +2. **YAML syntax error**: Fixed malformed YAML in the manifest file +3. **Targeting rule conflicts**: Smart conflict detection to avoid duplicate targeting rules +4. **Internal service errors**: Improved error handling and resource existence checking + +### Common Issues & Solutions **Missing Environment Variables** ``` āŒ LD_API_KEY environment variable not set ``` -Solution: Set your LaunchDarkly API key +Solution: Set your LaunchDarkly API key or add to `.env` file + +**"Already exists" errors** +``` +āŒ Segment already exists +``` +Solution: Use `--skip-existing` flag **Variation Key Mismatch** ``` @@ -84,11 +113,17 @@ Solution: Set your LaunchDarkly API key ``` Solution: Check that segment names match variation keys (use hyphens, not underscores) -**Duplicate Rules** +**Duplicate Targeting Rules** ``` āŒ Failed to update targeting: 400 - new rule is exact duplicate ``` -Solution: Delete existing AI Config or modify targeting rules +Solution: Script now automatically detects and skips duplicate rules + +**Internal Service Errors** +``` +āŒ 500 Internal Server Error +``` +Solution: Usually resolved by improved error handling in latest version ### Debug Mode Add detailed logging by modifying the script or checking the console output for step-by-step progress. @@ -103,11 +138,17 @@ The `ai_config_manifest.yaml` defines: ## šŸ”„ Re-running the Script -The script handles existing resources: -- **Segments**: Skips if already exist +The script intelligently handles existing resources: +- **Segments**: Skips if already exist (with `--skip-existing`) - **AI Config**: Skips if already exists - **Variations**: Skips if already exist -- **Targeting**: Updates existing rules +- **Targeting**: Detects and avoids duplicate rules + +### Command Line Options + +- `--skip-existing`: Skip creating resources that already exist (recommended) +- `--force-targeting`: Force targeting rule updates even if rules exist (use with caution) +- `--help`: Show all available options ## šŸ›”ļø Security diff --git a/launchdarkly/ai_config_strands/bootstrap/ai_config_manifest.yaml b/launchdarkly/ai_config_strands/bootstrap/ai_config_manifest.yaml index 231f78e..ab81a19 100644 --- a/launchdarkly/ai_config_strands/bootstrap/ai_config_manifest.yaml +++ b/launchdarkly/ai_config_strands/bootstrap/ai_config_manifest.yaml @@ -1,5 +1,5 @@ project: - key: "mp-webinar-test-1" + key: "gcasilva" environment: - key: production segment: @@ -52,84 +52,90 @@ project: contextKind: "user" negate: false ai-config: - key: "multi-agent-llm-prompt" + key: "multi-agent-strands-agentcore-2" description: "Multi-agent LLM Prompt" ai-model-configs: - - name: "us.anthropic.claude-3-7-sonnet-20250219-v1:0" - key: "Bedrock.us.anthropic.claude-3-7-sonnet-20250219-v1-0" + - name: "us.anthropic.claude-3-5-sonnet-20241022-v2:0" + key: "Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2-0" costPerInputToken: 0.000003 costPerOutputToken: 0.000015 - id: "us.anthropic.claude-3-7-sonnet-20250219-v1:0" + id: "us.anthropic.claude-3-5-sonnet-20241022-v2:0" provider: "Bedrock:Anthropic" + - name: "us.amazon.nova-pro-v1:0" + key: "Bedrock.us.amazon.nova-pro-v1-0" + costPerInputToken: 0.0000008 + costPerOutputToken: 0.0000032 + id: "us.amazon.nova-pro-v1:0" + provider: "Bedrock:Amazon" variations: - key: teacher-orchestrator system_prompt_source: fallback_prompts/teacher_orchestrator_fallback_prompt.txt - model_config_key: Bedrock.amazon.nova-pro-v1:0 + model_config_key: Bedrock.us.amazon.nova-pro-v1-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' - key: computer-science-assistant system_prompt_source: fallback_prompts/computer_science_assistant_fallback_prompt.txt - model_config_key: Bedrock.us.anthropic.claude-3-7-sonnet-20250219-v1-0 + model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' - key: math-assistant system_prompt_source: fallback_prompts/math_assistant_fallback_prompt.txt - model_config_key: Bedrock.us.anthropic.claude-3-7-sonnet-20250219-v1-0 + model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' - key: language-assistant system_prompt_source: fallback_prompts/language_assistant_fallback_prompt.txt - model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2:0 + model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' - key: english-assistant system_prompt_source: fallback_prompts/english_assistant_fallback_prompt.txt - model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2:0 + model_config_key: Bedrock.us.anthropic.claude-3-5-sonnet-20241022-v2-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' - key: general-assistant system_prompt_source: fallback_prompts/general_assistant_fallback_prompt.txt - model_config_key: Bedrock.amazon.nova-pro-v1:0 + model_config_key: Bedrock.us.amazon.nova-pro-v1-0 parameters: custom_parameters: '{"maxTokens":4096,"temperature":0.9,"topP":0.9}' rules: - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: - "agent-teacher-orchestrator" trackEvents: false - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: - "agent-computer-science-assistant" trackEvents: false - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: - "agent-math-assistant" trackEvents: false - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: - "agent-english-assistant" trackEvents: false - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: - "agent-language-assistant" trackEvents: false - clauses: - - attribute": "segmentMatch" + - attribute: "segmentMatch" negate: false op: "segmentMatch" values: diff --git a/launchdarkly/ai_config_strands/bootstrap/create_ai_config.py b/launchdarkly/ai_config_strands/bootstrap/create_ai_config.py index 4fb4ffe..b71b897 100755 --- a/launchdarkly/ai_config_strands/bootstrap/create_ai_config.py +++ b/launchdarkly/ai_config_strands/bootstrap/create_ai_config.py @@ -11,6 +11,14 @@ import time from pathlib import Path +# Load environment variables from .env file +try: + from dotenv import load_dotenv + load_dotenv(Path(__file__).parent.parent / '.env') +except ImportError: + print("āš ļø python-dotenv not installed. Install with: pip install python-dotenv") + print("āš ļø Or set LD_API_KEY environment variable manually") + class LaunchDarklyAIConfigCreator: def __init__(self, api_key, base_url="https://app.launchdarkly.com"): self.api_key = api_key @@ -41,7 +49,7 @@ def create_ai_model_config(self, project_key, model_config_data): print(f"āœ… AI Model Config '{model_config_data['key']}' created successfully") time.sleep(0.5) # Rate limiting delay return response.json() - elif response.status_code == 409: + elif response.status_code == 409 or (response.status_code == 400 and "already exists" in response.text.lower()): print(f"āš ļø AI Model Config '{model_config_data['key']}' already exists") return None elif response.status_code == 429: @@ -54,6 +62,12 @@ def create_ai_model_config(self, project_key, model_config_data): def create_ai_config(self, project_key, config_data): """Create AI Config using direct API call""" + # First check if it already exists + existing_config = self.get_ai_config(project_key, config_data["key"]) + if existing_config: + print(f"āš ļø AI Config '{config_data['key']}' already exists") + return existing_config + url = f"{self.base_url}/api/v2/projects/{project_key}/ai-configs" payload = { @@ -70,9 +84,13 @@ def create_ai_config(self, project_key, config_data): if response.status_code == 201: print(f"āœ… AI Config '{config_data['key']}' created successfully") return response.json() - elif response.status_code == 409 or (response.status_code == 400 and "enabled" in response.text): + elif response.status_code == 409 or (response.status_code == 400 and "already exists" in response.text.lower()): print(f"āš ļø AI Config '{config_data['key']}' already exists") return self.get_ai_config(project_key, config_data["key"]) + elif response.status_code == 429: + print(f"ā³ Rate limited, waiting 2 seconds...") + time.sleep(2) + return self.create_ai_config(project_key, config_data) else: print(f"āŒ Failed to create AI Config: {response.status_code} - {response.text}") return None @@ -204,13 +222,38 @@ def add_segment_rules(self, project_key, environment_key, segment_key, rules): print(f" āŒ Failed to add rules to segment '{segment_key}': {response.status_code} - {response.text}") return None - def update_ai_config_targeting(self, project_key, config_key, environment_key, targeting_rules): + def get_ai_config_targeting(self, project_key, config_key): + """Get existing AI Config targeting""" + url = f"{self.base_url}/api/v2/projects/{project_key}/ai-configs/{config_key}/targeting" + response = requests.get(url, headers=self.headers, timeout=self.REQUEST_TIMEOUT) + return response.json() if response.status_code == 200 else None + + def update_ai_config_targeting(self, project_key, config_key, environment_key, targeting_rules, force_targeting=False): """Update AI Config targeting using direct API call""" + # First check existing targeting to avoid duplicates + existing_targeting = self.get_ai_config_targeting(project_key, config_key) + existing_rules = [] + + if existing_targeting and "environments" in existing_targeting: + env_data = existing_targeting["environments"].get(environment_key, {}) + existing_rules = env_data.get("rules", []) + print(f"šŸ” Found {len(existing_rules)} existing targeting rules") + else: + print(f"šŸ” No existing targeting found") + url = f"{self.base_url}/api/v2/projects/{project_key}/ai-configs/{config_key}/targeting" - # Build targeting instructions from rules + # Build targeting instructions from rules, avoiding duplicates instructions = [] + # If there are existing rules, we need to be careful about conflicts + if existing_rules and not force_targeting: + print(f"āš ļø Found existing targeting rules. To avoid conflicts, skipping targeting setup.") + print(f" Use --force-targeting flag to override existing rules, or update manually in LaunchDarkly UI.") + return existing_targeting + elif existing_rules and force_targeting: + print(f"āš ļø Found existing targeting rules, but --force-targeting specified. Proceeding anyway.") + for rule in targeting_rules: instruction = { "kind": "addRule", @@ -219,6 +262,14 @@ def update_ai_config_targeting(self, project_key, config_key, environment_key, t "trackEvents": rule.get("trackEvents", False) } instructions.append(instruction) + print(f" āž• Adding new targeting rule for variation {rule.get('variationId')}") + + if not instructions: + if existing_rules: + print(f"āœ… Targeting rules already exist for environment '{environment_key}' (skipping to avoid conflicts)") + else: + print(f"āœ… All targeting rules already exist for environment '{environment_key}'") + return existing_targeting payload = { "environmentKey": environment_key, @@ -229,7 +280,7 @@ def update_ai_config_targeting(self, project_key, config_key, environment_key, t response = requests.patch(url, headers=self.headers, json=payload, timeout=self.REQUEST_TIMEOUT) if response.status_code == 200: - print(f"āœ… AI Config targeting updated for environment '{environment_key}'") + print(f"āœ… AI Config targeting updated for environment '{environment_key}' ({len(instructions)} new rules)") time.sleep(0.5) # Rate limiting delay return response.json() elif response.status_code == 429: @@ -258,6 +309,27 @@ def get_variation_map(self, ai_config_data): return variation_map def main(): + import sys + + # Check for help + if "--help" in sys.argv or "-h" in sys.argv: + print("LaunchDarkly AI Config Creator") + print("Usage: python create_ai_config.py [options]") + print("") + print("Options:") + print(" -s, --skip-existing Skip creating resources that already exist") + print(" Only update targeting rules") + print(" -f, --force-targeting Force update targeting rules even if they exist") + print(" -h, --help Show this help message") + print("") + print("Environment variables:") + print(" LD_API_KEY LaunchDarkly API key (required)") + return + + # Check for command line arguments + skip_existing = "--skip-existing" in sys.argv or "-s" in sys.argv + force_targeting = "--force-targeting" in sys.argv or "-f" in sys.argv + # Get API key from environment api_key = os.getenv("LD_API_KEY") if not api_key: @@ -277,36 +349,46 @@ def main(): project_key = manifest["project"]["key"] + if skip_existing: + print("⚔ Running in skip-existing mode - will only update targeting rules") + # Create segments if defined - if "segment" in manifest["project"]: + if "segment" in manifest["project"] and not skip_existing: print(f"šŸŽÆ Creating segments in project: {project_key}") - environment_key = "production" # Default environment + environment_key = "test" # Default environment for segment in manifest["project"]["segment"]: creator.create_segment(project_key, environment_key, segment) # Create AI Model Configs ai_config = manifest["project"]["ai-config"] - if "ai-model-configs" in ai_config: + if "ai-model-configs" in ai_config and not skip_existing: print(f"\nšŸ¤– Creating {len(ai_config['ai-model-configs'])} AI Model Configs...") for model_config in ai_config["ai-model-configs"]: creator.create_ai_model_config(project_key, model_config) # Create AI Config ai_config = manifest["project"]["ai-config"] - print(f"\nšŸš€ Creating AI Config in project: {project_key}") + if not skip_existing: + print(f"\nšŸš€ Creating AI Config in project: {project_key}") + else: + print(f"\nšŸ” Getting existing AI Config in project: {project_key}") config_result = creator.create_ai_config(project_key, ai_config) if not config_result: + print("āŒ Could not get AI Config - cannot proceed with targeting setup") return # Create variations - print(f"\nšŸ“ Creating {len(ai_config['variations'])} variations...") - variation_map = {} - for variation in ai_config["variations"]: - result = creator.create_variation(project_key, ai_config["key"], variation) - if result and "_id" in result: - variation_map[variation["key"]] = result["_id"] # Store variation ID + if not skip_existing: + print(f"\nšŸ“ Creating {len(ai_config['variations'])} variations...") + variation_map = {} + for variation in ai_config["variations"]: + result = creator.create_variation(project_key, ai_config["key"], variation) + if result and "_id" in result: + variation_map[variation["key"]] = result["_id"] # Store variation ID + else: + variation_map = {} # If no variations were created (already exist), get fresh AI Config data if not variation_map: @@ -323,7 +405,7 @@ def main(): if "rules" in ai_config and variation_map: print(f"\nšŸŽÆ Setting up AI Config targeting rules...") print(f"šŸ“Š Available variations: {list(variation_map.keys())}") - environment_key = "production" # Default environment + environment_key = "test" # Default environment # Build targeting rules with variation indices targeting_rules = [] @@ -351,7 +433,7 @@ def main(): print(f"šŸ“‹ Total targeting rules created: {len(targeting_rules)}") if targeting_rules: - creator.update_ai_config_targeting(project_key, ai_config["key"], environment_key, targeting_rules) + creator.update_ai_config_targeting(project_key, ai_config["key"], environment_key, targeting_rules, force_targeting) print(f"\n✨ Setup complete!") diff --git a/launchdarkly/ai_config_strands/bootstrap/requirements_ai_config.txt b/launchdarkly/ai_config_strands/bootstrap/requirements_ai_config.txt index 34304ae..a17184b 100644 --- a/launchdarkly/ai_config_strands/bootstrap/requirements_ai_config.txt +++ b/launchdarkly/ai_config_strands/bootstrap/requirements_ai_config.txt @@ -1,2 +1,5 @@ requests>=2.31.0 PyYAML>=6.0 +python-dotenv>=1.0.0 +strands-agents +strands-agents-tools \ No newline at end of file diff --git a/launchdarkly/ai_config_strands/utils/ld_ai_config_utils.py b/launchdarkly/ai_config_strands/utils/ld_ai_config_utils.py index 20172d6..544050e 100644 --- a/launchdarkly/ai_config_strands/utils/ld_ai_config_utils.py +++ b/launchdarkly/ai_config_strands/utils/ld_ai_config_utils.py @@ -177,7 +177,7 @@ def load_ai_config_prompt(agent_role, ai_config_id=None, fallback_file_path=None fallback_config = AIConfig( enabled=True, model=ModelConfig( - name="anthropic.claude-v2:1", + name="us.anthropic.claude-3-5-sonnet-20241022-v2:0", parameters={"temperature": 0.7, "max_tokens": 2000}, ), messages=[LDMessage(role="system", content=fallback_prompt)],