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