fluxloop-cli 0.2.12__tar.gz

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 (35) hide show
  1. fluxloop_cli-0.2.12/PKG-INFO +102 -0
  2. fluxloop_cli-0.2.12/README.md +62 -0
  3. fluxloop_cli-0.2.12/fluxloop_cli/__init__.py +9 -0
  4. fluxloop_cli-0.2.12/fluxloop_cli/arg_binder.py +274 -0
  5. fluxloop_cli-0.2.12/fluxloop_cli/commands/__init__.py +5 -0
  6. fluxloop_cli-0.2.12/fluxloop_cli/commands/config.py +365 -0
  7. fluxloop_cli-0.2.12/fluxloop_cli/commands/generate.py +224 -0
  8. fluxloop_cli-0.2.12/fluxloop_cli/commands/init.py +252 -0
  9. fluxloop_cli-0.2.12/fluxloop_cli/commands/parse.py +293 -0
  10. fluxloop_cli-0.2.12/fluxloop_cli/commands/record.py +150 -0
  11. fluxloop_cli-0.2.12/fluxloop_cli/commands/run.py +314 -0
  12. fluxloop_cli-0.2.12/fluxloop_cli/commands/status.py +263 -0
  13. fluxloop_cli-0.2.12/fluxloop_cli/config_loader.py +332 -0
  14. fluxloop_cli-0.2.12/fluxloop_cli/config_schema.py +83 -0
  15. fluxloop_cli-0.2.12/fluxloop_cli/constants.py +27 -0
  16. fluxloop_cli-0.2.12/fluxloop_cli/input_generator.py +138 -0
  17. fluxloop_cli-0.2.12/fluxloop_cli/llm_generator.py +417 -0
  18. fluxloop_cli-0.2.12/fluxloop_cli/main.py +98 -0
  19. fluxloop_cli-0.2.12/fluxloop_cli/project_paths.py +132 -0
  20. fluxloop_cli-0.2.12/fluxloop_cli/runner.py +755 -0
  21. fluxloop_cli-0.2.12/fluxloop_cli/target_loader.py +157 -0
  22. fluxloop_cli-0.2.12/fluxloop_cli/templates.py +448 -0
  23. fluxloop_cli-0.2.12/fluxloop_cli/validators.py +41 -0
  24. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/PKG-INFO +102 -0
  25. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/SOURCES.txt +33 -0
  26. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/dependency_links.txt +1 -0
  27. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/entry_points.txt +2 -0
  28. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/requires.txt +21 -0
  29. fluxloop_cli-0.2.12/fluxloop_cli.egg-info/top_level.txt +1 -0
  30. fluxloop_cli-0.2.12/pyproject.toml +82 -0
  31. fluxloop_cli-0.2.12/setup.cfg +4 -0
  32. fluxloop_cli-0.2.12/tests/test_arg_binder.py +160 -0
  33. fluxloop_cli-0.2.12/tests/test_config_command.py +95 -0
  34. fluxloop_cli-0.2.12/tests/test_input_generator.py +160 -0
  35. fluxloop_cli-0.2.12/tests/test_target_loader.py +89 -0
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: fluxloop-cli
3
+ Version: 0.2.12
4
+ Summary: FluxLoop CLI for running agent simulations
5
+ Author-email: FluxLoop Team <team@fluxloop.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/chuckgu/fluxloop
8
+ Project-URL: Documentation, https://docs.fluxloop.dev
9
+ Project-URL: Repository, https://github.com/chuckgu/fluxloop
10
+ Project-URL: Issues, https://github.com/chuckgu/fluxloop/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: typer[all]>=0.9.0
23
+ Requires-Dist: pydantic>=2.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: httpx>=0.24.0
26
+ Requires-Dist: rich>=13.0
27
+ Requires-Dist: python-dotenv>=1.0.0
28
+ Requires-Dist: fluxloop>=0.1.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.0; extra == "dev"
35
+ Requires-Dist: black>=23.0; extra == "dev"
36
+ Provides-Extra: openai
37
+ Requires-Dist: openai>=1.0.0; extra == "openai"
38
+ Provides-Extra: anthropic
39
+ Requires-Dist: anthropic>=0.7.0; extra == "anthropic"
40
+
41
+ # FluxLoop CLI
42
+
43
+ Command-line interface for running agent simulations.
44
+
45
+ ## Installation
46
+
47
+ ```
48
+ pip install fluxloop-cli
49
+ ```
50
+
51
+ ## Configuration Overview (v0.2.0)
52
+
53
+ FluxLoop CLI now stores experiment settings in four files under `configs/`:
54
+
55
+ - `configs/project.yaml` – project metadata, collector defaults
56
+ - `configs/input.yaml` – personas, base inputs, input generation options
57
+ - `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
58
+ - `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
59
+
60
+ The legacy `setting.yaml` is still supported, but new projects created with
61
+ `fluxloop init project` will generate the structured layout above.
62
+
63
+ ## Key Commands
64
+
65
+ - `fluxloop init project` – scaffold a new project (configs, `.env`, examples)
66
+ - `fluxloop generate inputs` – produce input variations for the active project
67
+ - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
68
+ - `fluxloop parse experiment` – convert experiment outputs into readable artifacts
69
+ - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
70
+ - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
71
+
72
+ Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
73
+
74
+ ## Runner Integration Patterns
75
+
76
+ Configure how FluxLoop calls your code in `configs/simulation.yaml`:
77
+
78
+ - Module + function: `module_path`/`function_name` or `target: "module:function"`
79
+ - Class.method (zero-arg ctor): `target: "module:Class.method"`
80
+ - Module-scoped instance method: `target: "module:instance.method"`
81
+ - Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
82
+ - Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
83
+
84
+ See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
85
+
86
+ ## Developing
87
+
88
+ Install dependencies and run tests:
89
+
90
+ ```
91
+ python -m venv .venv
92
+ source .venv/bin/activate
93
+ pip install -e .[dev]
94
+ pytest
95
+ ```
96
+
97
+ To package the CLI:
98
+
99
+ ```
100
+ ./build.sh
101
+ ```
102
+
@@ -0,0 +1,62 @@
1
+ # FluxLoop CLI
2
+
3
+ Command-line interface for running agent simulations.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ pip install fluxloop-cli
9
+ ```
10
+
11
+ ## Configuration Overview (v0.2.0)
12
+
13
+ FluxLoop CLI now stores experiment settings in four files under `configs/`:
14
+
15
+ - `configs/project.yaml` – project metadata, collector defaults
16
+ - `configs/input.yaml` – personas, base inputs, input generation options
17
+ - `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
18
+ - `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
19
+
20
+ The legacy `setting.yaml` is still supported, but new projects created with
21
+ `fluxloop init project` will generate the structured layout above.
22
+
23
+ ## Key Commands
24
+
25
+ - `fluxloop init project` – scaffold a new project (configs, `.env`, examples)
26
+ - `fluxloop generate inputs` – produce input variations for the active project
27
+ - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
28
+ - `fluxloop parse experiment` – convert experiment outputs into readable artifacts
29
+ - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
30
+ - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
31
+
32
+ Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
33
+
34
+ ## Runner Integration Patterns
35
+
36
+ Configure how FluxLoop calls your code in `configs/simulation.yaml`:
37
+
38
+ - Module + function: `module_path`/`function_name` or `target: "module:function"`
39
+ - Class.method (zero-arg ctor): `target: "module:Class.method"`
40
+ - Module-scoped instance method: `target: "module:instance.method"`
41
+ - Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
42
+ - Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
43
+
44
+ See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
45
+
46
+ ## Developing
47
+
48
+ Install dependencies and run tests:
49
+
50
+ ```
51
+ python -m venv .venv
52
+ source .venv/bin/activate
53
+ pip install -e .[dev]
54
+ pytest
55
+ ```
56
+
57
+ To package the CLI:
58
+
59
+ ```
60
+ ./build.sh
61
+ ```
62
+
@@ -0,0 +1,9 @@
1
+ """
2
+ FluxLoop CLI - Command-line interface for running agent simulations.
3
+ """
4
+
5
+ __version__ = "0.2.12"
6
+
7
+ from .main import app
8
+
9
+ __all__ = ["app"]
@@ -0,0 +1,274 @@
1
+ """Argument binding utilities with replay support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, Optional
9
+
10
+
11
+ class _AttrDict(dict):
12
+ """Dictionary that also supports attribute access for keys."""
13
+
14
+ def __getattr__(self, item: str) -> Any: # type: ignore[override]
15
+ try:
16
+ return self[item]
17
+ except KeyError as exc: # pragma: no cover
18
+ raise AttributeError(item) from exc
19
+
20
+ def __setattr__(self, key: str, value: Any) -> None: # type: ignore[override]
21
+ self[key] = value
22
+
23
+ def __delattr__(self, item: str) -> None: # type: ignore[override]
24
+ try:
25
+ del self[item]
26
+ except KeyError as exc: # pragma: no cover
27
+ raise AttributeError(item) from exc
28
+
29
+ from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig
30
+
31
+
32
+ class _AwaitableNone:
33
+ """Simple awaitable that resolves to ``None``."""
34
+
35
+ def __await__(self): # type: ignore[override]
36
+ async def _noop() -> None:
37
+ return None
38
+
39
+ return _noop().__await__()
40
+
41
+
42
+ class ArgBinder:
43
+ """Bind call arguments using replay data when configured."""
44
+
45
+ def __init__(self, experiment_config: ExperimentConfig) -> None:
46
+ self.config = experiment_config
47
+ self.replay_config: Optional[ReplayArgsConfig] = experiment_config.replay_args
48
+ self._recording: Optional[Dict[str, Any]] = None
49
+
50
+ if self.replay_config and self.replay_config.enabled:
51
+ self._load_recording()
52
+
53
+ def _load_recording(self) -> None:
54
+ replay = self.replay_config
55
+ assert replay is not None
56
+
57
+ if not replay.recording_file:
58
+ raise ValueError("replay_args.recording_file must be provided when replay is enabled")
59
+
60
+ file_path = Path(replay.recording_file)
61
+ if not file_path.is_absolute():
62
+ source_dir = self.config.get_source_dir()
63
+ if source_dir:
64
+ file_path = (source_dir / file_path).resolve()
65
+ else:
66
+ file_path = file_path.resolve()
67
+
68
+ if not file_path.exists():
69
+ raise FileNotFoundError(
70
+ f"Recording file not found: {file_path}. Make sure it is available locally."
71
+ )
72
+
73
+ last_line: Optional[str] = None
74
+ with file_path.open("r", encoding="utf-8") as fp:
75
+ for line in fp:
76
+ if line.strip():
77
+ last_line = line
78
+
79
+ if not last_line:
80
+ raise ValueError(f"Recording file is empty: {file_path}")
81
+
82
+ try:
83
+ self._recording = json.loads(last_line)
84
+ except json.JSONDecodeError as exc:
85
+ raise ValueError(f"Invalid JSON in recording file {file_path}: {exc}")
86
+
87
+ recording_target = self._recording.get("target")
88
+ config_target = self._resolve_config_target()
89
+ if recording_target and recording_target != config_target:
90
+ print(
91
+ "⚠️ Recording target mismatch:"
92
+ f" recording={recording_target}, config={config_target}. Proceeding anyway."
93
+ )
94
+
95
+ def bind_call_args(
96
+ self,
97
+ func: Callable,
98
+ *,
99
+ runtime_input: str,
100
+ iteration: int = 0,
101
+ ) -> Dict[str, Any]:
102
+ """Construct kwargs for calling *func* based on replay or inspection."""
103
+
104
+ if self._recording:
105
+ kwargs = self._recording.get("kwargs", {}).copy()
106
+
107
+ replay = self.replay_config
108
+ assert replay is not None
109
+
110
+ if replay.override_param_path:
111
+ try:
112
+ self._set_by_path(kwargs, replay.override_param_path, runtime_input)
113
+ except (KeyError, TypeError):
114
+ # If path missing, fall back to plain binding
115
+ return self._bind_by_signature(func, runtime_input)
116
+
117
+ self._restore_callables(kwargs, replay)
118
+ self._ensure_no_unmapped_callables(kwargs, replay)
119
+ return self._hydrate_structures(kwargs)
120
+
121
+ return self._bind_by_signature(func, runtime_input)
122
+
123
+ def _bind_by_signature(self, func: Callable, runtime_input: str) -> Dict[str, Any]:
124
+ signature = inspect.signature(func)
125
+ parameters = list(signature.parameters.values())
126
+
127
+ if parameters and parameters[0].name == "self":
128
+ parameters = parameters[1:]
129
+
130
+ candidate_names = [
131
+ "input",
132
+ "input_text",
133
+ "message",
134
+ "query",
135
+ "text",
136
+ "content",
137
+ ]
138
+
139
+ for param in parameters:
140
+ if param.name in candidate_names:
141
+ return {param.name: runtime_input}
142
+
143
+ if parameters:
144
+ return {parameters[0].name: runtime_input}
145
+
146
+ raise ValueError(
147
+ f"Cannot determine where to bind runtime input for function '{func.__name__}'."
148
+ )
149
+
150
+ def _restore_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
151
+ for param_name, provider in replay.callable_providers.items():
152
+ if param_name not in kwargs:
153
+ continue
154
+
155
+ marker = kwargs[param_name]
156
+ if isinstance(marker, str) and marker.startswith("<"):
157
+ kwargs[param_name] = self._resolve_builtin_callable(provider, marker)
158
+
159
+ def _ensure_no_unmapped_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
160
+ callable_markers = {
161
+ key: value
162
+ for key, value in kwargs.items()
163
+ if isinstance(value, str)
164
+ and value.startswith("<")
165
+ and not value.startswith("<repr:")
166
+ }
167
+
168
+ if not callable_markers:
169
+ return
170
+
171
+ configured = set(replay.callable_providers.keys())
172
+ missing = [key for key in callable_markers if key not in configured]
173
+ if missing:
174
+ raise ValueError(
175
+ "Missing callable providers for recorded parameters: "
176
+ f"{', '.join(missing)}. Configure them under replay_args.callable_providers."
177
+ )
178
+
179
+ def _hydrate_structures(self, payload: Dict[str, Any]) -> Dict[str, Any]:
180
+ return {key: self._hydrate_value(value) for key, value in payload.items()}
181
+
182
+ def _hydrate_value(self, value: Any) -> Any:
183
+ if callable(value):
184
+ return value
185
+ if isinstance(value, _AttrDict):
186
+ return value
187
+ if isinstance(value, dict):
188
+ return _AttrDict({k: self._hydrate_value(v) for k, v in value.items()})
189
+ if isinstance(value, list):
190
+ return [self._hydrate_value(item) for item in value]
191
+ return value
192
+
193
+ def _resolve_builtin_callable(self, provider: str, marker: str) -> Callable:
194
+ is_async = marker.endswith(":async>")
195
+
196
+ if provider == "builtin:collector.send":
197
+ messages: list = []
198
+
199
+ def _record(args: Any, kwargs: Any) -> None:
200
+ messages.append((args, kwargs))
201
+ pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
202
+
203
+ def send(*args: Any, **kwargs: Any) -> _AwaitableNone:
204
+ _record(args, kwargs)
205
+ return _AwaitableNone()
206
+
207
+ async def send_async(*args: Any, **kwargs: Any) -> None:
208
+ _record(args, kwargs)
209
+
210
+ send.messages = messages
211
+ send_async.messages = messages
212
+ send.__fluxloop_builtin__ = "collector.send"
213
+ send_async.__fluxloop_builtin__ = "collector.send:async"
214
+ return send_async if is_async else send
215
+
216
+ if provider == "builtin:collector.error":
217
+ errors: list = []
218
+
219
+ def _record_error(args: Any, kwargs: Any) -> None:
220
+ errors.append((args, kwargs))
221
+ pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
222
+ print(f"[ERROR] {pretty}")
223
+
224
+ def send_error(*args: Any, **kwargs: Any) -> _AwaitableNone:
225
+ _record_error(args, kwargs)
226
+ return _AwaitableNone()
227
+
228
+ async def send_error_async(*args: Any, **kwargs: Any) -> None:
229
+ _record_error(args, kwargs)
230
+
231
+ send_error.errors = errors
232
+ send_error_async.errors = errors
233
+ send_error.__fluxloop_builtin__ = "collector.error"
234
+ send_error_async.__fluxloop_builtin__ = "collector.error:async"
235
+ return send_error_async if is_async else send_error
236
+
237
+ raise ValueError(
238
+ f"Unknown callable provider '{provider}'. Supported providers: "
239
+ "builtin:collector.send, builtin:collector.error."
240
+ )
241
+
242
+ def _set_by_path(self, payload: Dict[str, Any], path: str, value: Any) -> None:
243
+ parts = path.split(".")
244
+ current: Any = payload
245
+
246
+ for key in parts[:-1]:
247
+ if isinstance(current, list):
248
+ index = self._coerce_list_index(key)
249
+ current = current[index]
250
+ else:
251
+ current = current[key]
252
+
253
+ final_key = parts[-1]
254
+ if isinstance(current, list):
255
+ index = self._coerce_list_index(final_key)
256
+ current[index] = value
257
+ else:
258
+ current[final_key] = value
259
+
260
+ @staticmethod
261
+ def _coerce_list_index(key: str) -> int:
262
+ try:
263
+ return int(key)
264
+ except ValueError as exc:
265
+ raise TypeError(
266
+ "List index segments in override_param_path must be integers"
267
+ ) from exc
268
+
269
+ def _resolve_config_target(self) -> Optional[str]:
270
+ runner = self.config.runner
271
+ if runner.target:
272
+ return runner.target
273
+ return f"{runner.module_path}:{runner.function_name}"
274
+
@@ -0,0 +1,5 @@
1
+ """CLI commands."""
2
+
3
+ from . import config, generate, init, parse, run, status, record
4
+
5
+ __all__ = ["config", "generate", "init", "parse", "run", "status", "record"]