fluxloop-cli 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.
Potentially problematic release.
This version of fluxloop-cli might be problematic. Click here for more details.
- fluxloop_cli/__init__.py +9 -0
- fluxloop_cli/arg_binder.py +219 -0
- fluxloop_cli/commands/__init__.py +5 -0
- fluxloop_cli/commands/config.py +355 -0
- fluxloop_cli/commands/generate.py +304 -0
- fluxloop_cli/commands/init.py +225 -0
- fluxloop_cli/commands/parse.py +293 -0
- fluxloop_cli/commands/run.py +310 -0
- fluxloop_cli/commands/status.py +227 -0
- fluxloop_cli/config_loader.py +159 -0
- fluxloop_cli/constants.py +12 -0
- fluxloop_cli/input_generator.py +158 -0
- fluxloop_cli/llm_generator.py +417 -0
- fluxloop_cli/main.py +97 -0
- fluxloop_cli/project_paths.py +80 -0
- fluxloop_cli/runner.py +634 -0
- fluxloop_cli/target_loader.py +95 -0
- fluxloop_cli/templates.py +277 -0
- fluxloop_cli/validators.py +31 -0
- fluxloop_cli-0.1.0.dist-info/METADATA +86 -0
- fluxloop_cli-0.1.0.dist-info/RECORD +24 -0
- fluxloop_cli-0.1.0.dist-info/WHEEL +5 -0
- fluxloop_cli-0.1.0.dist-info/entry_points.txt +2 -0
- fluxloop_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Generate command for producing input datasets."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from dotenv import dotenv_values
|
|
12
|
+
|
|
13
|
+
from ..config_loader import load_experiment_config
|
|
14
|
+
from ..input_generator import GenerationSettings, generate_inputs
|
|
15
|
+
from ..llm_generator import DEFAULT_STRATEGIES
|
|
16
|
+
from ..validators import parse_variation_strategies
|
|
17
|
+
from ..constants import DEFAULT_CONFIG_PATH, DEFAULT_ROOT_DIR_NAME
|
|
18
|
+
from ..project_paths import (
|
|
19
|
+
resolve_config_path,
|
|
20
|
+
resolve_project_relative,
|
|
21
|
+
resolve_root_dir,
|
|
22
|
+
)
|
|
23
|
+
from fluxloop.schemas import InputGenerationMode, VariationStrategy
|
|
24
|
+
|
|
25
|
+
app = typer.Typer()
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def inputs(
|
|
31
|
+
config_file: Path = typer.Option(
|
|
32
|
+
DEFAULT_CONFIG_PATH,
|
|
33
|
+
"--config",
|
|
34
|
+
"-c",
|
|
35
|
+
help="Path to experiment configuration file",
|
|
36
|
+
),
|
|
37
|
+
project: Optional[str] = typer.Option(
|
|
38
|
+
None,
|
|
39
|
+
"--project",
|
|
40
|
+
help="Project name under the FluxLoop root",
|
|
41
|
+
),
|
|
42
|
+
root: Path = typer.Option(
|
|
43
|
+
Path(DEFAULT_ROOT_DIR_NAME),
|
|
44
|
+
"--root",
|
|
45
|
+
help="FluxLoop root directory",
|
|
46
|
+
),
|
|
47
|
+
output_file: Optional[Path] = typer.Option(
|
|
48
|
+
None,
|
|
49
|
+
"--output",
|
|
50
|
+
"-o",
|
|
51
|
+
help="Path to write generated inputs file (defaults to setting.yaml -> inputs_file)",
|
|
52
|
+
),
|
|
53
|
+
limit: Optional[int] = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--limit",
|
|
56
|
+
"-l",
|
|
57
|
+
help="Maximum number of inputs to generate",
|
|
58
|
+
),
|
|
59
|
+
dry_run: bool = typer.Option(
|
|
60
|
+
False,
|
|
61
|
+
"--dry-run",
|
|
62
|
+
help="Print planned generation without creating a file",
|
|
63
|
+
),
|
|
64
|
+
overwrite: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--overwrite",
|
|
67
|
+
help="Allow overwriting an existing output file",
|
|
68
|
+
),
|
|
69
|
+
mode: Optional[InputGenerationMode] = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"--mode",
|
|
72
|
+
case_sensitive=False,
|
|
73
|
+
help="Generation mode: deterministic or llm",
|
|
74
|
+
),
|
|
75
|
+
strategy: List[str] = typer.Option( # type: ignore[assignment]
|
|
76
|
+
None,
|
|
77
|
+
"--strategy",
|
|
78
|
+
"-s",
|
|
79
|
+
help="Variation strategy to request (repeatable)",
|
|
80
|
+
),
|
|
81
|
+
llm_provider: Optional[str] = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--llm-provider",
|
|
84
|
+
help="Override LLM provider for input generation",
|
|
85
|
+
),
|
|
86
|
+
llm_model: Optional[str] = typer.Option(
|
|
87
|
+
None,
|
|
88
|
+
"--llm-model",
|
|
89
|
+
help="Override LLM model identifier",
|
|
90
|
+
),
|
|
91
|
+
llm_api_key: Optional[str] = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"--llm-api-key",
|
|
94
|
+
help="API key for LLM provider (falls back to FLUXLOOP_LLM_API_KEY)",
|
|
95
|
+
),
|
|
96
|
+
from_recording: Optional[Path] = typer.Option(
|
|
97
|
+
None,
|
|
98
|
+
"--from-recording",
|
|
99
|
+
help="Use a recorded call (JSONL) as template for generated inputs",
|
|
100
|
+
),
|
|
101
|
+
):
|
|
102
|
+
"""Generate input variations for review before running experiments."""
|
|
103
|
+
resolved_config = resolve_config_path(config_file, project, root)
|
|
104
|
+
if not resolved_config.exists():
|
|
105
|
+
console.print(f"[red]Error:[/red] Configuration file not found: {config_file}")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
# Generating inputs should not require the inputs file to exist yet.
|
|
109
|
+
require_inputs_file = False
|
|
110
|
+
|
|
111
|
+
# Load environment variables (project-level overrides root-level)
|
|
112
|
+
env_values: Dict[str, str] = {}
|
|
113
|
+
|
|
114
|
+
resolved_root = resolve_root_dir(root)
|
|
115
|
+
root_env_path = resolved_root / ".env"
|
|
116
|
+
if root_env_path.exists():
|
|
117
|
+
env_values.update(
|
|
118
|
+
{
|
|
119
|
+
key: value
|
|
120
|
+
for key, value in dotenv_values(root_env_path).items()
|
|
121
|
+
if value is not None
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if project:
|
|
126
|
+
project_env_path = resolve_project_relative(Path(".env"), project, root)
|
|
127
|
+
if project_env_path.exists():
|
|
128
|
+
env_values.update(
|
|
129
|
+
{
|
|
130
|
+
key: value
|
|
131
|
+
for key, value in dotenv_values(project_env_path).items()
|
|
132
|
+
if value is not None
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
config = load_experiment_config(
|
|
138
|
+
resolved_config,
|
|
139
|
+
require_inputs_file=require_inputs_file,
|
|
140
|
+
)
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
console.print(f"[red]Error loading configuration:[/red] {exc}")
|
|
143
|
+
raise typer.Exit(1)
|
|
144
|
+
|
|
145
|
+
# Determine output path (CLI option overrides config)
|
|
146
|
+
output_path = output_file or Path(config.inputs_file or "inputs/generated.yaml")
|
|
147
|
+
resolved_output = resolve_project_relative(output_path, project, root)
|
|
148
|
+
|
|
149
|
+
if resolved_output.exists() and not overwrite and not dry_run:
|
|
150
|
+
console.print(
|
|
151
|
+
f"[red]Error:[/red] Output file already exists: {resolved_output}\n"
|
|
152
|
+
"Use --overwrite to replace it."
|
|
153
|
+
)
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
console.print(f"📋 Loading configuration from: [cyan]{resolved_config}[/cyan]")
|
|
157
|
+
|
|
158
|
+
strategies: Optional[List[VariationStrategy]] = None
|
|
159
|
+
if strategy:
|
|
160
|
+
strategies = parse_variation_strategies(strategy)
|
|
161
|
+
|
|
162
|
+
# Apply CLI overrides to configuration
|
|
163
|
+
if mode:
|
|
164
|
+
config.input_generation.mode = mode
|
|
165
|
+
if mode == InputGenerationMode.LLM:
|
|
166
|
+
config.input_generation.llm.enabled = True
|
|
167
|
+
|
|
168
|
+
if llm_provider:
|
|
169
|
+
config.input_generation.llm.provider = llm_provider
|
|
170
|
+
config.input_generation.llm.enabled = True
|
|
171
|
+
|
|
172
|
+
if llm_model:
|
|
173
|
+
config.input_generation.llm.model = llm_model
|
|
174
|
+
config.input_generation.llm.enabled = True
|
|
175
|
+
|
|
176
|
+
resolved_api_key = (
|
|
177
|
+
llm_api_key
|
|
178
|
+
or config.input_generation.llm.api_key
|
|
179
|
+
or env_values.get("FLUXLOOP_LLM_API_KEY")
|
|
180
|
+
or env_values.get("OPENAI_API_KEY")
|
|
181
|
+
or os.getenv("FLUXLOOP_LLM_API_KEY")
|
|
182
|
+
or os.getenv("OPENAI_API_KEY")
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if resolved_api_key:
|
|
186
|
+
config.input_generation.llm.api_key = resolved_api_key
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
config.input_generation.mode == InputGenerationMode.LLM
|
|
190
|
+
and not config.input_generation.llm.api_key
|
|
191
|
+
):
|
|
192
|
+
console.print(
|
|
193
|
+
"[yellow]Warning:[/yellow] LLM mode requested but no API key provided."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
settings = GenerationSettings(
|
|
197
|
+
limit=limit,
|
|
198
|
+
dry_run=dry_run,
|
|
199
|
+
mode=mode,
|
|
200
|
+
strategies=strategies,
|
|
201
|
+
llm_api_key_override=llm_api_key,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
recording_template = None
|
|
205
|
+
if from_recording:
|
|
206
|
+
try:
|
|
207
|
+
recording_template = _load_recording_template(from_recording, config)
|
|
208
|
+
console.print(
|
|
209
|
+
"📝 Generating inputs from recording"
|
|
210
|
+
f"\n → Base content: [cyan]{recording_template['base_content'][:60]}[/cyan]"
|
|
211
|
+
)
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
console.print(f"[red]Failed to load recording template:[/red] {exc}")
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
result = generate_inputs(config, settings, recording_template=recording_template)
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
console.print(f"[red]Generation failed:[/red] {exc}")
|
|
220
|
+
if "--debug" in sys.argv:
|
|
221
|
+
console.print_exception()
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
|
|
224
|
+
if dry_run:
|
|
225
|
+
console.print("\n[yellow]Dry run mode - no file written[/yellow]")
|
|
226
|
+
console.print(f"Planned inputs: {len(result.entries)}")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
resolved_output.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
resolved_output.write_text(result.to_yaml(), encoding="utf-8")
|
|
231
|
+
|
|
232
|
+
strategies_used = result.metadata.get("strategies") or [s.value for s in DEFAULT_STRATEGIES]
|
|
233
|
+
|
|
234
|
+
console.print(
|
|
235
|
+
"\n[bold green]Generation complete![/bold green]"
|
|
236
|
+
f"\n📝 Inputs written to: [cyan]{resolved_output}[/cyan]"
|
|
237
|
+
f"\n✨ Total inputs: [green]{len(result.entries)}[/green]"
|
|
238
|
+
f"\n🧠 Mode: [magenta]{result.metadata.get('generation_mode', 'deterministic')}[/magenta]"
|
|
239
|
+
f"\n🎯 Strategies: [cyan]{', '.join(strategy for strategy in strategies_used)}[/cyan]"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _load_recording_template(path: Path, config: "ExperimentConfig") -> Dict[str, object]:
|
|
244
|
+
target_path = path
|
|
245
|
+
if not target_path.is_absolute():
|
|
246
|
+
source_dir = config.get_source_dir()
|
|
247
|
+
if source_dir:
|
|
248
|
+
target_path = (source_dir / target_path).resolve()
|
|
249
|
+
else:
|
|
250
|
+
target_path = target_path.resolve()
|
|
251
|
+
|
|
252
|
+
if not target_path.exists():
|
|
253
|
+
raise FileNotFoundError(f"Recording file not found: {target_path}")
|
|
254
|
+
|
|
255
|
+
with target_path.open("r", encoding="utf-8") as handle:
|
|
256
|
+
first_line = handle.readline()
|
|
257
|
+
|
|
258
|
+
if not first_line:
|
|
259
|
+
raise ValueError(f"Recording file is empty: {target_path}")
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
record = json.loads(first_line)
|
|
263
|
+
except json.JSONDecodeError as exc:
|
|
264
|
+
raise ValueError(f"Invalid JSON in recording file {target_path}: {exc}")
|
|
265
|
+
|
|
266
|
+
kwargs = record.get("kwargs", {})
|
|
267
|
+
base_content = _extract_content(kwargs)
|
|
268
|
+
if not base_content:
|
|
269
|
+
sample = json.dumps(kwargs, indent=2)[:500]
|
|
270
|
+
raise ValueError(
|
|
271
|
+
"Unable to locate textual content in recording. "
|
|
272
|
+
"Expected keys such as data.content, input, input_text, message, query, text, or content." \
|
|
273
|
+
f"\nRecording snapshot:\n{sample}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"base_content": base_content,
|
|
278
|
+
"full_kwargs": kwargs,
|
|
279
|
+
"target": record.get("target"),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _extract_content(kwargs: Dict[str, Any]) -> Optional[str]:
|
|
284
|
+
content_paths = [
|
|
285
|
+
"data.content",
|
|
286
|
+
"input",
|
|
287
|
+
"input_text",
|
|
288
|
+
"message",
|
|
289
|
+
"query",
|
|
290
|
+
"text",
|
|
291
|
+
"content",
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
for path in content_paths:
|
|
295
|
+
try:
|
|
296
|
+
current: Any = kwargs
|
|
297
|
+
for part in path.split('.'):
|
|
298
|
+
current = current[part]
|
|
299
|
+
if isinstance(current, str) and current:
|
|
300
|
+
return current
|
|
301
|
+
except (KeyError, TypeError):
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
return None
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Initialize command for creating new FluxLoop projects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.prompt import Confirm
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from ..templates import (
|
|
14
|
+
create_experiment_config,
|
|
15
|
+
create_sample_agent,
|
|
16
|
+
create_gitignore,
|
|
17
|
+
create_env_file,
|
|
18
|
+
)
|
|
19
|
+
from ..project_paths import resolve_root_dir, resolve_project_dir
|
|
20
|
+
from ..constants import DEFAULT_ROOT_DIR_NAME
|
|
21
|
+
|
|
22
|
+
app = typer.Typer()
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def project(
|
|
28
|
+
path: Path = typer.Argument(
|
|
29
|
+
Path(DEFAULT_ROOT_DIR_NAME),
|
|
30
|
+
help="Root directory for FluxLoop projects",
|
|
31
|
+
),
|
|
32
|
+
name: Optional[str] = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--name",
|
|
35
|
+
"-n",
|
|
36
|
+
help="Project name",
|
|
37
|
+
),
|
|
38
|
+
with_example: bool = typer.Option(
|
|
39
|
+
True,
|
|
40
|
+
"--with-example/--no-example",
|
|
41
|
+
help="Include example agent",
|
|
42
|
+
),
|
|
43
|
+
force: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--force",
|
|
46
|
+
"-f",
|
|
47
|
+
help="Overwrite existing files",
|
|
48
|
+
),
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initialize a new FluxLoop project.
|
|
52
|
+
|
|
53
|
+
This command creates:
|
|
54
|
+
- setting.yaml: Default experiment configuration
|
|
55
|
+
- .env: Environment variables template
|
|
56
|
+
- examples/: Sample agent code (optional)
|
|
57
|
+
"""
|
|
58
|
+
# Resolve path
|
|
59
|
+
root_dir = resolve_root_dir(path)
|
|
60
|
+
|
|
61
|
+
if not root_dir.exists():
|
|
62
|
+
console.print(f"[dim]Creating FluxLoop root directory at {root_dir}[/dim]")
|
|
63
|
+
root_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
if not name:
|
|
66
|
+
current = Path.cwd()
|
|
67
|
+
if current.parent == root_dir:
|
|
68
|
+
project_name = current.name
|
|
69
|
+
else:
|
|
70
|
+
console.print(
|
|
71
|
+
"[red]Error:[/red] Project name must be provided when running outside the FluxLoop root directory."
|
|
72
|
+
)
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
else:
|
|
75
|
+
project_name = name
|
|
76
|
+
project_path = resolve_project_dir(project_name, root_dir)
|
|
77
|
+
|
|
78
|
+
console.print(f"\n[bold blue]Initializing FluxLoop project:[/bold blue] {project_name}")
|
|
79
|
+
console.print(f"[dim]Location: {project_path}[/dim]\n")
|
|
80
|
+
|
|
81
|
+
console.print(f"\n[bold blue]Initializing FluxLoop project:[/bold blue] {project_name}")
|
|
82
|
+
console.print(f"[dim]Location: {project_path}[/dim]\n")
|
|
83
|
+
|
|
84
|
+
# Check if directory exists
|
|
85
|
+
if not project_path.exists():
|
|
86
|
+
if Confirm.ask(f"Directory {project_path} doesn't exist. Create it?"):
|
|
87
|
+
project_path.mkdir(parents=True)
|
|
88
|
+
else:
|
|
89
|
+
raise typer.Abort()
|
|
90
|
+
|
|
91
|
+
# Check for existing files
|
|
92
|
+
config_file = project_path / "setting.yaml"
|
|
93
|
+
env_file = project_path / ".env"
|
|
94
|
+
gitignore_file = project_path / ".gitignore"
|
|
95
|
+
|
|
96
|
+
if not force:
|
|
97
|
+
existing_files = []
|
|
98
|
+
if config_file.exists():
|
|
99
|
+
existing_files.append("setting.yaml")
|
|
100
|
+
if env_file.exists():
|
|
101
|
+
existing_files.append(".env")
|
|
102
|
+
|
|
103
|
+
if existing_files:
|
|
104
|
+
console.print(
|
|
105
|
+
f"[yellow]Warning:[/yellow] The following files already exist: {', '.join(existing_files)}"
|
|
106
|
+
)
|
|
107
|
+
if not Confirm.ask("Overwrite existing files?", default=False):
|
|
108
|
+
raise typer.Abort()
|
|
109
|
+
|
|
110
|
+
# Create configuration file
|
|
111
|
+
console.print("📝 Creating experiment configuration...")
|
|
112
|
+
config_content = create_experiment_config(project_name)
|
|
113
|
+
config_file.write_text(config_content)
|
|
114
|
+
|
|
115
|
+
# Ensure root .env exists, create project override template
|
|
116
|
+
root_env_file = root_dir / ".env"
|
|
117
|
+
if not root_env_file.exists():
|
|
118
|
+
console.print("🔐 Creating root environment template...")
|
|
119
|
+
root_env_file.write_text(create_env_file())
|
|
120
|
+
|
|
121
|
+
console.print("🔐 Creating project .env template...")
|
|
122
|
+
recordings_dir = project_path / "recordings"
|
|
123
|
+
recordings_dir.mkdir(exist_ok=True)
|
|
124
|
+
|
|
125
|
+
project_env_content = "# Project-specific overrides\n"
|
|
126
|
+
env_file.write_text(project_env_content)
|
|
127
|
+
|
|
128
|
+
# Update .gitignore
|
|
129
|
+
if not gitignore_file.exists():
|
|
130
|
+
console.print("📄 Creating .gitignore...")
|
|
131
|
+
gitignore_content = create_gitignore()
|
|
132
|
+
gitignore_file.write_text(gitignore_content)
|
|
133
|
+
|
|
134
|
+
# Create example agent if requested
|
|
135
|
+
if with_example:
|
|
136
|
+
console.print("🤖 Creating example agent...")
|
|
137
|
+
examples_dir = project_path / "examples"
|
|
138
|
+
examples_dir.mkdir(exist_ok=True)
|
|
139
|
+
|
|
140
|
+
agent_file = examples_dir / "simple_agent.py"
|
|
141
|
+
agent_content = create_sample_agent()
|
|
142
|
+
agent_file.write_text(agent_content)
|
|
143
|
+
|
|
144
|
+
# Display created structure
|
|
145
|
+
console.print("\n[bold green]✓ Project initialized successfully![/bold green]\n")
|
|
146
|
+
|
|
147
|
+
tree = Tree(f"[bold]{project_name}/[/bold]")
|
|
148
|
+
tree.add("📄 setting.yaml")
|
|
149
|
+
tree.add("🔐 .env")
|
|
150
|
+
tree.add("📄 .gitignore")
|
|
151
|
+
tree.add("📁 recordings/")
|
|
152
|
+
|
|
153
|
+
if with_example:
|
|
154
|
+
examples_tree = tree.add("📁 examples/")
|
|
155
|
+
examples_tree.add("🐍 simple_agent.py")
|
|
156
|
+
|
|
157
|
+
console.print(tree)
|
|
158
|
+
|
|
159
|
+
# Show next steps
|
|
160
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
161
|
+
console.print("1. Edit [cyan]setting.yaml[/cyan] to configure your experiment")
|
|
162
|
+
console.print("2. Set up environment variables in [cyan].env[/cyan]")
|
|
163
|
+
if with_example:
|
|
164
|
+
console.print("3. Try running: [green]fluxloop run experiment[/green]")
|
|
165
|
+
else:
|
|
166
|
+
console.print("3. Add your agent code")
|
|
167
|
+
console.print("4. Run: [green]fluxloop run experiment[/green]")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command()
|
|
171
|
+
def agent(
|
|
172
|
+
name: str = typer.Argument(
|
|
173
|
+
...,
|
|
174
|
+
help="Name of the agent module",
|
|
175
|
+
),
|
|
176
|
+
path: Path = typer.Option(
|
|
177
|
+
Path.cwd(),
|
|
178
|
+
"--path",
|
|
179
|
+
"-p",
|
|
180
|
+
help="Directory to create the agent in",
|
|
181
|
+
),
|
|
182
|
+
template: str = typer.Option(
|
|
183
|
+
"simple",
|
|
184
|
+
"--template",
|
|
185
|
+
"-t",
|
|
186
|
+
help="Agent template to use (simple, langchain, langgraph)",
|
|
187
|
+
),
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Create a new agent from a template.
|
|
191
|
+
"""
|
|
192
|
+
# Validate template
|
|
193
|
+
valid_templates = ["simple", "langchain", "langgraph"]
|
|
194
|
+
if template not in valid_templates:
|
|
195
|
+
console.print(
|
|
196
|
+
f"[red]Error:[/red] Invalid template '{template}'. "
|
|
197
|
+
f"Choose from: {', '.join(valid_templates)}"
|
|
198
|
+
)
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
# Create agent file
|
|
202
|
+
agent_dir = path / "agents"
|
|
203
|
+
agent_dir.mkdir(exist_ok=True)
|
|
204
|
+
|
|
205
|
+
agent_file = agent_dir / f"{name}.py"
|
|
206
|
+
|
|
207
|
+
if agent_file.exists():
|
|
208
|
+
if not Confirm.ask(f"Agent {name}.py already exists. Overwrite?", default=False):
|
|
209
|
+
raise typer.Abort()
|
|
210
|
+
|
|
211
|
+
# Create agent based on template
|
|
212
|
+
console.print(f"🤖 Creating {template} agent: {name}")
|
|
213
|
+
|
|
214
|
+
if template == "simple":
|
|
215
|
+
content = create_sample_agent()
|
|
216
|
+
else:
|
|
217
|
+
# TODO: Add more templates
|
|
218
|
+
content = create_sample_agent()
|
|
219
|
+
|
|
220
|
+
agent_file.write_text(content)
|
|
221
|
+
|
|
222
|
+
console.print(f"[green]✓[/green] Agent created: {agent_file}")
|
|
223
|
+
console.print("\nTo use this agent, update your setting.yaml:")
|
|
224
|
+
console.print(f" runner.module_path: agents.{name}")
|
|
225
|
+
console.print(" runner.function_name: run")
|