contextagent 0.1.0__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.
Files changed (66) hide show
  1. agentz/agent/base.py +262 -0
  2. agentz/artifacts/__init__.py +5 -0
  3. agentz/artifacts/artifact_writer.py +538 -0
  4. agentz/artifacts/reporter.py +235 -0
  5. agentz/artifacts/terminal_writer.py +100 -0
  6. agentz/context/__init__.py +6 -0
  7. agentz/context/context.py +91 -0
  8. agentz/context/conversation.py +205 -0
  9. agentz/context/data_store.py +208 -0
  10. agentz/llm/llm_setup.py +156 -0
  11. agentz/mcp/manager.py +142 -0
  12. agentz/mcp/patches.py +88 -0
  13. agentz/mcp/servers/chrome_devtools/server.py +14 -0
  14. agentz/profiles/base.py +108 -0
  15. agentz/profiles/data/data_analysis.py +38 -0
  16. agentz/profiles/data/data_loader.py +35 -0
  17. agentz/profiles/data/evaluation.py +43 -0
  18. agentz/profiles/data/model_training.py +47 -0
  19. agentz/profiles/data/preprocessing.py +47 -0
  20. agentz/profiles/data/visualization.py +47 -0
  21. agentz/profiles/manager/evaluate.py +51 -0
  22. agentz/profiles/manager/memory.py +62 -0
  23. agentz/profiles/manager/observe.py +48 -0
  24. agentz/profiles/manager/routing.py +66 -0
  25. agentz/profiles/manager/writer.py +51 -0
  26. agentz/profiles/mcp/browser.py +21 -0
  27. agentz/profiles/mcp/chrome.py +21 -0
  28. agentz/profiles/mcp/notion.py +21 -0
  29. agentz/runner/__init__.py +74 -0
  30. agentz/runner/base.py +28 -0
  31. agentz/runner/executor.py +320 -0
  32. agentz/runner/hooks.py +110 -0
  33. agentz/runner/iteration.py +142 -0
  34. agentz/runner/patterns.py +215 -0
  35. agentz/runner/tracker.py +188 -0
  36. agentz/runner/utils.py +45 -0
  37. agentz/runner/workflow.py +250 -0
  38. agentz/tools/__init__.py +20 -0
  39. agentz/tools/data_tools/__init__.py +17 -0
  40. agentz/tools/data_tools/data_analysis.py +152 -0
  41. agentz/tools/data_tools/data_loading.py +92 -0
  42. agentz/tools/data_tools/evaluation.py +175 -0
  43. agentz/tools/data_tools/helpers.py +120 -0
  44. agentz/tools/data_tools/model_training.py +192 -0
  45. agentz/tools/data_tools/preprocessing.py +229 -0
  46. agentz/tools/data_tools/visualization.py +281 -0
  47. agentz/utils/__init__.py +69 -0
  48. agentz/utils/config.py +708 -0
  49. agentz/utils/helpers.py +10 -0
  50. agentz/utils/parsers.py +142 -0
  51. agentz/utils/printer.py +539 -0
  52. contextagent-0.1.0.dist-info/METADATA +269 -0
  53. contextagent-0.1.0.dist-info/RECORD +66 -0
  54. contextagent-0.1.0.dist-info/WHEEL +5 -0
  55. contextagent-0.1.0.dist-info/licenses/LICENSE +21 -0
  56. contextagent-0.1.0.dist-info/top_level.txt +2 -0
  57. pipelines/base.py +972 -0
  58. pipelines/data_scientist.py +97 -0
  59. pipelines/data_scientist_memory.py +151 -0
  60. pipelines/experience_learner.py +0 -0
  61. pipelines/prompt_generator.py +0 -0
  62. pipelines/simple.py +78 -0
  63. pipelines/simple_browser.py +145 -0
  64. pipelines/simple_chrome.py +75 -0
  65. pipelines/simple_notion.py +103 -0
  66. pipelines/tool_builder.py +0 -0
agentz/utils/config.py ADDED
@@ -0,0 +1,708 @@
1
+ """
2
+ Configuration management utilities for loading and processing config files.
3
+
4
+ This module provides both simple file I/O utilities and the core BaseConfig class
5
+ for strongly-typed pipeline configuration objects.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Mapping, MutableMapping, Optional, Union, List, Tuple, Callable
15
+
16
+ import yaml
17
+ from dotenv import load_dotenv
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+
20
+
21
+ # ============================================================================
22
+ # Simple File I/O Utilities
23
+ # ============================================================================
24
+
25
+ def load_json_config(config_path: str) -> Dict[str, Any]:
26
+ """Load JSON configuration file."""
27
+ with open(config_path, 'r') as f:
28
+ return json.load(f)
29
+
30
+
31
+ def save_json_config(config: Dict[str, Any], config_path: str) -> None:
32
+ """Save configuration to JSON file."""
33
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
34
+ with open(config_path, 'w') as f:
35
+ json.dump(config, f, indent=2)
36
+
37
+
38
+ def merge_configs(base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]:
39
+ """Merge two configuration dictionaries."""
40
+ merged = base_config.copy()
41
+ merged.update(override_config)
42
+ return merged
43
+
44
+
45
+ def get_env_with_prefix(base_name: str, prefix: str = "DR_", default: str = None) -> str:
46
+ """
47
+ Retrieves an environment variable, checking for a prefixed version first.
48
+
49
+ Args:
50
+ base_name: The base name of the environment variable (e.g., "OPENAI_API_KEY").
51
+ prefix: The prefix to check for (e.g., "DR_"). Defaults to "DR_".
52
+ default: The default value to return if neither the prefixed nor the
53
+ base variable is found.
54
+
55
+ Returns:
56
+ The value of the environment variable, or the default value, or None.
57
+ """
58
+ prefixed_name = f"{prefix}{base_name}"
59
+ value = os.getenv(prefixed_name)
60
+ if value is not None:
61
+ return value
62
+ return os.getenv(base_name, default)
63
+
64
+
65
+ def _substitute_env_vars(obj: Any) -> Any:
66
+ """
67
+ Recursively substitute ${VAR_NAME} with environment variable values.
68
+
69
+ Args:
70
+ obj: Object to process (dict, list, str, etc.)
71
+
72
+ Returns:
73
+ Object with environment variables substituted
74
+ """
75
+ if isinstance(obj, dict):
76
+ return {k: _substitute_env_vars(v) for k, v in obj.items()}
77
+ elif isinstance(obj, list):
78
+ return [_substitute_env_vars(item) for item in obj]
79
+ elif isinstance(obj, str):
80
+ # Match ${VAR_NAME} pattern
81
+ pattern = r'\$\{([^}]+)\}'
82
+ matches = re.findall(pattern, obj)
83
+
84
+ result = obj
85
+ for var_name in matches:
86
+ env_value = os.getenv(var_name, '')
87
+ result = result.replace(f'${{{var_name}}}', env_value)
88
+
89
+ return result
90
+ else:
91
+ return obj
92
+
93
+
94
+ def load_config(config_file: Union[str, Path]) -> Dict[str, Any]:
95
+ """
96
+ Load configuration from YAML or JSON file with env variable substitution.
97
+
98
+ Args:
99
+ config_file: Path to config file (YAML or JSON)
100
+
101
+ Returns:
102
+ Dictionary containing the configuration
103
+
104
+ Example:
105
+ config = load_config("configs/data_science_gemini.yaml")
106
+ """
107
+ config_path = Path(config_file)
108
+
109
+ if not config_path.exists():
110
+ raise FileNotFoundError(f"Config file not found: {config_file}")
111
+
112
+ # Load file based on extension
113
+ with open(config_path, 'r') as f:
114
+ if config_path.suffix in ['.yaml', '.yml']:
115
+ config = yaml.safe_load(f)
116
+ elif config_path.suffix == '.json':
117
+ config = json.load(f)
118
+ else:
119
+ raise ValueError(f"Unsupported config file format: {config_path.suffix}")
120
+
121
+ # Substitute environment variables
122
+ config = _substitute_env_vars(config)
123
+
124
+ return config
125
+
126
+
127
+ def get_agent_instructions(config: Dict[str, Any], agent_name: str) -> str:
128
+ """
129
+ Extract agent instructions from config.
130
+
131
+ Args:
132
+ config: Configuration dictionary
133
+ agent_name: Name of the agent (e.g., 'evaluate_agent')
134
+
135
+ Returns:
136
+ Instructions string for the agent
137
+ """
138
+ return config.get('agents', {}).get(agent_name, {}).get('instructions', '')
139
+
140
+
141
+ def get_pipeline_settings(config: Dict[str, Any]) -> Dict[str, Any]:
142
+ """
143
+ Extract pipeline settings from config.
144
+
145
+ Args:
146
+ config: Configuration dictionary
147
+
148
+ Returns:
149
+ Pipeline settings dictionary
150
+ """
151
+ return config.get('pipeline', {})
152
+
153
+
154
+ # ============================================================================
155
+ # Core Configuration Classes
156
+ # ============================================================================
157
+
158
+ class BaseConfig(BaseModel):
159
+ """Base class for strongly-typed pipeline configuration objects."""
160
+
161
+ # Provider/LLM configuration
162
+ provider: str
163
+ model: Optional[str] = None
164
+ api_key: Optional[str] = None
165
+ base_url: Optional[str] = None
166
+ model_settings: Optional[Dict[str, Any]] = None
167
+ azure_config: Optional[Dict[str, Any]] = None
168
+ aws_config: Optional[Dict[str, Any]] = None
169
+
170
+ # Data configuration
171
+ data: Dict[str, Any] = Field(default_factory=dict)
172
+ user_prompt: Optional[str] = None
173
+
174
+ # Pipeline settings
175
+ pipeline: Dict[str, Any] = Field(default_factory=dict)
176
+
177
+ # Agents: may be flat or single-level groups; leaves will be runtime Agent instances
178
+ agents: Dict[str, Any] = Field(default_factory=dict)
179
+
180
+ # Runtime/computed fields (excluded from serialization)
181
+ llm: Any = Field(default=None, exclude=True)
182
+ config_file: Optional[str] = Field(default=None, exclude=True)
183
+ # runtime conveniences for agents
184
+ agents_flat: Dict[str, Any] = Field(default_factory=dict, exclude=True) # name -> Agent
185
+ agent_groups: Dict[str, List[str]] = Field(default_factory=dict, exclude=True)
186
+ agents_index: Dict[str, Dict[str, Any]] = Field(default_factory=dict, exclude=True) # name -> {instructions, params}
187
+
188
+ model_config = ConfigDict(
189
+ arbitrary_types_allowed=True,
190
+ populate_by_name=True,
191
+ extra="allow",
192
+ )
193
+
194
+ @property
195
+ def data_path(self) -> Optional[str]:
196
+ """Get data path from data section."""
197
+ return self.data.get("path")
198
+
199
+ @property
200
+ def prompt(self) -> Optional[str]:
201
+ """Get prompt from user_prompt or data section."""
202
+ return self.user_prompt or self.data.get("prompt")
203
+
204
+ def to_dict(self) -> Dict[str, Any]:
205
+ """Return a plain serialisable dictionary of the configuration."""
206
+ return self.model_dump(exclude_none=True)
207
+
208
+ @classmethod
209
+ def from_dict(cls, data: Mapping[str, Any]) -> "BaseConfig":
210
+ """Instantiate the config object from a mapping."""
211
+ return cls.model_validate(data)
212
+
213
+ @classmethod
214
+ def from_file(cls, path: Union[str, Path]) -> "BaseConfig":
215
+ """Instantiate the config object from a YAML or JSON file."""
216
+ data = load_mapping_from_path(path)
217
+ config = cls.from_dict(data)
218
+ config.config_file = str(path)
219
+ return config
220
+
221
+
222
+ def load_mapping_from_path(path: Union[str, Path]) -> Dict[str, Any]:
223
+ """Load a mapping from YAML or JSON file, supporting env substitution."""
224
+ data = load_config(Path(path))
225
+ if not isinstance(data, MutableMapping):
226
+ raise ValueError(f"Configuration file must define a mapping, got {type(data)!r}")
227
+ return dict(data)
228
+
229
+
230
+ def get_api_key_from_env(provider: str) -> str:
231
+ """Auto-load API key from environment based on provider."""
232
+ env_map = {
233
+ "openai": "OPENAI_API_KEY",
234
+ "gemini": "GEMINI_API_KEY",
235
+ "deepseek": "DEEPSEEK_API_KEY",
236
+ "anthropic": "ANTHROPIC_API_KEY",
237
+ "openrouter": "OPENROUTER_API_KEY",
238
+ "perplexity": "PERPLEXITY_API_KEY",
239
+ }
240
+
241
+ env_var = env_map.get(provider)
242
+ if not env_var:
243
+ raise ValueError(f"Unknown provider: {provider}. Cannot auto-load API key.")
244
+
245
+ api_key = os.getenv(env_var)
246
+ if not api_key:
247
+ raise ValueError(
248
+ f"API key not found. Set {env_var} in environment or .env file."
249
+ )
250
+
251
+ return api_key
252
+
253
+
254
+ ############################
255
+ # New helpers (inline) #
256
+ ############################
257
+
258
+ # We will not create new files; helpers live here.
259
+ try:
260
+ from agents import Agent # runtime Agent class must exist
261
+ except Exception as e:
262
+ raise ImportError("Expected `from agents import Agent` to be importable") from e
263
+
264
+ def _is_agent_instance(obj: Any) -> bool:
265
+ return hasattr(obj, "name") and hasattr(obj, "instructions")
266
+
267
+ def _make_agent_from_mapping(m: Mapping[str, Any]) -> Agent:
268
+ if not isinstance(m, Mapping):
269
+ raise TypeError(f"Agent spec must be a mapping, got {type(m)}")
270
+ name = m.get("name")
271
+ instr = m.get("instructions")
272
+ if not name or not instr:
273
+ raise ValueError("Agent spec must include 'name' and 'instructions'")
274
+ kwargs: Dict[str, Any] = {k: v for k, v in m.items() if k not in {"name", "instructions"}}
275
+ return Agent(name=name, instructions=instr, **kwargs)
276
+
277
+ def _coerce_agent_like(value: Any) -> Agent:
278
+ if _is_agent_instance(value):
279
+ return value
280
+ if isinstance(value, Mapping):
281
+ return _make_agent_from_mapping(value)
282
+ raise TypeError("Agent must be an Agent instance or mapping with 'name' and 'instructions'")
283
+
284
+ def _normalize_top_level_keys(d: Mapping[str, Any]) -> Dict[str, Any]:
285
+ """
286
+ - Accept synonyms: data_path -> data.path; prompt -> user_prompt
287
+ - Migrate legacy top-level 'manager_agents' into agents['manager'] unless collision.
288
+ """
289
+ out: Dict[str, Any] = dict(d)
290
+ if "data_path" in out:
291
+ out.setdefault("data", {})
292
+ out["data"] = dict(out["data"], path=out.pop("data_path"))
293
+ if "prompt" in out and "user_prompt" not in out:
294
+ out["user_prompt"] = out.pop("prompt")
295
+ if "manager_agents" in out:
296
+ mgr = out.pop("manager_agents")
297
+ out.setdefault("agents", {})
298
+ if "manager" in out["agents"] and isinstance(out["agents"]["manager"], Mapping):
299
+ out["agents"]["manager"] = {**mgr, **out["agents"]["manager"]}
300
+ elif "manager" not in out["agents"]:
301
+ out["agents"]["manager"] = mgr
302
+ else:
303
+ out["agents"]["manager_migrated"] = mgr
304
+ return out
305
+
306
+ def _normalize_agents_tree(agents_node: Mapping[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any], Dict[str, List[str]]]:
307
+ """
308
+ First-level values can be:
309
+ - Agent-like leaf (Agent or mapping with name/instructions)
310
+ - Group dict: { agent_name: Agent-like }
311
+ - Legacy config dict (pass through as-is for registry system)
312
+ Returns:
313
+ agents_tree: same shape, but leaves are runtime Agent instances (or legacy dicts)
314
+ agents_flat: { agent_name: Agent }
315
+ agent_groups: { group_name: [agent_name, ...] }, root-level leaves under "_root"
316
+ Only one level of grouping is allowed; deeper nesting raises ValueError.
317
+ """
318
+ agents_tree: Dict[str, Any] = {}
319
+ agents_flat: Dict[str, Any] = {}
320
+ agent_groups: Dict[str, List[str]] = {}
321
+
322
+ for k, v in (agents_node or {}).items():
323
+ # root-level leaf (runtime Agent)
324
+ if _is_agent_instance(v) or (isinstance(v, Mapping) and "name" in v and "instructions" in v):
325
+ agent = _coerce_agent_like(v)
326
+ agents_tree[k] = agent
327
+ if k in agents_flat and agents_flat[k] is not agent:
328
+ raise ValueError(f"Duplicate agent name detected: {k}")
329
+ agents_flat[k] = agent
330
+ agent_groups.setdefault("_root", []).append(k)
331
+ continue
332
+
333
+ # group (check if it's a runtime agent group or legacy config)
334
+ if isinstance(v, Mapping):
335
+ group_name = k
336
+ # Check if this is a runtime agent group (all values are Agent-like)
337
+ is_runtime_group = any(_is_agent_instance(av) or (isinstance(av, Mapping) and "name" in av and "instructions" in av) for av in v.values())
338
+
339
+ if is_runtime_group:
340
+ group_dict: Dict[str, Any] = {}
341
+ names: List[str] = []
342
+ for ak, av in v.items():
343
+ # disallow deeper nesting (i.e., group inside group)
344
+ if isinstance(av, Mapping) and not ("name" in av and "instructions" in av) and any(isinstance(x, Mapping) for x in av.values()):
345
+ raise ValueError(f"Agents group '{group_name}' contains nested groups; only one level is allowed.")
346
+ agent = _coerce_agent_like(av)
347
+ if ak in agents_flat and agents_flat[ak] is not agent:
348
+ raise ValueError(f"Duplicate agent name across groups: {ak}")
349
+ group_dict[ak] = agent
350
+ agents_flat[ak] = agent
351
+ names.append(ak)
352
+ agents_tree[group_name] = group_dict
353
+ agent_groups[group_name] = names
354
+ else:
355
+ # Legacy config dict - pass through as-is
356
+ agents_tree[group_name] = dict(v)
357
+ continue
358
+
359
+ # Pass through other types as-is (for backward compatibility)
360
+ agents_tree[k] = v
361
+
362
+ return agents_tree, agents_flat, agent_groups
363
+
364
+ def _deep_merge(base: Dict[str, Any], override: Mapping[str, Any]) -> Dict[str, Any]:
365
+ out = dict(base)
366
+ for k, v in (override or {}).items():
367
+ if isinstance(v, Mapping) and isinstance(out.get(k), Mapping):
368
+ out[k] = _deep_merge(dict(out[k]), v)
369
+ else:
370
+ out[k] = v
371
+ return out
372
+
373
+
374
+ def normalize_agents(agents_config: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
375
+ """Normalize agent config into flat index: {name: {instructions, params}}.
376
+
377
+ Args:
378
+ agents_config: Raw agents section from config
379
+
380
+ Returns:
381
+ Dict mapping agent name to {instructions: str, params: dict}
382
+ """
383
+ index: Dict[str, Dict[str, Any]] = {}
384
+
385
+ def _walk(node: Dict[str, Any], parent_path: str = "") -> None:
386
+ for key, value in node.items():
387
+ # Check if this is an agent spec
388
+ if isinstance(value, str):
389
+ # String -> instructions
390
+ index[key] = {"instructions": value, "params": {}}
391
+ elif isinstance(value, dict):
392
+ # Check if it has instructions (leaf spec)
393
+ if "instructions" in value or "profile" in value or "params" in value:
394
+ entry: Dict[str, Any] = {
395
+ "params": value.get("params", {})
396
+ }
397
+ if "instructions" in value:
398
+ entry["instructions"] = value["instructions"]
399
+ if "profile" in value:
400
+ entry["profile"] = value["profile"]
401
+ index[key] = entry
402
+ elif _is_agent_instance(value):
403
+ # Runtime Agent instance - skip (handled elsewhere)
404
+ pass
405
+ else:
406
+ # It's a group - recurse
407
+ _walk(value, key)
408
+
409
+ _walk(agents_config)
410
+ return index
411
+
412
+
413
+ def get_agent_spec(cfg: BaseConfig, name: str, required: bool = True) -> Optional[Dict[str, Any]]:
414
+ """Get normalized agent spec from config.
415
+
416
+ Args:
417
+ cfg: BaseConfig instance
418
+ name: Agent name
419
+ required: If True, raise error if not found; if False, return None
420
+
421
+ Returns:
422
+ Dict with {instructions: str, params: dict} or None if not found and not required
423
+
424
+ Raises:
425
+ ValueError: If agent not found or missing instructions and required=True
426
+ """
427
+ idx = getattr(cfg, "agents_index", None) or {}
428
+
429
+ spec = dict(idx.get(name, {}))
430
+ instructions = spec.get("instructions")
431
+ params_override = dict(spec.get("params", {}))
432
+
433
+ profile_name = spec.get("profile")
434
+
435
+ if instructions is None:
436
+ if required:
437
+ available = sorted(idx.keys())
438
+ raise ValueError(
439
+ f"Agent '{name}' has no instructions in config. "
440
+ f"Configured agents: {available}."
441
+ )
442
+ return None
443
+
444
+ result: Dict[str, Any] = {
445
+ "instructions": instructions,
446
+ "params": params_override,
447
+ }
448
+ if profile_name:
449
+ result["profile"] = profile_name
450
+
451
+ return result
452
+
453
+
454
+
455
+
456
+ def _resolve_relative_paths(data: Dict[str, Any], base_dir: Path) -> Dict[str, Any]:
457
+ """Resolve relative paths in data.path relative to base_dir."""
458
+ out = dict(data)
459
+ if "data" in out and isinstance(out["data"], Mapping):
460
+ data_section = dict(out["data"])
461
+ if "path" in data_section and isinstance(data_section["path"], str):
462
+ path_str = data_section["path"]
463
+ # Only resolve if it looks like a relative path
464
+ if not Path(path_str).is_absolute() and not path_str.startswith("http"):
465
+ resolved = (base_dir / path_str).resolve()
466
+ data_section["path"] = str(resolved)
467
+ out["data"] = data_section
468
+ return out
469
+
470
+
471
+ def resolve_config(spec: Union[str, Path, Mapping[str, Any], BaseConfig]) -> BaseConfig:
472
+ """Resolve configuration from various input formats.
473
+
474
+ Args:
475
+ spec: Configuration specification:
476
+ - str/Path: Load YAML/JSON file
477
+ - dict with 'config_path': Load file, then deep-merge dict on top
478
+ - dict without 'config_path': Use as-is
479
+ - BaseConfig: Use as-is
480
+
481
+ Returns:
482
+ BaseConfig instance with all fields resolved and validated.
483
+
484
+ Examples:
485
+ # Load from file
486
+ config = resolve_config("config.yaml")
487
+
488
+ # Dict without config_path
489
+ config = resolve_config({"provider": "openai", "data": {"path": "data.csv"}})
490
+
491
+ # Dict with config_path (patches file)
492
+ config = resolve_config({
493
+ "config_path": "base.yaml",
494
+ "data": {"path": "override.csv"},
495
+ "agents": {"manager_agents": {"custom_agent": Agent(...)}}
496
+ })
497
+
498
+ # BaseConfig instance
499
+ config = resolve_config(BaseConfig(provider="openai", ...))
500
+ """
501
+ # Load environment variables
502
+ load_dotenv()
503
+
504
+ base_dir: Optional[Path] = None
505
+ raw: Dict[str, Any] = {}
506
+
507
+ # 1. Handle different spec types
508
+ if isinstance(spec, BaseConfig):
509
+ # Already a BaseConfig, convert to dict
510
+ raw = spec.to_dict()
511
+ if spec.config_file:
512
+ base_dir = Path(spec.config_file).parent
513
+
514
+ elif isinstance(spec, (str, Path)):
515
+ # Simple file path
516
+ path = Path(spec)
517
+ raw = load_mapping_from_path(path)
518
+ base_dir = path.parent
519
+
520
+ elif isinstance(spec, Mapping):
521
+ # Dictionary input
522
+ spec_dict = dict(spec)
523
+
524
+ # Check for config_path key
525
+ config_path = spec_dict.pop("config_path", None)
526
+
527
+ if config_path:
528
+ # Load base file first
529
+ path = Path(config_path)
530
+ base_data = load_mapping_from_path(path)
531
+ base_dir = path.parent
532
+ # Deep merge spec on top of base
533
+ raw = _deep_merge(base_data, spec_dict)
534
+ else:
535
+ # No config_path, use dict as-is
536
+ raw = spec_dict
537
+
538
+ else:
539
+ raise TypeError(
540
+ f"spec must be str, Path, Mapping, or BaseConfig; got {type(spec).__name__}"
541
+ )
542
+
543
+ # 2. Expand environment variables in all string fields
544
+ raw = _substitute_env_vars(raw)
545
+
546
+ # 3. Resolve relative paths if we have a base directory
547
+ if base_dir:
548
+ raw = _resolve_relative_paths(raw, base_dir)
549
+
550
+ # 4. Normalize synonyms & migrate legacy keys
551
+ raw = _normalize_top_level_keys(raw)
552
+
553
+ # 5. Ensure required sections exist
554
+ raw.setdefault("data", {})
555
+ raw.setdefault("pipeline", {})
556
+ raw.setdefault("agents", {})
557
+
558
+ # 6. Build BaseConfig instance
559
+ try:
560
+ config = BaseConfig.from_dict(raw)
561
+ except Exception as e:
562
+ raise ValueError(f"Failed to validate config: {e}") from e
563
+
564
+ # 7. Normalize agents tree
565
+ try:
566
+ agents_tree, agents_flat, agent_groups = _normalize_agents_tree(config.agents or {})
567
+ config.agents = agents_tree
568
+ config.agents_flat = agents_flat
569
+ config.agent_groups = agent_groups
570
+
571
+ # Build agents index for easy spec lookup
572
+ config.agents_index = normalize_agents(config.agents or {})
573
+ except Exception as e:
574
+ raise ValueError(f"Failed to process agents configuration: {e}") from e
575
+
576
+ # 8. Resolve API key from environment if not provided
577
+ if not config.api_key and hasattr(config, 'provider') and config.provider:
578
+ try:
579
+ config.api_key = get_api_key_from_env(config.provider)
580
+ except ValueError:
581
+ pass # API key will be validated later if needed
582
+
583
+ # 9. Build LLM config
584
+ from agentz.llm.llm_setup import LLMConfig
585
+
586
+ llm_config_dict: Dict[str, Any] = {
587
+ "provider": config.provider,
588
+ "api_key": config.api_key,
589
+ }
590
+ for optional_key in (
591
+ "model",
592
+ "base_url",
593
+ "model_settings",
594
+ "azure_config",
595
+ "aws_config",
596
+ ):
597
+ value = getattr(config, optional_key, None)
598
+ if value is not None:
599
+ llm_config_dict[optional_key] = value
600
+
601
+ config.llm = LLMConfig(llm_config_dict, config.to_dict())
602
+
603
+ # Store config file path if we have it
604
+ if base_dir and not config.config_file:
605
+ config.config_file = str(base_dir / "config")
606
+
607
+ return config
608
+
609
+
610
+ def load_pipeline_config(
611
+ source: Union[BaseConfig, Mapping[str, Any], str, Path],
612
+ *,
613
+ overrides: Optional[Mapping[str, Any]] = None,
614
+ ) -> BaseConfig:
615
+ """Load and process pipeline configuration (legacy API).
616
+
617
+ Args:
618
+ source: Config input (BaseConfig, dict, or file path).
619
+ overrides: Optional dict to deep merge into config.
620
+
621
+ Returns:
622
+ BaseConfig instance with llm field populated.
623
+
624
+ Note:
625
+ This is the legacy API. Prefer resolve_config() for new code.
626
+ For the new API, use config_path in the dict instead of overrides.
627
+ """
628
+ # Load environment variables
629
+ load_dotenv()
630
+
631
+ # Import here to avoid circular dependency
632
+ from agentz.llm.llm_setup import LLMConfig
633
+
634
+ # Ingest anything → mapping
635
+ if isinstance(source, BaseConfig):
636
+ raw: Dict[str, Any] = source.to_dict()
637
+ elif isinstance(source, Mapping):
638
+ raw = dict(source)
639
+ elif isinstance(source, (str, Path)):
640
+ raw = load_mapping_from_path(source)
641
+ else:
642
+ raise TypeError(f"Unsupported config type: {type(source)}.")
643
+
644
+ # Normalize synonyms & migrate legacy keys
645
+ raw = _normalize_top_level_keys(raw)
646
+
647
+ # Ensure sections
648
+ raw.setdefault("data", {})
649
+ raw.setdefault("pipeline", {})
650
+ raw.setdefault("agents", {})
651
+
652
+ # Apply overrides last
653
+ if overrides:
654
+ raw = _deep_merge(raw, overrides)
655
+
656
+ # Build initial BaseConfig
657
+ config = BaseConfig.from_dict(raw)
658
+
659
+ # Normalize agents: coerce to runtime Agent, support one-level groups
660
+ agents_tree, agents_flat, agent_groups = _normalize_agents_tree(config.agents or {})
661
+ config.agents = agents_tree
662
+ config.agents_flat = agents_flat
663
+ config.agent_groups = agent_groups
664
+
665
+ # Resolve API key from environment if not provided
666
+ if not config.api_key:
667
+ config.api_key = get_api_key_from_env(config.provider)
668
+
669
+ # Build LLM config dict
670
+ llm_config_dict: Dict[str, Any] = {
671
+ "provider": config.provider,
672
+ "api_key": config.api_key,
673
+ }
674
+ for optional_key in (
675
+ "model",
676
+ "base_url",
677
+ "model_settings",
678
+ "azure_config",
679
+ "aws_config",
680
+ ):
681
+ value = getattr(config, optional_key, None)
682
+ if value is not None:
683
+ llm_config_dict[optional_key] = value
684
+
685
+ # Create LLM config instance
686
+ config.llm = LLMConfig(llm_config_dict, config.to_dict())
687
+
688
+ return config
689
+
690
+
691
+ __all__ = [
692
+ # Simple utilities
693
+ "load_json_config",
694
+ "save_json_config",
695
+ "merge_configs",
696
+ "get_env_with_prefix",
697
+ "load_config",
698
+ "get_agent_instructions",
699
+ "get_pipeline_settings",
700
+ # Core configuration
701
+ "BaseConfig",
702
+ "load_mapping_from_path",
703
+ "get_api_key_from_env",
704
+ "resolve_config",
705
+ "load_pipeline_config",
706
+ "normalize_agents",
707
+ "get_agent_spec",
708
+ ]