ambivo-agents 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,22 @@
1
1
  # ambivo_agents/config/loader.py
2
2
  """
3
- Configuration loader for ambivo_agents.
4
- All configurations must be sourced from agent_config.yaml - no defaults.
3
+ Enhanced configuration loader for ambivo_agents.
4
+ Supports both YAML file and environment variables for configuration.
5
+ YAML file is now OPTIONAL when environment variables are provided.
5
6
  """
6
7
 
7
8
  import os
8
- import yaml
9
+ import logging
9
10
  from pathlib import Path
10
- from typing import Dict, Any
11
+ from typing import Dict, Any, Optional, Union, List
12
+
13
+ # Try to import yaml, but make it optional
14
+ try:
15
+ import yaml
16
+
17
+ YAML_AVAILABLE = True
18
+ except ImportError:
19
+ YAML_AVAILABLE = False
11
20
 
12
21
 
13
22
  class ConfigurationError(Exception):
@@ -15,30 +24,280 @@ class ConfigurationError(Exception):
15
24
  pass
16
25
 
17
26
 
18
- def load_config(config_path: str = None) -> Dict[str, Any]:
27
+ # Environment variable prefix for all ambivo_agents settings
28
+ ENV_PREFIX = "AMBIVO_AGENTS_"
29
+
30
+ # Environment variable mapping for configuration sections
31
+ ENV_VARIABLE_MAPPING = {
32
+ # Redis Configuration
33
+ f"{ENV_PREFIX}REDIS_HOST": ("redis", "host"),
34
+ f"{ENV_PREFIX}REDIS_PORT": ("redis", "port"),
35
+ f"{ENV_PREFIX}REDIS_PASSWORD": ("redis", "password"),
36
+ f"{ENV_PREFIX}REDIS_DB": ("redis", "db"),
37
+
38
+ # LLM Configuration
39
+ f"{ENV_PREFIX}LLM_PREFERRED_PROVIDER": ("llm", "preferred_provider"),
40
+ f"{ENV_PREFIX}LLM_TEMPERATURE": ("llm", "temperature"),
41
+ f"{ENV_PREFIX}LLM_MAX_TOKENS": ("llm", "max_tokens"),
42
+ f"{ENV_PREFIX}LLM_OPENAI_API_KEY": ("llm", "openai_api_key"),
43
+ f"{ENV_PREFIX}OPENAI_API_KEY": ("llm", "openai_api_key"), # Alternative
44
+ f"{ENV_PREFIX}LLM_ANTHROPIC_API_KEY": ("llm", "anthropic_api_key"),
45
+ f"{ENV_PREFIX}ANTHROPIC_API_KEY": ("llm", "anthropic_api_key"), # Alternative
46
+ f"{ENV_PREFIX}LLM_VOYAGE_API_KEY": ("llm", "voyage_api_key"),
47
+ f"{ENV_PREFIX}VOYAGE_API_KEY": ("llm", "voyage_api_key"), # Alternative
48
+ f"{ENV_PREFIX}LLM_AWS_ACCESS_KEY_ID": ("llm", "aws_access_key_id"),
49
+ f"{ENV_PREFIX}AWS_ACCESS_KEY_ID": ("llm", "aws_access_key_id"), # Alternative
50
+ f"{ENV_PREFIX}LLM_AWS_SECRET_ACCESS_KEY": ("llm", "aws_secret_access_key"),
51
+ f"{ENV_PREFIX}AWS_SECRET_ACCESS_KEY": ("llm", "aws_secret_access_key"), # Alternative
52
+ f"{ENV_PREFIX}LLM_AWS_REGION": ("llm", "aws_region"),
53
+ f"{ENV_PREFIX}AWS_REGION": ("llm", "aws_region"), # Alternative
54
+
55
+ # Agent Capabilities
56
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_KNOWLEDGE_BASE": ("agent_capabilities", "enable_knowledge_base"),
57
+ f"{ENV_PREFIX}ENABLE_KNOWLEDGE_BASE": ("agent_capabilities", "enable_knowledge_base"), # Alternative
58
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_WEB_SEARCH": ("agent_capabilities", "enable_web_search"),
59
+ f"{ENV_PREFIX}ENABLE_WEB_SEARCH": ("agent_capabilities", "enable_web_search"), # Alternative
60
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_CODE_EXECUTION": ("agent_capabilities", "enable_code_execution"),
61
+ f"{ENV_PREFIX}ENABLE_CODE_EXECUTION": ("agent_capabilities", "enable_code_execution"), # Alternative
62
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_WEB_SCRAPING": ("agent_capabilities", "enable_web_scraping"),
63
+ f"{ENV_PREFIX}ENABLE_WEB_SCRAPING": ("agent_capabilities", "enable_web_scraping"), # Alternative
64
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_MEDIA_EDITOR": ("agent_capabilities", "enable_media_editor"),
65
+ f"{ENV_PREFIX}ENABLE_MEDIA_EDITOR": ("agent_capabilities", "enable_media_editor"), # Alternative
66
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_YOUTUBE_DOWNLOAD": ("agent_capabilities", "enable_youtube_download"),
67
+ f"{ENV_PREFIX}ENABLE_YOUTUBE_DOWNLOAD": ("agent_capabilities", "enable_youtube_download"), # Alternative
68
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_PROXY_MODE": ("agent_capabilities", "enable_proxy_mode"),
69
+ f"{ENV_PREFIX}ENABLE_PROXY_MODE": ("agent_capabilities", "enable_proxy_mode"), # Alternative
70
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_FILE_PROCESSING": ("agent_capabilities", "enable_file_processing"),
71
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_WEB_INGESTION": ("agent_capabilities", "enable_web_ingestion"),
72
+ f"{ENV_PREFIX}AGENT_CAPABILITIES_ENABLE_API_CALLS": ("agent_capabilities", "enable_api_calls"),
73
+
74
+ # Web Search Configuration
75
+ f"{ENV_PREFIX}WEB_SEARCH_BRAVE_API_KEY": ("web_search", "brave_api_key"),
76
+ f"{ENV_PREFIX}BRAVE_API_KEY": ("web_search", "brave_api_key"), # Alternative
77
+ f"{ENV_PREFIX}WEB_SEARCH_AVESAPI_API_KEY": ("web_search", "avesapi_api_key"),
78
+ f"{ENV_PREFIX}AVES_API_KEY": ("web_search", "avesapi_api_key"), # Alternative
79
+ f"{ENV_PREFIX}WEB_SEARCH_DEFAULT_MAX_RESULTS": ("web_search", "default_max_results"),
80
+ f"{ENV_PREFIX}WEB_SEARCH_MAX_RESULTS": ("web_search", "default_max_results"), # Alternative
81
+
82
+ # Knowledge Base Configuration
83
+ f"{ENV_PREFIX}KNOWLEDGE_BASE_QDRANT_URL": ("knowledge_base", "qdrant_url"),
84
+ f"{ENV_PREFIX}QDRANT_URL": ("knowledge_base", "qdrant_url"), # Alternative
85
+ f"{ENV_PREFIX}KNOWLEDGE_BASE_QDRANT_API_KEY": ("knowledge_base", "qdrant_api_key"),
86
+ f"{ENV_PREFIX}QDRANT_API_KEY": ("knowledge_base", "qdrant_api_key"), # Alternative
87
+ f"{ENV_PREFIX}KNOWLEDGE_BASE_CHUNK_SIZE": ("knowledge_base", "chunk_size"),
88
+ f"{ENV_PREFIX}KB_CHUNK_SIZE": ("knowledge_base", "chunk_size"), # Alternative
89
+ f"{ENV_PREFIX}KNOWLEDGE_BASE_SIMILARITY_TOP_K": ("knowledge_base", "similarity_top_k"),
90
+ f"{ENV_PREFIX}KB_SIMILARITY_TOP_K": ("knowledge_base", "similarity_top_k"), # Alternative
91
+
92
+ # Web Scraping Configuration
93
+ f"{ENV_PREFIX}WEB_SCRAPING_PROXY_CONFIG_HTTP_PROXY": ("web_scraping", "proxy_config", "http_proxy"),
94
+ f"{ENV_PREFIX}SCRAPER_PROXY": ("web_scraping", "proxy_config", "http_proxy"), # Alternative
95
+ f"{ENV_PREFIX}WEB_SCRAPING_PROXY_ENABLED": ("web_scraping", "proxy_enabled"),
96
+ f"{ENV_PREFIX}SCRAPER_PROXY_ENABLED": ("web_scraping", "proxy_enabled"), # Alternative
97
+ f"{ENV_PREFIX}WEB_SCRAPING_TIMEOUT": ("web_scraping", "timeout"),
98
+ f"{ENV_PREFIX}SCRAPER_TIMEOUT": ("web_scraping", "timeout"), # Alternative
99
+ f"{ENV_PREFIX}WEB_SCRAPING_DOCKER_IMAGE": ("web_scraping", "docker_image"),
100
+
101
+ # YouTube Download Configuration
102
+ f"{ENV_PREFIX}YOUTUBE_DOWNLOAD_DOWNLOAD_DIR": ("youtube_download", "download_dir"),
103
+ f"{ENV_PREFIX}YOUTUBE_DOWNLOAD_DIR": ("youtube_download", "download_dir"), # Alternative
104
+ f"{ENV_PREFIX}YOUTUBE_DOWNLOAD_DEFAULT_AUDIO_ONLY": ("youtube_download", "default_audio_only"),
105
+ f"{ENV_PREFIX}YOUTUBE_DEFAULT_AUDIO_ONLY": ("youtube_download", "default_audio_only"), # Alternative
106
+ f"{ENV_PREFIX}YOUTUBE_DOWNLOAD_TIMEOUT": ("youtube_download", "timeout"),
107
+ f"{ENV_PREFIX}YOUTUBE_TIMEOUT": ("youtube_download", "timeout"), # Alternative
108
+ f"{ENV_PREFIX}YOUTUBE_DOWNLOAD_DOCKER_IMAGE": ("youtube_download", "docker_image"),
109
+
110
+ # Media Editor Configuration
111
+ f"{ENV_PREFIX}MEDIA_EDITOR_INPUT_DIR": ("media_editor", "input_dir"),
112
+ f"{ENV_PREFIX}MEDIA_INPUT_DIR": ("media_editor", "input_dir"), # Alternative
113
+ f"{ENV_PREFIX}MEDIA_EDITOR_OUTPUT_DIR": ("media_editor", "output_dir"),
114
+ f"{ENV_PREFIX}MEDIA_OUTPUT_DIR": ("media_editor", "output_dir"), # Alternative
115
+ f"{ENV_PREFIX}MEDIA_EDITOR_TIMEOUT": ("media_editor", "timeout"),
116
+ f"{ENV_PREFIX}MEDIA_TIMEOUT": ("media_editor", "timeout"), # Alternative
117
+ f"{ENV_PREFIX}MEDIA_EDITOR_DOCKER_IMAGE": ("media_editor", "docker_image"),
118
+
119
+ # Docker Configuration
120
+ f"{ENV_PREFIX}DOCKER_MEMORY_LIMIT": ("docker", "memory_limit"),
121
+ f"{ENV_PREFIX}DOCKER_TIMEOUT": ("docker", "timeout"),
122
+ f"{ENV_PREFIX}DOCKER_IMAGES": ("docker", "images"),
123
+ f"{ENV_PREFIX}DOCKER_IMAGE": ("docker", "images"), # Alternative - will be converted to list
124
+ f"{ENV_PREFIX}DOCKER_WORK_DIR": ("docker", "work_dir"),
125
+
126
+ # Service Configuration
127
+ f"{ENV_PREFIX}SERVICE_MAX_SESSIONS": ("service", "max_sessions"),
128
+ f"{ENV_PREFIX}SERVICE_LOG_LEVEL": ("service", "log_level"),
129
+ f"{ENV_PREFIX}SERVICE_SESSION_TIMEOUT": ("service", "session_timeout"),
130
+ f"{ENV_PREFIX}SERVICE_ENABLE_METRICS": ("service", "enable_metrics"),
131
+ f"{ENV_PREFIX}SERVICE_LOG_TO_FILE": ("service", "log_to_file"),
132
+
133
+ # Moderator Configuration
134
+ f"{ENV_PREFIX}MODERATOR_DEFAULT_ENABLED_AGENTS": ("moderator", "default_enabled_agents"),
135
+ f"{ENV_PREFIX}MODERATOR_ENABLED_AGENTS": ("moderator", "default_enabled_agents"), # Alternative
136
+ f"{ENV_PREFIX}MODERATOR_ROUTING_CONFIDENCE_THRESHOLD": ("moderator", "routing", "confidence_threshold"),
137
+ f"{ENV_PREFIX}MODERATOR_CONFIDENCE_THRESHOLD": ("moderator", "routing", "confidence_threshold"), # Alternative
138
+ }
139
+
140
+ # Required environment variables for minimal configuration
141
+ REQUIRED_ENV_VARS = [
142
+ f"{ENV_PREFIX}REDIS_HOST",
143
+ f"{ENV_PREFIX}REDIS_PORT",
144
+ ]
145
+
146
+ # At least one LLM provider is required
147
+ LLM_PROVIDER_ENV_VARS = [
148
+ f"{ENV_PREFIX}LLM_OPENAI_API_KEY",
149
+ f"{ENV_PREFIX}OPENAI_API_KEY",
150
+ f"{ENV_PREFIX}LLM_ANTHROPIC_API_KEY",
151
+ f"{ENV_PREFIX}ANTHROPIC_API_KEY",
152
+ f"{ENV_PREFIX}LLM_AWS_ACCESS_KEY_ID",
153
+ f"{ENV_PREFIX}AWS_ACCESS_KEY_ID"
154
+ ]
155
+
156
+
157
+ def load_config(config_path: str = None, use_env_vars: bool = None) -> Dict[str, Any]:
19
158
  """
20
- Load configuration from agent_config.yaml.
159
+ Load configuration with OPTIONAL YAML file support.
160
+
161
+ Priority order:
162
+ 1. Environment variables (if detected or use_env_vars=True)
163
+ 2. YAML file (if available and no env vars)
164
+ 3. Minimal defaults (if nothing else available)
21
165
 
22
166
  Args:
23
- config_path: Optional path to config file. If None, searches for agent_config.yaml
24
- in current directory and parent directories.
167
+ config_path: Optional path to config file
168
+ use_env_vars: Force use of environment variables. If None, auto-detects.
25
169
 
26
170
  Returns:
27
171
  Configuration dictionary
28
172
 
29
173
  Raises:
30
- ConfigurationError: If config file is not found or invalid
174
+ ConfigurationError: If no valid configuration found
31
175
  """
176
+
177
+ config = {}
178
+ config_source = ""
179
+
180
+ # Auto-detect if we should use environment variables
181
+ if use_env_vars is None:
182
+ use_env_vars = _has_env_vars()
183
+
184
+ if use_env_vars:
185
+ # PRIMARY: Try environment variables first
186
+ try:
187
+ config = _load_config_from_env()
188
+ config_source = "environment variables"
189
+ logging.info("✅ Configuration loaded from environment variables")
190
+
191
+ # Validate env config
192
+ _validate_config(config)
193
+
194
+ # Add config source metadata
195
+ config['_config_source'] = config_source
196
+ return config
197
+
198
+ except ConfigurationError as e:
199
+ if _has_minimal_env_vars():
200
+ # If we have some env vars but they're incomplete, raise error
201
+ raise ConfigurationError(f"Incomplete environment variable configuration: {e}")
202
+ else:
203
+ # Fall back to YAML file
204
+ logging.warning(f"Environment variable config incomplete: {e}")
205
+ use_env_vars = False
206
+
207
+ if not use_env_vars:
208
+ # FALLBACK: Try YAML file
209
+ try:
210
+ yaml_config = _load_config_from_yaml(config_path)
211
+ if config:
212
+ # Merge env vars with YAML (env vars take precedence)
213
+ config = _merge_configs(yaml_config, config)
214
+ config_source = "YAML file + environment variables"
215
+ else:
216
+ config = yaml_config
217
+ config_source = "YAML file"
218
+
219
+ logging.info(f"✅ Configuration loaded from {config_source}")
220
+
221
+ except ConfigurationError as e:
222
+ if config:
223
+ # We have partial env config, use it even if YAML failed
224
+ logging.warning(f"YAML config failed, using environment variables: {e}")
225
+ config_source = "environment variables (partial)"
226
+ else:
227
+ # No config at all - use minimal defaults
228
+ logging.warning(f"Both environment variables and YAML failed: {e}")
229
+ config = _get_minimal_defaults()
230
+ config_source = "minimal defaults"
231
+
232
+ if not config:
233
+ raise ConfigurationError(
234
+ "No configuration found. Please either:\n"
235
+ "1. Set environment variables with AMBIVO_AGENTS_ prefix, OR\n"
236
+ "2. Create agent_config.yaml in your project directory\n\n"
237
+ f"Required environment variables: {REQUIRED_ENV_VARS + ['At least one of: ' + str(LLM_PROVIDER_ENV_VARS)]}"
238
+ )
239
+
240
+ # Add metadata about config source
241
+ config['_config_source'] = config_source
242
+
243
+ return config
244
+
245
+
246
+ def _has_env_vars() -> bool:
247
+ """Check if ANY ambivo agents environment variables are set."""
248
+ return any(os.getenv(env_var) for env_var in ENV_VARIABLE_MAPPING.keys())
249
+
250
+
251
+ def _has_minimal_env_vars() -> bool:
252
+ """Check if minimal required environment variables are set."""
253
+ # Check if we have Redis config
254
+ has_redis = any(os.getenv(var) for var in REQUIRED_ENV_VARS)
255
+
256
+ # Check if we have at least one LLM provider
257
+ has_llm = any(os.getenv(var) for var in LLM_PROVIDER_ENV_VARS)
258
+
259
+ return has_redis and has_llm
260
+
261
+
262
+ def _load_config_from_env() -> Dict[str, Any]:
263
+ """Load configuration from environment variables."""
264
+ config = {}
265
+
266
+ # Process all mapped environment variables
267
+ for env_var, config_path in ENV_VARIABLE_MAPPING.items():
268
+ value = os.getenv(env_var)
269
+ if value is not None:
270
+ _set_nested_value(config, config_path, _convert_env_value(value))
271
+
272
+ # Set defaults for sections that exist
273
+ _set_env_config_defaults(config)
274
+
275
+ # Validate that we have minimum required configuration
276
+ if not config.get('redis') or not config.get('llm'):
277
+ missing = []
278
+ if not config.get('redis'):
279
+ missing.append('redis')
280
+ if not config.get('llm'):
281
+ missing.append('llm')
282
+ raise ConfigurationError(f"Missing required sections from environment variables: {missing}")
283
+
284
+ return config
285
+
286
+
287
+ def _load_config_from_yaml(config_path: str = None) -> Dict[str, Any]:
288
+ """Load configuration from YAML file."""
289
+ if not YAML_AVAILABLE:
290
+ raise ConfigurationError("PyYAML is required to load YAML configuration files")
291
+
32
292
  if config_path:
33
293
  config_file = Path(config_path)
34
294
  else:
35
- # Search for agent_config.yaml starting from current directory
36
295
  config_file = _find_config_file()
37
296
 
38
297
  if not config_file or not config_file.exists():
39
298
  raise ConfigurationError(
40
- "agent_config.yaml not found. This file is required for ambivo_agents to function. "
41
- "Please create agent_config.yaml in your project root or specify the path explicitly."
299
+ f"agent_config.yaml not found{' at ' + str(config_path) if config_path else ' in current or parent directories'}. "
300
+ "Either create this file or use environment variables."
42
301
  )
43
302
 
44
303
  try:
@@ -48,9 +307,7 @@ def load_config(config_path: str = None) -> Dict[str, Any]:
48
307
  if not config:
49
308
  raise ConfigurationError("agent_config.yaml is empty or contains invalid YAML")
50
309
 
51
- # Validate required sections
52
310
  _validate_config(config)
53
-
54
311
  return config
55
312
 
56
313
  except yaml.YAMLError as e:
@@ -59,7 +316,239 @@ def load_config(config_path: str = None) -> Dict[str, Any]:
59
316
  raise ConfigurationError(f"Failed to load agent_config.yaml: {e}")
60
317
 
61
318
 
62
- def _find_config_file() -> Path:
319
+ def _get_minimal_defaults() -> Dict[str, Any]:
320
+ """Get minimal default configuration when nothing else is available."""
321
+ return {
322
+ 'redis': {
323
+ 'host': 'localhost',
324
+ 'port': 6379,
325
+ 'db': 0,
326
+ 'password': None
327
+ },
328
+ 'llm': {
329
+ 'preferred_provider': 'openai',
330
+ 'temperature': 0.7,
331
+ 'max_tokens': 4000
332
+ },
333
+ 'agent_capabilities': {
334
+ 'enable_knowledge_base': False,
335
+ 'enable_web_search': False,
336
+ 'enable_code_execution': True,
337
+ 'enable_file_processing': False,
338
+ 'enable_web_ingestion': False,
339
+ 'enable_api_calls': False,
340
+ 'enable_web_scraping': False,
341
+ 'enable_proxy_mode': True,
342
+ 'enable_media_editor': False,
343
+ 'enable_youtube_download': False
344
+ },
345
+ 'service': {
346
+ 'enable_metrics': True,
347
+ 'log_level': 'INFO',
348
+ 'max_sessions': 100,
349
+ 'session_timeout': 3600
350
+ },
351
+ 'moderator': {
352
+ 'default_enabled_agents': ['assistant']
353
+ },
354
+ 'docker': {
355
+ 'images': ['sgosain/amb-ubuntu-python-public-pod'],
356
+ 'memory_limit': '512m',
357
+ 'timeout': 60,
358
+ 'work_dir': '/opt/ambivo/work_dir'
359
+ }
360
+ }
361
+
362
+
363
+ def _set_nested_value(config: Dict[str, Any], path: tuple, value: Any) -> None:
364
+ """Set a nested value in configuration dictionary."""
365
+ current = config
366
+
367
+ # Navigate to the parent of the target key
368
+ for key in path[:-1]:
369
+ if key not in current:
370
+ current[key] = {}
371
+ current = current[key]
372
+
373
+ # Handle special cases
374
+ final_key = path[-1]
375
+
376
+ if final_key == "images" and isinstance(value, str):
377
+ # Docker images should be a list
378
+ current[final_key] = [value]
379
+ elif final_key == "default_enabled_agents" and isinstance(value, str):
380
+ # Moderator enabled agents should be a list
381
+ current[final_key] = [agent.strip() for agent in value.split(",")]
382
+ else:
383
+ current[final_key] = value
384
+
385
+
386
+ def _convert_env_value(value: str) -> Union[str, int, float, bool]:
387
+ """Convert environment variable string to appropriate type."""
388
+ if not value:
389
+ return None
390
+
391
+ # Boolean conversion
392
+ if value.lower() in ('true', 'yes', '1', 'on'):
393
+ return True
394
+ elif value.lower() in ('false', 'no', '0', 'off'):
395
+ return False
396
+
397
+ # Integer conversion
398
+ try:
399
+ if '.' not in value and value.lstrip('-').isdigit():
400
+ return int(value)
401
+ except ValueError:
402
+ pass
403
+
404
+ # Float conversion
405
+ try:
406
+ return float(value)
407
+ except ValueError:
408
+ pass
409
+
410
+ # String (default)
411
+ return value
412
+
413
+
414
+ def _set_env_config_defaults(config: Dict[str, Any]) -> None:
415
+ """Set default values for configuration sections when using environment variables."""
416
+
417
+ # Set Redis defaults
418
+ if 'redis' in config:
419
+ config['redis'].setdefault('db', 0)
420
+
421
+ # Set LLM defaults
422
+ if 'llm' in config:
423
+ config['llm'].setdefault('temperature', 0.5)
424
+ config['llm'].setdefault('max_tokens', 4000)
425
+ config['llm'].setdefault('preferred_provider', 'openai')
426
+
427
+ # Set agent capabilities defaults
428
+ if 'agent_capabilities' in config:
429
+ caps = config['agent_capabilities']
430
+ caps.setdefault('enable_file_processing', True)
431
+ caps.setdefault('enable_web_ingestion', True)
432
+ caps.setdefault('enable_api_calls', True)
433
+ caps.setdefault('enable_agent_collaboration', True)
434
+ caps.setdefault('enable_result_synthesis', True)
435
+ caps.setdefault('enable_multi_source_validation', True)
436
+ caps.setdefault('max_concurrent_operations', 5)
437
+ caps.setdefault('operation_timeout_seconds', 30)
438
+ caps.setdefault('max_memory_usage_mb', 500)
439
+
440
+ # Set web search defaults
441
+ if 'web_search' in config:
442
+ ws = config['web_search']
443
+ ws.setdefault('default_max_results', 10)
444
+ ws.setdefault('search_timeout_seconds', 10)
445
+ ws.setdefault('enable_caching', True)
446
+ ws.setdefault('cache_ttl_minutes', 30)
447
+
448
+ # Set knowledge base defaults
449
+ if 'knowledge_base' in config:
450
+ kb = config['knowledge_base']
451
+ kb.setdefault('chunk_size', 1024)
452
+ kb.setdefault('chunk_overlap', 20)
453
+ kb.setdefault('similarity_top_k', 5)
454
+ kb.setdefault('vector_size', 1536)
455
+ kb.setdefault('distance_metric', 'cosine')
456
+ kb.setdefault('default_collection_prefix', 'kb')
457
+ kb.setdefault('max_file_size_mb', 50)
458
+
459
+ # Set web scraping defaults
460
+ if 'web_scraping' in config:
461
+ ws = config['web_scraping']
462
+ ws.setdefault('timeout', 120)
463
+ ws.setdefault('proxy_enabled', False)
464
+ ws.setdefault('docker_image', 'sgosain/amb-ubuntu-python-public-pod')
465
+
466
+ # Set YouTube download defaults
467
+ if 'youtube_download' in config:
468
+ yt = config['youtube_download']
469
+ yt.setdefault('download_dir', './youtube_downloads')
470
+ yt.setdefault('timeout', 600)
471
+ yt.setdefault('memory_limit', '1g')
472
+ yt.setdefault('default_audio_only', True)
473
+ yt.setdefault('docker_image', 'sgosain/amb-ubuntu-python-public-pod')
474
+
475
+ # Set media editor defaults
476
+ if 'media_editor' in config:
477
+ me = config['media_editor']
478
+ me.setdefault('input_dir', './examples/media_input')
479
+ me.setdefault('output_dir', './examples/media_output')
480
+ me.setdefault('timeout', 300)
481
+ me.setdefault('docker_image', 'sgosain/amb-ubuntu-python-public-pod')
482
+ me.setdefault('work_dir', '/opt/ambivo/work_dir')
483
+
484
+ # Set Docker defaults
485
+ if 'docker' in config:
486
+ docker = config['docker']
487
+ docker.setdefault('memory_limit', '512m')
488
+ docker.setdefault('timeout', 60)
489
+ docker.setdefault('work_dir', '/opt/ambivo/work_dir')
490
+ if 'images' not in docker:
491
+ docker['images'] = ['sgosain/amb-ubuntu-python-public-pod']
492
+
493
+ # Set service defaults
494
+ if 'service' in config:
495
+ service = config['service']
496
+ service.setdefault('max_sessions', 100)
497
+ service.setdefault('session_timeout', 3600)
498
+ service.setdefault('log_level', 'INFO')
499
+ service.setdefault('log_to_file', False)
500
+ service.setdefault('enable_metrics', True)
501
+
502
+ # Set moderator defaults
503
+ if 'moderator' in config:
504
+ mod = config['moderator']
505
+ if 'default_enabled_agents' not in mod:
506
+ # Set default based on what's enabled
507
+ enabled_agents = ['assistant']
508
+ if config.get('agent_capabilities', {}).get('enable_knowledge_base'):
509
+ enabled_agents.append('knowledge_base')
510
+ if config.get('agent_capabilities', {}).get('enable_web_search'):
511
+ enabled_agents.append('web_search')
512
+ if config.get('agent_capabilities', {}).get('enable_youtube_download'):
513
+ enabled_agents.append('youtube_download')
514
+ if config.get('agent_capabilities', {}).get('enable_media_editor'):
515
+ enabled_agents.append('media_editor')
516
+ if config.get('agent_capabilities', {}).get('enable_web_scraping'):
517
+ enabled_agents.append('web_scraper')
518
+ mod['default_enabled_agents'] = enabled_agents
519
+
520
+ # Set routing defaults
521
+ if 'routing' not in mod:
522
+ mod['routing'] = {}
523
+ mod['routing'].setdefault('confidence_threshold', 0.6)
524
+ mod['routing'].setdefault('enable_multi_agent', True)
525
+ mod['routing'].setdefault('fallback_agent', 'assistant')
526
+ mod['routing'].setdefault('max_routing_attempts', 3)
527
+
528
+ # Set memory management defaults
529
+ config.setdefault('memory_management', {
530
+ 'compression': {'enabled': True, 'algorithm': 'lz4', 'compression_level': 1},
531
+ 'cache': {'enabled': True, 'max_size': 1000, 'ttl_seconds': 300},
532
+ 'backup': {'enabled': True, 'interval_minutes': 60, 'backup_directory': './backups'}
533
+ })
534
+
535
+
536
+ def _merge_configs(yaml_config: Dict[str, Any], env_config: Dict[str, Any]) -> Dict[str, Any]:
537
+ """Merge YAML and environment configurations (env takes precedence)."""
538
+
539
+ def deep_merge(base: dict, override: dict) -> dict:
540
+ result = base.copy()
541
+ for key, value in override.items():
542
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
543
+ result[key] = deep_merge(result[key], value)
544
+ else:
545
+ result[key] = value
546
+ return result
547
+
548
+ return deep_merge(yaml_config, env_config)
549
+
550
+
551
+ def _find_config_file() -> Optional[Path]:
63
552
  """Find agent_config.yaml in current directory or parent directories."""
64
553
  current_dir = Path.cwd()
65
554
 
@@ -78,15 +567,7 @@ def _find_config_file() -> Path:
78
567
 
79
568
 
80
569
  def _validate_config(config: Dict[str, Any]) -> None:
81
- """
82
- Validate that required configuration sections exist.
83
-
84
- Args:
85
- config: Configuration dictionary
86
-
87
- Raises:
88
- ConfigurationError: If required sections are missing
89
- """
570
+ """Validate that required configuration sections exist."""
90
571
  required_sections = ['redis', 'llm']
91
572
  missing_sections = []
92
573
 
@@ -97,7 +578,7 @@ def _validate_config(config: Dict[str, Any]) -> None:
97
578
  if missing_sections:
98
579
  raise ConfigurationError(
99
580
  f"Required configuration sections missing: {missing_sections}. "
100
- "Please check your agent_config.yaml file."
581
+ "Please check your configuration."
101
582
  )
102
583
 
103
584
  # Validate Redis config
@@ -124,29 +605,50 @@ def _validate_config(config: Dict[str, Any]) -> None:
124
605
 
125
606
 
126
607
  def get_config_section(section: str, config: Dict[str, Any] = None) -> Dict[str, Any]:
127
- """
128
- Get a specific configuration section.
129
-
130
- Args:
131
- section: Section name (e.g., 'redis', 'llm', 'web_scraping')
132
- config: Optional config dict. If None, loads from file.
133
-
134
- Returns:
135
- Configuration section dictionary
136
-
137
- Raises:
138
- ConfigurationError: If section is not found
139
- """
608
+ """Get a specific configuration section."""
140
609
  if config is None:
141
610
  config = load_config()
142
611
 
143
612
  if section not in config:
144
- raise ConfigurationError(f"Configuration section '{section}' not found in agent_config.yaml")
613
+ # Return empty dict instead of raising error to allow graceful fallback
614
+ logging.warning(f"Configuration section '{section}' not found")
615
+ return {}
145
616
 
146
617
  return config[section]
147
618
 
148
619
 
149
- # Centralized capability and agent type mapping - UPDATED WITH YOUTUBE
620
+ # Environment variable convenience functions
621
+ def print_env_var_template():
622
+ """Print a template of all available environment variables."""
623
+ print("# Ambivo Agents Environment Variables Template")
624
+ print("# Copy and customize these environment variables as needed")
625
+ print("# All variables use the AMBIVO_AGENTS_ prefix")
626
+ print()
627
+
628
+ sections = {}
629
+ for env_var, path in ENV_VARIABLE_MAPPING.items():
630
+ section = path[0]
631
+ if section not in sections:
632
+ sections[section] = []
633
+ sections[section].append(env_var)
634
+
635
+ for section, vars in sections.items():
636
+ print(f"# {section.upper()} Configuration")
637
+ for var in sorted(vars):
638
+ print(f"# export {var}=your_value_here")
639
+ print()
640
+
641
+
642
+ def get_current_config_source() -> str:
643
+ """Get the source of the current configuration."""
644
+ try:
645
+ config = load_config()
646
+ return config.get('_config_source', 'unknown')
647
+ except:
648
+ return 'none'
649
+
650
+
651
+ # Backward compatibility - keep existing functions
150
652
  CAPABILITY_TO_AGENT_TYPE = {
151
653
  'assistant': 'assistant',
152
654
  'code_execution': 'code_executor',
@@ -155,7 +657,7 @@ CAPABILITY_TO_AGENT_TYPE = {
155
657
  'knowledge_base': 'knowledge_base',
156
658
  'web_search': 'web_search',
157
659
  'media_editor': 'media_editor',
158
- 'youtube_download': 'youtube_download' # NEW
660
+ 'youtube_download': 'youtube_download'
159
661
  }
160
662
 
161
663
  CONFIG_FLAG_TO_CAPABILITY = {
@@ -163,38 +665,26 @@ CONFIG_FLAG_TO_CAPABILITY = {
163
665
  'enable_knowledge_base': 'knowledge_base',
164
666
  'enable_web_search': 'web_search',
165
667
  'enable_media_editor': 'media_editor',
166
- 'enable_youtube_download': 'youtube_download', # NEW
668
+ 'enable_youtube_download': 'youtube_download',
167
669
  'enable_code_execution': 'code_execution',
168
670
  'enable_proxy_mode': 'proxy'
169
671
  }
170
672
 
171
673
 
172
674
  def validate_agent_capabilities(config: Dict[str, Any] = None) -> Dict[str, bool]:
173
- """
174
- Validate and return available agent capabilities based on configuration.
175
-
176
- This is the SINGLE SOURCE OF TRUTH for capability checking.
177
-
178
- Args:
179
- config: Optional config dict. If None, loads from file.
180
-
181
- Returns:
182
- Dictionary of capability_name -> enabled status
183
- """
675
+ """Validate and return available agent capabilities based on configuration."""
184
676
  if config is None:
185
677
  config = load_config()
186
678
 
187
- # Always available capabilities
188
679
  capabilities = {
189
- 'assistant': True, # Always available
190
- 'code_execution': True, # Always available with Docker
191
- 'proxy': True, # Always available
680
+ 'assistant': True,
681
+ 'code_execution': True,
682
+ 'moderator': True,
683
+ 'proxy': True,
192
684
  }
193
685
 
194
- # Get agent_capabilities section
195
686
  agent_caps = config.get('agent_capabilities', {})
196
687
 
197
- # Check optional capabilities based on both flag AND config section existence
198
688
  capabilities['web_scraping'] = (
199
689
  agent_caps.get('enable_web_scraping', False) and
200
690
  'web_scraping' in config
@@ -215,7 +705,6 @@ def validate_agent_capabilities(config: Dict[str, Any] = None) -> Dict[str, bool
215
705
  'media_editor' in config
216
706
  )
217
707
 
218
- # YouTube download capability
219
708
  capabilities['youtube_download'] = (
220
709
  agent_caps.get('enable_youtube_download', False) and
221
710
  'youtube_download' in config
@@ -225,31 +714,15 @@ def validate_agent_capabilities(config: Dict[str, Any] = None) -> Dict[str, bool
225
714
 
226
715
 
227
716
  def get_available_agent_types(config: Dict[str, Any] = None) -> Dict[str, bool]:
228
- """
229
- Get available agent types based on capabilities.
230
-
231
- This maps capabilities to agent type names consistently.
232
-
233
- Args:
234
- config: Optional config dict. If None, loads from file.
235
-
236
- Returns:
237
- Dictionary of agent_type_name -> available status
238
- """
717
+ """Get available agent types based on capabilities."""
239
718
  try:
240
719
  capabilities = validate_agent_capabilities(config)
241
-
242
- # Map capabilities to agent types using centralized mapping
243
720
  agent_types = {}
244
721
  for capability, agent_type in CAPABILITY_TO_AGENT_TYPE.items():
245
722
  agent_types[agent_type] = capabilities.get(capability, False)
246
-
247
723
  return agent_types
248
-
249
724
  except Exception as e:
250
- import logging
251
725
  logging.error(f"Error getting available agent types: {e}")
252
- # Safe fallback
253
726
  return {
254
727
  'assistant': True,
255
728
  'code_executor': True,
@@ -262,30 +735,14 @@ def get_available_agent_types(config: Dict[str, Any] = None) -> Dict[str, bool]:
262
735
  }
263
736
 
264
737
 
265
- def get_enabled_capabilities(config: Dict[str, Any] = None) -> list[str]:
266
- """
267
- Get list of enabled capability names.
268
-
269
- Args:
270
- config: Optional config dict. If None, loads from file.
271
-
272
- Returns:
273
- List of enabled capability names
274
- """
738
+ def get_enabled_capabilities(config: Dict[str, Any] = None) -> List[str]:
739
+ """Get list of enabled capability names."""
275
740
  capabilities = validate_agent_capabilities(config)
276
741
  return [cap for cap, enabled in capabilities.items() if enabled]
277
742
 
278
743
 
279
- def get_available_agent_type_names(config: Dict[str, Any] = None) -> list[str]:
280
- """
281
- Get list of available agent type names.
282
-
283
- Args:
284
- config: Optional config dict. If None, loads from file.
285
-
286
- Returns:
287
- List of available agent type names
288
- """
744
+ def get_available_agent_type_names(config: Dict[str, Any] = None) -> List[str]:
745
+ """Get list of available agent type names."""
289
746
  agent_types = get_available_agent_types(config)
290
747
  return [agent_type for agent_type, available in agent_types.items() if available]
291
748
 
@@ -298,4 +755,73 @@ def capability_to_agent_type(capability: str) -> str:
298
755
  def agent_type_to_capability(agent_type: str) -> str:
299
756
  """Convert agent type name to capability name."""
300
757
  reverse_mapping = {v: k for k, v in CAPABILITY_TO_AGENT_TYPE.items()}
301
- return reverse_mapping.get(agent_type, agent_type)
758
+ return reverse_mapping.get(agent_type, agent_type)
759
+
760
+
761
+ def debug_env_vars():
762
+ """Debug function to print all AMBIVO_AGENTS_ environment variables."""
763
+ print("🔍 AMBIVO_AGENTS Environment Variables Debug")
764
+ print("=" * 50)
765
+
766
+ env_vars = {k: v for k, v in os.environ.items() if k.startswith('AMBIVO_AGENTS_')}
767
+
768
+ if not env_vars:
769
+ print("❌ No AMBIVO_AGENTS_ environment variables found")
770
+ return
771
+
772
+ print(f"✅ Found {len(env_vars)} environment variables:")
773
+ for key, value in sorted(env_vars.items()):
774
+ # Mask sensitive values
775
+ if any(sensitive in key.lower() for sensitive in ['key', 'password', 'secret']):
776
+ masked_value = value[:8] + "..." if len(value) > 8 else "***"
777
+ print(f" {key} = {masked_value}")
778
+ else:
779
+ print(f" {key} = {value}")
780
+
781
+ print("\n🔧 Configuration loading test:")
782
+ try:
783
+ config = load_config()
784
+ print(f"✅ Config loaded successfully from: {config.get('_config_source', 'unknown')}")
785
+ print(f"📊 Sections: {list(config.keys())}")
786
+ except Exception as e:
787
+ print(f"❌ Config loading failed: {e}")
788
+
789
+
790
+ def check_config_health() -> Dict[str, Any]:
791
+ """Check the health of the current configuration."""
792
+ health = {
793
+ 'config_loaded': False,
794
+ 'config_source': 'none',
795
+ 'redis_configured': False,
796
+ 'llm_configured': False,
797
+ 'agents_enabled': [],
798
+ 'errors': []
799
+ }
800
+
801
+ try:
802
+ config = load_config()
803
+ health['config_loaded'] = True
804
+ health['config_source'] = config.get('_config_source', 'unknown')
805
+
806
+ # Check Redis
807
+ redis_config = config.get('redis', {})
808
+ if redis_config.get('host') and redis_config.get('port'):
809
+ health['redis_configured'] = True
810
+ else:
811
+ health['errors'].append('Redis not properly configured')
812
+
813
+ # Check LLM
814
+ llm_config = config.get('llm', {})
815
+ if any(key in llm_config for key in ['openai_api_key', 'anthropic_api_key', 'aws_access_key_id']):
816
+ health['llm_configured'] = True
817
+ else:
818
+ health['errors'].append('No LLM provider configured')
819
+
820
+ # Check enabled agents
821
+ capabilities = validate_agent_capabilities(config)
822
+ health['agents_enabled'] = [cap for cap, enabled in capabilities.items() if enabled]
823
+
824
+ except Exception as e:
825
+ health['errors'].append(f"Configuration error: {e}")
826
+
827
+ return health