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,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Status command for checking system and experiment status.
|
|
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.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from ..constants import DEFAULT_CONFIG_PATH, DEFAULT_ROOT_DIR_NAME
|
|
14
|
+
from ..project_paths import resolve_config_path, resolve_root_dir, resolve_project_relative
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def check(
|
|
22
|
+
verbose: bool = typer.Option(
|
|
23
|
+
False,
|
|
24
|
+
"--verbose",
|
|
25
|
+
"-v",
|
|
26
|
+
help="Show detailed information",
|
|
27
|
+
),
|
|
28
|
+
project: Optional[str] = typer.Option(
|
|
29
|
+
None,
|
|
30
|
+
"--project",
|
|
31
|
+
help="Project name under the FluxLoop root",
|
|
32
|
+
),
|
|
33
|
+
root: Path = typer.Option(
|
|
34
|
+
Path(DEFAULT_ROOT_DIR_NAME),
|
|
35
|
+
"--root",
|
|
36
|
+
help="FluxLoop root directory",
|
|
37
|
+
),
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Check FluxLoop system status.
|
|
41
|
+
|
|
42
|
+
Verifies:
|
|
43
|
+
- SDK installation
|
|
44
|
+
- Collector connectivity
|
|
45
|
+
- Configuration validity
|
|
46
|
+
"""
|
|
47
|
+
console.print("[bold]FluxLoop Status Check[/bold]\n")
|
|
48
|
+
|
|
49
|
+
status_table = Table(show_header=False)
|
|
50
|
+
status_table.add_column("Component", style="cyan")
|
|
51
|
+
status_table.add_column("Status")
|
|
52
|
+
status_table.add_column("Details", style="dim")
|
|
53
|
+
|
|
54
|
+
# Check SDK
|
|
55
|
+
try:
|
|
56
|
+
import fluxloop
|
|
57
|
+
sdk_version = fluxloop.__version__
|
|
58
|
+
status_table.add_row(
|
|
59
|
+
"SDK",
|
|
60
|
+
"[green]✓ Installed[/green]",
|
|
61
|
+
f"Version {sdk_version}"
|
|
62
|
+
)
|
|
63
|
+
except ImportError:
|
|
64
|
+
status_table.add_row(
|
|
65
|
+
"SDK",
|
|
66
|
+
"[red]✗ Not installed[/red]",
|
|
67
|
+
"Run: pip install fluxloop-sdk"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Check collector connectivity
|
|
71
|
+
try:
|
|
72
|
+
from fluxloop import get_config
|
|
73
|
+
from fluxloop.client import FluxLoopClient
|
|
74
|
+
|
|
75
|
+
config = get_config()
|
|
76
|
+
client = FluxLoopClient()
|
|
77
|
+
|
|
78
|
+
# Try to connect (this would need a health endpoint)
|
|
79
|
+
status_table.add_row(
|
|
80
|
+
"Collector",
|
|
81
|
+
"[yellow]? Unknown[/yellow]",
|
|
82
|
+
f"URL: {config.collector_url}"
|
|
83
|
+
)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
status_table.add_row(
|
|
86
|
+
"Collector",
|
|
87
|
+
"[red]✗ Error[/red]",
|
|
88
|
+
str(e) if verbose else "Connection failed"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Check for configuration file
|
|
92
|
+
resolved_config = resolve_config_path(DEFAULT_CONFIG_PATH, project, root)
|
|
93
|
+
if resolved_config.exists():
|
|
94
|
+
status_table.add_row(
|
|
95
|
+
"Config",
|
|
96
|
+
"[green]✓ Found[/green]",
|
|
97
|
+
str(resolved_config)
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
status_table.add_row(
|
|
101
|
+
"Config",
|
|
102
|
+
"[yellow]- Not found[/yellow]",
|
|
103
|
+
"Run: fluxloop init project"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Check environment
|
|
107
|
+
import os
|
|
108
|
+
if os.getenv("FLUXLOOP_API_KEY"):
|
|
109
|
+
status_table.add_row(
|
|
110
|
+
"API Key",
|
|
111
|
+
"[green]✓ Set[/green]",
|
|
112
|
+
"****" + os.getenv("FLUXLOOP_API_KEY")[-4:] if verbose else "Configured"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
status_table.add_row(
|
|
116
|
+
"API Key",
|
|
117
|
+
"[yellow]- Not set[/yellow]",
|
|
118
|
+
"Set FLUXLOOP_API_KEY in .env"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
console.print(status_table)
|
|
122
|
+
|
|
123
|
+
# Show recommendations
|
|
124
|
+
recommendations = []
|
|
125
|
+
if not resolved_config.exists():
|
|
126
|
+
recommendations.append("Initialize a project: [cyan]fluxloop init project[/cyan]")
|
|
127
|
+
if not os.getenv("FLUXLOOP_API_KEY"):
|
|
128
|
+
recommendations.append("Set up API key in .env file")
|
|
129
|
+
|
|
130
|
+
if recommendations:
|
|
131
|
+
console.print("\n[bold]Recommendations:[/bold]")
|
|
132
|
+
for rec in recommendations:
|
|
133
|
+
console.print(f" • {rec}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def experiments(
|
|
138
|
+
output_dir: Path = typer.Option(
|
|
139
|
+
Path("./experiments"),
|
|
140
|
+
"--output",
|
|
141
|
+
"-o",
|
|
142
|
+
help="Directory containing experiment results",
|
|
143
|
+
),
|
|
144
|
+
project: Optional[str] = typer.Option(
|
|
145
|
+
None,
|
|
146
|
+
"--project",
|
|
147
|
+
help="Project name under the FluxLoop root",
|
|
148
|
+
),
|
|
149
|
+
root: Path = typer.Option(
|
|
150
|
+
Path(DEFAULT_ROOT_DIR_NAME),
|
|
151
|
+
"--root",
|
|
152
|
+
help="FluxLoop root directory",
|
|
153
|
+
),
|
|
154
|
+
limit: int = typer.Option(
|
|
155
|
+
10,
|
|
156
|
+
"--limit",
|
|
157
|
+
"-l",
|
|
158
|
+
help="Number of experiments to show",
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
List recent experiments and their results.
|
|
163
|
+
"""
|
|
164
|
+
resolved_output = resolve_project_relative(output_dir, project, root)
|
|
165
|
+
|
|
166
|
+
if not resolved_output.exists():
|
|
167
|
+
console.print(f"[yellow]No experiments found in:[/yellow] {resolved_output}")
|
|
168
|
+
console.print("\nRun an experiment first: [cyan]fluxloop run experiment[/cyan]")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Find experiment directories
|
|
172
|
+
exp_dirs = sorted(
|
|
173
|
+
[d for d in resolved_output.iterdir() if d.is_dir()],
|
|
174
|
+
key=lambda x: x.stat().st_mtime,
|
|
175
|
+
reverse=True
|
|
176
|
+
)[:limit]
|
|
177
|
+
|
|
178
|
+
if not exp_dirs:
|
|
179
|
+
console.print("[yellow]No experiments found[/yellow]")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
console.print(f"[bold]Recent Experiments[/bold] (showing {len(exp_dirs)} of {len(list(resolved_output.iterdir()))})\n")
|
|
183
|
+
|
|
184
|
+
for exp_dir in exp_dirs:
|
|
185
|
+
# Try to load summary
|
|
186
|
+
summary_file = exp_dir / "summary.json"
|
|
187
|
+
if summary_file.exists():
|
|
188
|
+
import json
|
|
189
|
+
summary = json.loads(summary_file.read_text())
|
|
190
|
+
|
|
191
|
+
# Create mini table for each experiment
|
|
192
|
+
exp_panel = Panel(
|
|
193
|
+
f"[cyan]Name:[/cyan] {summary.get('name', 'Unknown')}\n"
|
|
194
|
+
f"[cyan]Date:[/cyan] {summary.get('date', 'Unknown')}\n"
|
|
195
|
+
f"[cyan]Runs:[/cyan] {summary.get('total_runs', 0)}\n"
|
|
196
|
+
f"[cyan]Success Rate:[/cyan] {summary.get('success_rate', 0)*100:.1f}%\n"
|
|
197
|
+
f"[cyan]Avg Duration:[/cyan] {summary.get('avg_duration_ms', 0):.0f}ms",
|
|
198
|
+
title=f"[bold]{exp_dir.name}[/bold]",
|
|
199
|
+
border_style="blue",
|
|
200
|
+
)
|
|
201
|
+
console.print(exp_panel)
|
|
202
|
+
else:
|
|
203
|
+
console.print(f"📁 {exp_dir.name} [dim](no summary available)[/dim]")
|
|
204
|
+
|
|
205
|
+
console.print() # Add spacing
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command()
|
|
209
|
+
def traces(
|
|
210
|
+
experiment_id: Optional[str] = typer.Argument(
|
|
211
|
+
None,
|
|
212
|
+
help="Experiment ID to show traces for",
|
|
213
|
+
),
|
|
214
|
+
limit: int = typer.Option(
|
|
215
|
+
10,
|
|
216
|
+
"--limit",
|
|
217
|
+
"-l",
|
|
218
|
+
help="Number of traces to show",
|
|
219
|
+
),
|
|
220
|
+
):
|
|
221
|
+
"""
|
|
222
|
+
List recent traces from experiments.
|
|
223
|
+
"""
|
|
224
|
+
# This would connect to the collector service
|
|
225
|
+
console.print("[yellow]Trace viewing requires collector service[/yellow]")
|
|
226
|
+
console.print("\nThis feature will be available when the collector is running.")
|
|
227
|
+
console.print("For now, check the experiment output directories for trace data.")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loader for experiments.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from .project_paths import resolve_config_path
|
|
13
|
+
|
|
14
|
+
# Add shared schemas to path
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "shared"))
|
|
16
|
+
|
|
17
|
+
from fluxloop.schemas import ExperimentConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_experiment_config(
|
|
21
|
+
config_file: Path,
|
|
22
|
+
*,
|
|
23
|
+
require_inputs_file: bool = True,
|
|
24
|
+
) -> ExperimentConfig:
|
|
25
|
+
"""
|
|
26
|
+
Load and validate experiment configuration from YAML file.
|
|
27
|
+
"""
|
|
28
|
+
resolved_path = resolve_config_path(config_file, project=None, root=None)
|
|
29
|
+
if not resolved_path.exists():
|
|
30
|
+
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
31
|
+
|
|
32
|
+
# Load YAML
|
|
33
|
+
with open(resolved_path) as f:
|
|
34
|
+
data = yaml.safe_load(f)
|
|
35
|
+
|
|
36
|
+
if not data:
|
|
37
|
+
raise ValueError("Configuration file is empty")
|
|
38
|
+
|
|
39
|
+
# Validate and create config object
|
|
40
|
+
try:
|
|
41
|
+
config = ExperimentConfig(**data)
|
|
42
|
+
config.set_source_dir(resolved_path.parent)
|
|
43
|
+
resolved_input_count = _resolve_input_count(
|
|
44
|
+
config,
|
|
45
|
+
require_inputs_file=require_inputs_file,
|
|
46
|
+
)
|
|
47
|
+
config.set_resolved_input_count(resolved_input_count)
|
|
48
|
+
except ValidationError as e:
|
|
49
|
+
# Format validation errors nicely
|
|
50
|
+
errors = []
|
|
51
|
+
for error in e.errors():
|
|
52
|
+
loc = ".".join(str(x) for x in error["loc"])
|
|
53
|
+
msg = error["msg"]
|
|
54
|
+
errors.append(f" - {loc}: {msg}")
|
|
55
|
+
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Invalid configuration:\n" + "\n".join(errors)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return config
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_input_count(
|
|
64
|
+
config: ExperimentConfig,
|
|
65
|
+
*,
|
|
66
|
+
require_inputs_file: bool = True,
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Determine the effective number of inputs for this configuration."""
|
|
69
|
+
if config.inputs_file:
|
|
70
|
+
inputs_path = (config.get_source_dir() / Path(config.inputs_file)
|
|
71
|
+
if config.get_source_dir() and not Path(config.inputs_file).is_absolute()
|
|
72
|
+
else Path(config.inputs_file)).resolve()
|
|
73
|
+
|
|
74
|
+
if not inputs_path.exists():
|
|
75
|
+
if require_inputs_file:
|
|
76
|
+
raise FileNotFoundError(
|
|
77
|
+
f"Inputs file not found when loading config: {inputs_path}"
|
|
78
|
+
)
|
|
79
|
+
return len(config.base_inputs)
|
|
80
|
+
|
|
81
|
+
with open(inputs_path, "r", encoding="utf-8") as f:
|
|
82
|
+
payload = yaml.safe_load(f)
|
|
83
|
+
|
|
84
|
+
if not payload:
|
|
85
|
+
if require_inputs_file:
|
|
86
|
+
raise ValueError(f"Inputs file is empty: {inputs_path}")
|
|
87
|
+
return len(config.base_inputs)
|
|
88
|
+
|
|
89
|
+
if isinstance(payload, dict):
|
|
90
|
+
entries = payload.get("inputs")
|
|
91
|
+
if entries is None:
|
|
92
|
+
if require_inputs_file:
|
|
93
|
+
raise ValueError("Inputs file must contain an 'inputs' list when using mapping format")
|
|
94
|
+
return len(config.base_inputs)
|
|
95
|
+
elif isinstance(payload, list):
|
|
96
|
+
entries = payload
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError("Inputs file must be either a list or a mapping with an 'inputs' key")
|
|
99
|
+
|
|
100
|
+
if not isinstance(entries, list):
|
|
101
|
+
if require_inputs_file:
|
|
102
|
+
raise ValueError("Inputs entries must be provided as a list")
|
|
103
|
+
return len(config.base_inputs)
|
|
104
|
+
|
|
105
|
+
return len(entries)
|
|
106
|
+
|
|
107
|
+
# No external file – rely on base_inputs multiplied by variation count
|
|
108
|
+
base_count = len(config.base_inputs)
|
|
109
|
+
variation_multiplier = max(1, config.variation_count)
|
|
110
|
+
return base_count * variation_multiplier if base_count else variation_multiplier
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_experiment_config(config: ExperimentConfig, config_file: Path) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Save experiment configuration to YAML file.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
config: ExperimentConfig object to save
|
|
119
|
+
config_file: Path to save configuration to
|
|
120
|
+
"""
|
|
121
|
+
# Convert to dict and save
|
|
122
|
+
data = config.to_dict()
|
|
123
|
+
|
|
124
|
+
with open(config_file, "w") as f:
|
|
125
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def merge_config_overrides(
|
|
129
|
+
config: ExperimentConfig,
|
|
130
|
+
overrides: Dict[str, Any]
|
|
131
|
+
) -> ExperimentConfig:
|
|
132
|
+
"""
|
|
133
|
+
Merge override values into configuration.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config: Base configuration
|
|
137
|
+
overrides: Dictionary of overrides (dot notation supported)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
New configuration with overrides applied
|
|
141
|
+
"""
|
|
142
|
+
# Convert config to dict
|
|
143
|
+
data = config.to_dict()
|
|
144
|
+
|
|
145
|
+
# Apply overrides
|
|
146
|
+
for key, value in overrides.items():
|
|
147
|
+
# Support dot notation (e.g., "runner.timeout")
|
|
148
|
+
keys = key.split(".")
|
|
149
|
+
current = data
|
|
150
|
+
|
|
151
|
+
for k in keys[:-1]:
|
|
152
|
+
if k not in current:
|
|
153
|
+
current[k] = {}
|
|
154
|
+
current = current[k]
|
|
155
|
+
|
|
156
|
+
current[keys[-1]] = value
|
|
157
|
+
|
|
158
|
+
# Create new config
|
|
159
|
+
return ExperimentConfig(**data)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shared constants for the FluxLoop CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DEFAULT_ROOT_DIR_NAME = "fluxloop"
|
|
7
|
+
DEFAULT_CONFIG_FILENAME = "setting.yaml"
|
|
8
|
+
LEGACY_CONFIG_FILENAMES = ("fluxloop.yaml",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_CONFIG_PATH = Path(DEFAULT_CONFIG_FILENAME)
|
|
12
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Utilities for generating input datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from fluxloop.schemas import (
|
|
13
|
+
ExperimentConfig,
|
|
14
|
+
InputGenerationMode,
|
|
15
|
+
PersonaConfig,
|
|
16
|
+
VariationStrategy,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .llm_generator import DEFAULT_STRATEGIES, LLMGenerationError, generate_llm_inputs
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .llm_generator import LLMClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GenerationSettings:
|
|
27
|
+
"""Options controlling input generation."""
|
|
28
|
+
|
|
29
|
+
limit: Optional[int] = None
|
|
30
|
+
dry_run: bool = False
|
|
31
|
+
mode: Optional[InputGenerationMode] = None
|
|
32
|
+
strategies: Optional[Sequence[VariationStrategy]] = None
|
|
33
|
+
use_cache: bool = True
|
|
34
|
+
llm_api_key_override: Optional[str] = None
|
|
35
|
+
llm_client: Optional["LLMClient"] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GeneratedInput:
|
|
40
|
+
"""Represents a single generated input entry."""
|
|
41
|
+
|
|
42
|
+
input: str
|
|
43
|
+
metadata: Dict[str, object] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GenerationResult:
|
|
48
|
+
"""Container for generation output."""
|
|
49
|
+
|
|
50
|
+
entries: List[GeneratedInput]
|
|
51
|
+
metadata: Dict[str, object]
|
|
52
|
+
|
|
53
|
+
def to_yaml(self) -> str:
|
|
54
|
+
payload = {
|
|
55
|
+
"generated_at": dt.datetime.utcnow().isoformat() + "Z",
|
|
56
|
+
"metadata": self.metadata,
|
|
57
|
+
"inputs": [
|
|
58
|
+
{
|
|
59
|
+
"input": entry.input,
|
|
60
|
+
"metadata": entry.metadata,
|
|
61
|
+
}
|
|
62
|
+
for entry in self.entries
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True)
|
|
66
|
+
|
|
67
|
+
def to_json(self) -> str:
|
|
68
|
+
return json.dumps(
|
|
69
|
+
{
|
|
70
|
+
"generated_at": dt.datetime.utcnow().isoformat() + "Z",
|
|
71
|
+
"metadata": self.metadata,
|
|
72
|
+
"inputs": [
|
|
73
|
+
{
|
|
74
|
+
"input": entry.input,
|
|
75
|
+
"metadata": entry.metadata,
|
|
76
|
+
}
|
|
77
|
+
for entry in self.entries
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
indent=2,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GenerationError(Exception):
|
|
85
|
+
"""Raised when input generation cannot proceed."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_inputs(
|
|
89
|
+
config: ExperimentConfig,
|
|
90
|
+
settings: GenerationSettings,
|
|
91
|
+
recording_template: Optional[Dict[str, Any]] = None,
|
|
92
|
+
) -> GenerationResult:
|
|
93
|
+
"""Generate deterministic input entries based on configuration."""
|
|
94
|
+
base_inputs = config.base_inputs
|
|
95
|
+
config_for_generation = config
|
|
96
|
+
if recording_template:
|
|
97
|
+
base_inputs = [
|
|
98
|
+
{
|
|
99
|
+
"input": recording_template["base_content"],
|
|
100
|
+
"metadata": {
|
|
101
|
+
"source": "recording",
|
|
102
|
+
"target": recording_template.get("target"),
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
config_for_generation = config.model_copy(update={"base_inputs": base_inputs})
|
|
107
|
+
|
|
108
|
+
if not base_inputs:
|
|
109
|
+
raise GenerationError("base_inputs must be defined to generate inputs")
|
|
110
|
+
|
|
111
|
+
mode = settings.mode or config_for_generation.input_generation.mode
|
|
112
|
+
|
|
113
|
+
if mode == InputGenerationMode.LLM:
|
|
114
|
+
strategies: Sequence[VariationStrategy]
|
|
115
|
+
if settings.strategies and len(settings.strategies) > 0:
|
|
116
|
+
strategies = list(settings.strategies)
|
|
117
|
+
elif config_for_generation.variation_strategies:
|
|
118
|
+
strategies = config_for_generation.variation_strategies
|
|
119
|
+
else:
|
|
120
|
+
strategies = DEFAULT_STRATEGIES
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
raw_entries = generate_llm_inputs(
|
|
124
|
+
config=config_for_generation,
|
|
125
|
+
strategies=strategies,
|
|
126
|
+
settings=settings,
|
|
127
|
+
)
|
|
128
|
+
except LLMGenerationError as exc:
|
|
129
|
+
raise GenerationError(str(exc)) from exc
|
|
130
|
+
|
|
131
|
+
entries = [
|
|
132
|
+
GeneratedInput(input=item["input"], metadata=item.get("metadata", {}))
|
|
133
|
+
for item in raw_entries
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
metadata = {
|
|
137
|
+
"config_name": config_for_generation.name,
|
|
138
|
+
"total_base_inputs": len(base_inputs),
|
|
139
|
+
"total_personas": len(config_for_generation.personas),
|
|
140
|
+
"strategies": [strategy.value for strategy in strategies],
|
|
141
|
+
"limit": settings.limit,
|
|
142
|
+
"generation_mode": InputGenerationMode.LLM.value,
|
|
143
|
+
"llm_provider": config_for_generation.input_generation.llm.provider,
|
|
144
|
+
"llm_model": config_for_generation.input_generation.llm.model,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if recording_template:
|
|
148
|
+
for entry in entries:
|
|
149
|
+
entry.metadata["args_template"] = "use_recorded"
|
|
150
|
+
entry.metadata["template_kwargs"] = recording_template.get("full_kwargs")
|
|
151
|
+
metadata["recording_target"] = recording_template.get("target")
|
|
152
|
+
metadata["recording_base_input"] = recording_template.get("base_content")
|
|
153
|
+
|
|
154
|
+
return GenerationResult(entries=entries, metadata=metadata)
|
|
155
|
+
|
|
156
|
+
raise GenerationError(
|
|
157
|
+
"Only LLM-based generation is supported. Set input_generation.mode to 'llm'"
|
|
158
|
+
)
|