agentloom 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 (56) hide show
  1. agentloom/__init__.py +3 -0
  2. agentloom/cli/__init__.py +1 -0
  3. agentloom/cli/info.py +66 -0
  4. agentloom/cli/main.py +25 -0
  5. agentloom/cli/run.py +236 -0
  6. agentloom/cli/validate.py +48 -0
  7. agentloom/cli/visualize.py +118 -0
  8. agentloom/compat.py +57 -0
  9. agentloom/config.py +55 -0
  10. agentloom/core/__init__.py +1 -0
  11. agentloom/core/dag.py +155 -0
  12. agentloom/core/dsl.py +148 -0
  13. agentloom/core/engine.py +403 -0
  14. agentloom/core/models.py +99 -0
  15. agentloom/core/parser.py +125 -0
  16. agentloom/core/results.py +63 -0
  17. agentloom/core/state.py +132 -0
  18. agentloom/exceptions.py +64 -0
  19. agentloom/observability/__init__.py +18 -0
  20. agentloom/observability/cost_tracker.py +95 -0
  21. agentloom/observability/logging.py +82 -0
  22. agentloom/observability/metrics.py +293 -0
  23. agentloom/observability/noop.py +74 -0
  24. agentloom/observability/observer.py +161 -0
  25. agentloom/observability/tracing.py +92 -0
  26. agentloom/providers/__init__.py +5 -0
  27. agentloom/providers/anthropic.py +109 -0
  28. agentloom/providers/base.py +80 -0
  29. agentloom/providers/gateway.py +181 -0
  30. agentloom/providers/google.py +121 -0
  31. agentloom/providers/ollama.py +96 -0
  32. agentloom/providers/openai.py +92 -0
  33. agentloom/providers/pricing.py +95 -0
  34. agentloom/py.typed +0 -0
  35. agentloom/resilience/__init__.py +15 -0
  36. agentloom/resilience/budget.py +60 -0
  37. agentloom/resilience/circuit_breaker.py +131 -0
  38. agentloom/resilience/rate_limiter.py +82 -0
  39. agentloom/resilience/retry.py +84 -0
  40. agentloom/steps/__init__.py +6 -0
  41. agentloom/steps/base.py +40 -0
  42. agentloom/steps/llm_call.py +126 -0
  43. agentloom/steps/registry.py +35 -0
  44. agentloom/steps/router.py +188 -0
  45. agentloom/steps/subworkflow.py +96 -0
  46. agentloom/steps/tool_step.py +82 -0
  47. agentloom/tools/__init__.py +7 -0
  48. agentloom/tools/base.py +26 -0
  49. agentloom/tools/builtins.py +140 -0
  50. agentloom/tools/decorator.py +97 -0
  51. agentloom/tools/registry.py +84 -0
  52. agentloom-0.1.0.dist-info/METADATA +235 -0
  53. agentloom-0.1.0.dist-info/RECORD +56 -0
  54. agentloom-0.1.0.dist-info/WHEEL +4 -0
  55. agentloom-0.1.0.dist-info/entry_points.txt +2 -0
  56. agentloom-0.1.0.dist-info/licenses/LICENSE +21 -0
agentloom/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AgentLoom: Production-ready agentic workflow orchestrator."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """CLI entry point for AgentLoom."""
agentloom/cli/info.py ADDED
@@ -0,0 +1,66 @@
1
+ """CLI command: show system information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import sys
8
+
9
+ import typer
10
+
11
+ from agentloom import __version__
12
+ from agentloom.compat import is_available, try_import
13
+
14
+
15
+ def info() -> None:
16
+ """Show AgentLoom version, dependencies, and system information."""
17
+ typer.echo(f"AgentLoom v{__version__}")
18
+ typer.echo(f"{'=' * 40}")
19
+
20
+ # Python info
21
+ typer.echo(f"\nPython: {sys.version.split()[0]}")
22
+ typer.echo(f"Platform: {platform.platform()}")
23
+ typer.echo(f"Arch: {platform.machine()}")
24
+
25
+ # Core dependencies
26
+ typer.echo("\nCore dependencies:")
27
+ for pkg in ["pydantic", "httpx", "pyyaml", "typer", "anyio"]:
28
+ _show_dep(pkg)
29
+
30
+ # Optional dependencies
31
+ typer.echo("\nOptional (observability):")
32
+ otel = try_import("opentelemetry.sdk", extra="observability")
33
+ prom = try_import("prometheus_client", extra="observability")
34
+ typer.echo(f" opentelemetry: {'installed' if is_available(otel) else 'not installed'}")
35
+ typer.echo(f" prometheus: {'installed' if is_available(prom) else 'not installed'}")
36
+
37
+ # Provider API keys
38
+ typer.echo("\nProviders:")
39
+ providers = {
40
+ "OpenAI": "OPENAI_API_KEY",
41
+ "Anthropic": "ANTHROPIC_API_KEY",
42
+ "Google": "GOOGLE_API_KEY",
43
+ "Ollama": "OLLAMA_BASE_URL",
44
+ }
45
+ for name, env_var in providers.items():
46
+ value = os.environ.get(env_var, "")
47
+ if env_var == "OLLAMA_BASE_URL":
48
+ status = value or "http://localhost:11434 (default)"
49
+ elif value:
50
+ status = f"configured ({value[:8]}...)"
51
+ else:
52
+ status = "not configured"
53
+ typer.echo(f" {name:12s} {status}")
54
+
55
+ typer.echo("")
56
+
57
+
58
+ def _show_dep(package: str) -> None:
59
+ """Show version of an installed package."""
60
+ try:
61
+ from importlib.metadata import version
62
+
63
+ ver = version(package.replace("-", "_"))
64
+ typer.echo(f" {package:12s} {ver}")
65
+ except Exception:
66
+ typer.echo(f" {package:12s} not found")
agentloom/cli/main.py ADDED
@@ -0,0 +1,25 @@
1
+ """Main CLI application for AgentLoom."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ app = typer.Typer(
8
+ name="agentloom",
9
+ help="AgentLoom: Production-ready agentic workflow orchestrator.",
10
+ no_args_is_help=True,
11
+ )
12
+
13
+ from agentloom.cli.info import info # noqa: E402
14
+ from agentloom.cli.run import run # noqa: E402
15
+ from agentloom.cli.validate import validate # noqa: E402
16
+ from agentloom.cli.visualize import visualize # noqa: E402
17
+
18
+ app.command("run")(run)
19
+ app.command("validate")(validate)
20
+ app.command("visualize")(visualize)
21
+ app.command("info")(info)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ app()
agentloom/cli/run.py ADDED
@@ -0,0 +1,236 @@
1
+ """CLI command: run a workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import anyio
9
+ import typer
10
+
11
+ from agentloom.core.parser import WorkflowParser
12
+ from agentloom.core.results import WorkflowStatus
13
+ from agentloom.core.state import StateManager
14
+
15
+ if TYPE_CHECKING:
16
+ from agentloom.observability.observer import WorkflowObserver
17
+ from agentloom.providers.gateway import ProviderGateway
18
+
19
+
20
+ def run(
21
+ workflow_path: Path = typer.Argument(..., help="Path to the workflow YAML file.", exists=True),
22
+ state: list[str] = typer.Option(
23
+ [], "--state", "-s", help="State variables as key=value pairs."
24
+ ),
25
+ provider: str | None = typer.Option(
26
+ None, "--provider", "-p", help="Override default provider."
27
+ ),
28
+ model: str | None = typer.Option(None, "--model", "-m", help="Override default model."),
29
+ budget: float | None = typer.Option(None, "--budget", "-b", help="Maximum budget in USD."),
30
+ lite: bool = typer.Option(False, "--lite", help="Run in lite mode (no observability)."),
31
+ output_json: bool = typer.Option(False, "--json", help="Output results as JSON."),
32
+ ) -> None:
33
+ """Execute a workflow from a YAML definition file."""
34
+ anyio.run(_run_async, workflow_path, state, provider, model, budget, lite, output_json)
35
+
36
+
37
+ async def _run_async(
38
+ workflow_path: Path,
39
+ state_args: list[str],
40
+ provider_override: str | None,
41
+ model_override: str | None,
42
+ budget: float | None,
43
+ lite: bool,
44
+ output_json: bool,
45
+ ) -> None:
46
+ """Async implementation of the run command."""
47
+ from agentloom.core.engine import WorkflowEngine
48
+ from agentloom.providers.gateway import ProviderGateway
49
+ from agentloom.tools.builtins import register_builtins
50
+ from agentloom.tools.registry import ToolRegistry
51
+
52
+ # Parse workflow
53
+ try:
54
+ workflow = WorkflowParser.from_yaml(workflow_path)
55
+ except Exception as e:
56
+ typer.echo(f"Error loading workflow: {e}", err=True)
57
+ raise typer.Exit(1)
58
+
59
+ # Apply overrides
60
+ if provider_override:
61
+ workflow.config.provider = provider_override
62
+ if model_override:
63
+ workflow.config.model = model_override
64
+ if budget is not None:
65
+ workflow.config.budget_usd = budget
66
+
67
+ # Parse state overrides
68
+ initial_state = dict(workflow.state)
69
+ for item in state_args:
70
+ if "=" not in item:
71
+ typer.echo(f"Invalid state format '{item}'. Use key=value.", err=True)
72
+ raise typer.Exit(1)
73
+ key, value = item.split("=", 1)
74
+ initial_state[key] = value
75
+
76
+ state_manager = StateManager(initial_state=initial_state)
77
+
78
+ # Setup provider gateway
79
+ gateway = ProviderGateway()
80
+ _setup_providers(gateway, workflow.config.provider)
81
+
82
+ # Setup tools
83
+ tool_registry = ToolRegistry()
84
+ register_builtins(tool_registry)
85
+
86
+ # Setup observability (unless --lite)
87
+ observer = _setup_observer(lite)
88
+
89
+ # Run engine
90
+ engine = WorkflowEngine(
91
+ workflow=workflow,
92
+ state_manager=state_manager,
93
+ provider_gateway=gateway,
94
+ tool_registry=tool_registry,
95
+ observer=observer,
96
+ )
97
+
98
+ typer.echo(f"Running workflow: {workflow.name}")
99
+ result = await engine.run()
100
+
101
+ # Output results
102
+ if output_json:
103
+ typer.echo(result.model_dump_json(indent=2))
104
+ else:
105
+ _print_result(result)
106
+
107
+ if observer:
108
+ observer.shutdown()
109
+ await gateway.close()
110
+
111
+ if result.status != WorkflowStatus.SUCCESS:
112
+ raise typer.Exit(1)
113
+
114
+
115
+ def _setup_observer(lite: bool) -> WorkflowObserver | None:
116
+ """Create the observability observer unless running in lite mode."""
117
+ if lite:
118
+ return None
119
+
120
+ from agentloom.compat import is_available, try_import
121
+ from agentloom.observability.observer import WorkflowObserver
122
+
123
+ tracing_mod = try_import("opentelemetry.trace", extra="observability")
124
+ metrics_mod = try_import("opentelemetry.sdk.metrics", extra="observability")
125
+
126
+ tracing = None
127
+ metrics = None
128
+
129
+ if is_available(tracing_mod):
130
+ from agentloom.observability.tracing import TracingManager
131
+
132
+ tracing = TracingManager()
133
+
134
+ if is_available(metrics_mod):
135
+ from agentloom.observability.metrics import MetricsManager
136
+
137
+ metrics = MetricsManager()
138
+
139
+ if tracing or metrics:
140
+ return WorkflowObserver(tracing=tracing, metrics=metrics)
141
+
142
+ return None
143
+
144
+
145
+ def _setup_providers(gateway: ProviderGateway, default_provider: str) -> None:
146
+ """Setup providers based on available API keys."""
147
+ # HACK: provider discovery from env vars — should really be in a config file
148
+ import os
149
+
150
+ if os.environ.get("OPENAI_API_KEY"):
151
+ from agentloom.providers.openai import OpenAIProvider
152
+
153
+ gateway.register(
154
+ OpenAIProvider(),
155
+ priority=0 if default_provider == "openai" else 10,
156
+ models=[
157
+ "gpt-4o-mini",
158
+ "gpt-4o",
159
+ "gpt-4.1",
160
+ "o4-mini",
161
+ ],
162
+ )
163
+
164
+ if os.environ.get("ANTHROPIC_API_KEY"):
165
+ from agentloom.providers.anthropic import AnthropicProvider
166
+
167
+ gateway.register(
168
+ AnthropicProvider(),
169
+ priority=0 if default_provider == "anthropic" else 10,
170
+ models=[
171
+ "claude-haiku-4-5-20251001",
172
+ ],
173
+ )
174
+
175
+ if os.environ.get("GOOGLE_API_KEY"):
176
+ from agentloom.providers.google import GoogleProvider
177
+
178
+ gateway.register(
179
+ GoogleProvider(),
180
+ priority=0 if default_provider == "google" else 10,
181
+ models=[
182
+ "gemini-2.0-flash",
183
+ "gemini-2.5-flash",
184
+ ],
185
+ )
186
+
187
+ # Ollama is always available as fallback (local/LAN)
188
+ from agentloom.providers.ollama import OllamaProvider
189
+
190
+ ollama_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
191
+ gateway.register(
192
+ OllamaProvider(base_url=ollama_url),
193
+ priority=0 if default_provider == "ollama" else 100,
194
+ is_fallback=True,
195
+ )
196
+
197
+
198
+ def _print_result(result: object) -> None:
199
+ """Pretty-print a workflow result."""
200
+ from agentloom.core.results import StepStatus, WorkflowResult
201
+
202
+ r: WorkflowResult = result # type: ignore[assignment]
203
+
204
+ status_icon = {
205
+ "success": "[OK]",
206
+ "failed": "[FAIL]",
207
+ "timeout": "[TIMEOUT]",
208
+ "budget_exceeded": "[BUDGET]",
209
+ }
210
+
211
+ typer.echo(f"\n{'=' * 60}")
212
+ typer.echo(f"Workflow: {r.workflow_name}")
213
+ typer.echo(f"Status: {status_icon.get(r.status.value, '?')} {r.status.value}")
214
+ typer.echo(f"Duration: {r.total_duration_ms:.1f}ms")
215
+ typer.echo(f"Tokens: {r.total_tokens}")
216
+ typer.echo(f"Cost: ${r.total_cost_usd:.4f}")
217
+
218
+ if r.error:
219
+ typer.echo(f"Error: {r.error}")
220
+
221
+ typer.echo("\nSteps:")
222
+ for step_id, sr in r.step_results.items():
223
+ icon = (
224
+ "[OK]"
225
+ if sr.status == StepStatus.SUCCESS
226
+ else ("[SKIP]" if sr.status == StepStatus.SKIPPED else "[FAIL]")
227
+ )
228
+ line = f" {icon} {step_id} ({sr.duration_ms:.0f}ms)"
229
+ if sr.cost_usd > 0:
230
+ line += f" ${sr.cost_usd:.4f}"
231
+ typer.echo(line)
232
+
233
+ # Show final output
234
+ final_state = r.final_state
235
+ typer.echo(f"\nFinal State Keys: {list(final_state.keys())}")
236
+ typer.echo(f"{'=' * 60}")
@@ -0,0 +1,48 @@
1
+ """CLI command: validate a workflow definition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from agentloom.core.parser import WorkflowParser
10
+ from agentloom.exceptions import ValidationError
11
+
12
+
13
+ def validate(
14
+ workflow_path: Path = typer.Argument(..., help="Path to the workflow YAML file.", exists=True),
15
+ ) -> None:
16
+ """Validate a workflow YAML file for correctness."""
17
+ try:
18
+ workflow = WorkflowParser.from_yaml(workflow_path)
19
+ except ValidationError as e:
20
+ typer.echo(f"Validation FAILED:\n{e}", err=True)
21
+ raise typer.Exit(1)
22
+ except Exception as e:
23
+ typer.echo(f"Error: {e}", err=True)
24
+ raise typer.Exit(1)
25
+
26
+ # Also validate the DAG
27
+ try:
28
+ dag = WorkflowParser.build_dag(workflow)
29
+ except ValidationError as e:
30
+ typer.echo(f"DAG validation FAILED:\n{e}", err=True)
31
+ raise typer.Exit(1)
32
+
33
+ layers = dag.execution_layers()
34
+
35
+ typer.echo(f"Workflow '{workflow.name}' is valid.")
36
+ typer.echo(f" Version: {workflow.version}")
37
+ typer.echo(f" Steps: {len(workflow.steps)}")
38
+ typer.echo(f" Layers: {len(layers)}")
39
+ typer.echo(f" Provider: {workflow.config.provider}")
40
+ typer.echo(f" Model: {workflow.config.model}")
41
+
42
+ if workflow.config.budget_usd:
43
+ typer.echo(f" Budget: ${workflow.config.budget_usd:.2f}")
44
+
45
+ typer.echo("\n Execution plan:")
46
+ for i, layer in enumerate(layers):
47
+ parallel = " (parallel)" if len(layer) > 1 else ""
48
+ typer.echo(f" Layer {i}: {', '.join(layer)}{parallel}")
@@ -0,0 +1,118 @@
1
+ """CLI command: visualize a workflow DAG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from agentloom.core.models import StepType
10
+ from agentloom.core.parser import WorkflowParser
11
+ from agentloom.exceptions import ValidationError
12
+
13
+
14
+ def visualize(
15
+ workflow_path: Path = typer.Argument(..., help="Path to the workflow YAML file.", exists=True),
16
+ format: str = typer.Option("ascii", "--format", "-f", help="Output format: ascii or mermaid."),
17
+ ) -> None:
18
+ """Visualize a workflow as an ASCII diagram or Mermaid graph."""
19
+ try:
20
+ workflow = WorkflowParser.from_yaml(workflow_path)
21
+ dag = WorkflowParser.build_dag(workflow)
22
+ except (ValidationError, Exception) as e:
23
+ typer.echo(f"Error: {e}", err=True)
24
+ raise typer.Exit(1)
25
+
26
+ if format == "mermaid":
27
+ _print_mermaid(workflow, dag)
28
+ else:
29
+ _print_ascii(workflow, dag)
30
+
31
+
32
+ def _print_ascii(workflow: object, dag: object) -> None:
33
+ """Print an ASCII representation of the workflow DAG."""
34
+ from agentloom.core.dag import DAG
35
+ from agentloom.core.models import WorkflowDefinition
36
+
37
+ w: WorkflowDefinition = workflow # type: ignore[assignment]
38
+ d: DAG = dag # type: ignore[assignment]
39
+ layers = d.execution_layers()
40
+
41
+ step_types = {s.id: s.type for s in w.steps}
42
+ type_icons = {
43
+ StepType.LLM_CALL: "LLM",
44
+ StepType.TOOL: "TOOL",
45
+ StepType.ROUTER: "IF/ELSE",
46
+ StepType.SUBWORKFLOW: "SUB",
47
+ }
48
+
49
+ typer.echo(f"\n Workflow: {w.name}")
50
+ typer.echo(f" {'=' * 50}")
51
+
52
+ for i, layer in enumerate(layers):
53
+ if i > 0:
54
+ # Draw arrows from previous layer
55
+ typer.echo(f" {' |':^50}")
56
+ typer.echo(f" {' v':^50}")
57
+
58
+ # Draw steps in this layer
59
+ boxes = []
60
+ for step_id in layer:
61
+ stype = step_types.get(step_id, StepType.LLM_CALL)
62
+ icon = type_icons.get(stype, "?")
63
+ box = f"[{icon}: {step_id}]"
64
+ boxes.append(box)
65
+
66
+ if len(boxes) == 1:
67
+ typer.echo(f" {boxes[0]:^50}")
68
+ else:
69
+ # Parallel steps side by side
70
+ line = " " + " ".join(boxes)
71
+ typer.echo(line)
72
+
73
+ typer.echo(f" {'=' * 50}\n")
74
+
75
+
76
+ def _print_mermaid(workflow: object, dag: object) -> None:
77
+ """Print a Mermaid graph definition."""
78
+ from agentloom.core.models import WorkflowDefinition
79
+
80
+ w: WorkflowDefinition = workflow # type: ignore[assignment]
81
+
82
+ step_types = {s.id: s.type for s in w.steps}
83
+
84
+ typer.echo("```mermaid")
85
+ typer.echo("graph TD")
86
+
87
+ # Define nodes
88
+ for step in w.steps:
89
+ stype = step_types.get(step.id, StepType.LLM_CALL)
90
+ if stype == StepType.ROUTER:
91
+ typer.echo(f" {step.id}{{{step.id}}}")
92
+ elif stype == StepType.TOOL:
93
+ typer.echo(f" {step.id}[/{step.id}/]")
94
+ elif stype == StepType.SUBWORKFLOW:
95
+ typer.echo(f" {step.id}[[{step.id}]]")
96
+ else:
97
+ typer.echo(f" {step.id}[{step.id}]")
98
+
99
+ # Define edges
100
+ for step in w.steps:
101
+ for dep in step.depends_on:
102
+ dep_step = w.get_step(dep)
103
+ if dep_step and dep_step.type == StepType.ROUTER:
104
+ # Show router conditions as edge labels
105
+ for cond in dep_step.conditions:
106
+ if cond.target == step.id:
107
+ label = cond.expression[:20]
108
+ typer.echo(f" {dep} -->|{label}| {step.id}")
109
+ break
110
+ else:
111
+ if dep_step.default == step.id:
112
+ typer.echo(f" {dep} -->|default| {step.id}")
113
+ else:
114
+ typer.echo(f" {dep} --> {step.id}")
115
+ else:
116
+ typer.echo(f" {dep} --> {step.id}")
117
+
118
+ typer.echo("```")
agentloom/compat.py ADDED
@@ -0,0 +1,57 @@
1
+ """Conditional import mechanism for optional dependencies.
2
+
3
+ Provides `try_import()` which returns the real module if available,
4
+ or a MissingDependencyProxy that raises ImportError with install
5
+ instructions on any attribute access.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ from types import ModuleType
12
+
13
+
14
+ class MissingDependencyProxy:
15
+ """Proxy object that raises ImportError with install instructions on attribute access."""
16
+
17
+ def __init__(self, module_name: str, extra: str = "all") -> None:
18
+ object.__setattr__(self, "_module_name", module_name)
19
+ object.__setattr__(self, "_extra", extra)
20
+
21
+ def _raise(self) -> None:
22
+ module_name = object.__getattribute__(self, "_module_name")
23
+ extra = object.__getattribute__(self, "_extra")
24
+ raise ImportError(
25
+ f"'{module_name}' is required for this feature. "
26
+ f"Install with: pip install agentloom[{extra}]"
27
+ )
28
+
29
+ def __getattr__(self, name: str) -> None:
30
+ self._raise()
31
+
32
+ def __call__(self, *args: object, **kwargs: object) -> None:
33
+ self._raise()
34
+
35
+ def __bool__(self) -> bool:
36
+ return False
37
+
38
+
39
+ def try_import(module_name: str, extra: str = "all") -> ModuleType | MissingDependencyProxy:
40
+ """Try to import a module, returning a proxy if it's not installed.
41
+
42
+ Args:
43
+ module_name: Fully qualified module name (e.g., "opentelemetry.sdk").
44
+ extra: The pip extra that provides this module (e.g., "observability").
45
+
46
+ Returns:
47
+ The real module if available, or a MissingDependencyProxy.
48
+ """
49
+ try:
50
+ return importlib.import_module(module_name)
51
+ except ImportError:
52
+ return MissingDependencyProxy(module_name, extra)
53
+
54
+
55
+ def is_available(module_or_proxy: ModuleType | MissingDependencyProxy) -> bool:
56
+ """Check if a try_import result is a real module (True) or a proxy (False)."""
57
+ return not isinstance(module_or_proxy, MissingDependencyProxy)
agentloom/config.py ADDED
@@ -0,0 +1,55 @@
1
+ """Global configuration for AgentLoom."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ProviderConfig(BaseModel):
9
+ """Configuration for a single LLM provider."""
10
+
11
+ name: str
12
+ api_key: str = ""
13
+ base_url: str = ""
14
+ models: list[str] = Field(default_factory=list)
15
+ priority: int = 0
16
+ is_fallback: bool = False
17
+ max_retries: int = 3
18
+ timeout: float = 30.0
19
+
20
+
21
+ class AgentLoomConfig(BaseModel):
22
+ """Global configuration, loaded from env vars or config file."""
23
+
24
+ log_level: str = "INFO"
25
+ log_format: str = "json" # "json" or "text"
26
+ observability_enabled: bool = True
27
+ default_provider: str = "openai"
28
+ max_concurrent_steps: int = 10
29
+ budget_limit_usd: float | None = None
30
+ checkpoint_enabled: bool = False
31
+ checkpoint_dir: str = ".agentloom/checkpoints"
32
+ providers: list[ProviderConfig] = Field(default_factory=list)
33
+
34
+
35
+ # TODO: support AGENTLOOM_ env var prefix (pydantic-settings would be nice here)
36
+ def load_config(config_path: str | None = None) -> AgentLoomConfig:
37
+ """Load configuration from a YAML file or return defaults.
38
+
39
+ Args:
40
+ config_path: Path to agentloom.yaml config file.
41
+
42
+ Returns:
43
+ Validated AgentLoomConfig instance.
44
+ """
45
+ if config_path is None:
46
+ return AgentLoomConfig()
47
+
48
+ from pathlib import Path
49
+
50
+ import yaml # noqa: TCH002
51
+
52
+ raw = yaml.safe_load(Path(config_path).read_text())
53
+ if raw is None:
54
+ return AgentLoomConfig()
55
+ return AgentLoomConfig.model_validate(raw)
@@ -0,0 +1 @@
1
+ """Core workflow engine components."""