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.
- agentloom/__init__.py +3 -0
- agentloom/cli/__init__.py +1 -0
- agentloom/cli/info.py +66 -0
- agentloom/cli/main.py +25 -0
- agentloom/cli/run.py +236 -0
- agentloom/cli/validate.py +48 -0
- agentloom/cli/visualize.py +118 -0
- agentloom/compat.py +57 -0
- agentloom/config.py +55 -0
- agentloom/core/__init__.py +1 -0
- agentloom/core/dag.py +155 -0
- agentloom/core/dsl.py +148 -0
- agentloom/core/engine.py +403 -0
- agentloom/core/models.py +99 -0
- agentloom/core/parser.py +125 -0
- agentloom/core/results.py +63 -0
- agentloom/core/state.py +132 -0
- agentloom/exceptions.py +64 -0
- agentloom/observability/__init__.py +18 -0
- agentloom/observability/cost_tracker.py +95 -0
- agentloom/observability/logging.py +82 -0
- agentloom/observability/metrics.py +293 -0
- agentloom/observability/noop.py +74 -0
- agentloom/observability/observer.py +161 -0
- agentloom/observability/tracing.py +92 -0
- agentloom/providers/__init__.py +5 -0
- agentloom/providers/anthropic.py +109 -0
- agentloom/providers/base.py +80 -0
- agentloom/providers/gateway.py +181 -0
- agentloom/providers/google.py +121 -0
- agentloom/providers/ollama.py +96 -0
- agentloom/providers/openai.py +92 -0
- agentloom/providers/pricing.py +95 -0
- agentloom/py.typed +0 -0
- agentloom/resilience/__init__.py +15 -0
- agentloom/resilience/budget.py +60 -0
- agentloom/resilience/circuit_breaker.py +131 -0
- agentloom/resilience/rate_limiter.py +82 -0
- agentloom/resilience/retry.py +84 -0
- agentloom/steps/__init__.py +6 -0
- agentloom/steps/base.py +40 -0
- agentloom/steps/llm_call.py +126 -0
- agentloom/steps/registry.py +35 -0
- agentloom/steps/router.py +188 -0
- agentloom/steps/subworkflow.py +96 -0
- agentloom/steps/tool_step.py +82 -0
- agentloom/tools/__init__.py +7 -0
- agentloom/tools/base.py +26 -0
- agentloom/tools/builtins.py +140 -0
- agentloom/tools/decorator.py +97 -0
- agentloom/tools/registry.py +84 -0
- agentloom-0.1.0.dist-info/METADATA +235 -0
- agentloom-0.1.0.dist-info/RECORD +56 -0
- agentloom-0.1.0.dist-info/WHEEL +4 -0
- agentloom-0.1.0.dist-info/entry_points.txt +2 -0
- agentloom-0.1.0.dist-info/licenses/LICENSE +21 -0
agentloom/__init__.py
ADDED
|
@@ -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."""
|