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.
- agentz/agent/base.py +262 -0
- agentz/artifacts/__init__.py +5 -0
- agentz/artifacts/artifact_writer.py +538 -0
- agentz/artifacts/reporter.py +235 -0
- agentz/artifacts/terminal_writer.py +100 -0
- agentz/context/__init__.py +6 -0
- agentz/context/context.py +91 -0
- agentz/context/conversation.py +205 -0
- agentz/context/data_store.py +208 -0
- agentz/llm/llm_setup.py +156 -0
- agentz/mcp/manager.py +142 -0
- agentz/mcp/patches.py +88 -0
- agentz/mcp/servers/chrome_devtools/server.py +14 -0
- agentz/profiles/base.py +108 -0
- agentz/profiles/data/data_analysis.py +38 -0
- agentz/profiles/data/data_loader.py +35 -0
- agentz/profiles/data/evaluation.py +43 -0
- agentz/profiles/data/model_training.py +47 -0
- agentz/profiles/data/preprocessing.py +47 -0
- agentz/profiles/data/visualization.py +47 -0
- agentz/profiles/manager/evaluate.py +51 -0
- agentz/profiles/manager/memory.py +62 -0
- agentz/profiles/manager/observe.py +48 -0
- agentz/profiles/manager/routing.py +66 -0
- agentz/profiles/manager/writer.py +51 -0
- agentz/profiles/mcp/browser.py +21 -0
- agentz/profiles/mcp/chrome.py +21 -0
- agentz/profiles/mcp/notion.py +21 -0
- agentz/runner/__init__.py +74 -0
- agentz/runner/base.py +28 -0
- agentz/runner/executor.py +320 -0
- agentz/runner/hooks.py +110 -0
- agentz/runner/iteration.py +142 -0
- agentz/runner/patterns.py +215 -0
- agentz/runner/tracker.py +188 -0
- agentz/runner/utils.py +45 -0
- agentz/runner/workflow.py +250 -0
- agentz/tools/__init__.py +20 -0
- agentz/tools/data_tools/__init__.py +17 -0
- agentz/tools/data_tools/data_analysis.py +152 -0
- agentz/tools/data_tools/data_loading.py +92 -0
- agentz/tools/data_tools/evaluation.py +175 -0
- agentz/tools/data_tools/helpers.py +120 -0
- agentz/tools/data_tools/model_training.py +192 -0
- agentz/tools/data_tools/preprocessing.py +229 -0
- agentz/tools/data_tools/visualization.py +281 -0
- agentz/utils/__init__.py +69 -0
- agentz/utils/config.py +708 -0
- agentz/utils/helpers.py +10 -0
- agentz/utils/parsers.py +142 -0
- agentz/utils/printer.py +539 -0
- contextagent-0.1.0.dist-info/METADATA +269 -0
- contextagent-0.1.0.dist-info/RECORD +66 -0
- contextagent-0.1.0.dist-info/WHEEL +5 -0
- contextagent-0.1.0.dist-info/licenses/LICENSE +21 -0
- contextagent-0.1.0.dist-info/top_level.txt +2 -0
- pipelines/base.py +972 -0
- pipelines/data_scientist.py +97 -0
- pipelines/data_scientist_memory.py +151 -0
- pipelines/experience_learner.py +0 -0
- pipelines/prompt_generator.py +0 -0
- pipelines/simple.py +78 -0
- pipelines/simple_browser.py +145 -0
- pipelines/simple_chrome.py +75 -0
- pipelines/simple_notion.py +103 -0
- 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
|
+
]
|