fluxloop-cli 0.1.0__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.
Potentially problematic release.
This version of fluxloop-cli might be problematic. Click here for more details.
- fluxloop_cli-0.1.0/PKG-INFO +86 -0
- fluxloop_cli-0.1.0/README.md +46 -0
- fluxloop_cli-0.1.0/fluxloop_cli/__init__.py +9 -0
- fluxloop_cli-0.1.0/fluxloop_cli/arg_binder.py +219 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/__init__.py +5 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/config.py +355 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/generate.py +304 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/init.py +225 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/parse.py +293 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/run.py +310 -0
- fluxloop_cli-0.1.0/fluxloop_cli/commands/status.py +227 -0
- fluxloop_cli-0.1.0/fluxloop_cli/config_loader.py +159 -0
- fluxloop_cli-0.1.0/fluxloop_cli/constants.py +12 -0
- fluxloop_cli-0.1.0/fluxloop_cli/input_generator.py +158 -0
- fluxloop_cli-0.1.0/fluxloop_cli/llm_generator.py +417 -0
- fluxloop_cli-0.1.0/fluxloop_cli/main.py +97 -0
- fluxloop_cli-0.1.0/fluxloop_cli/project_paths.py +80 -0
- fluxloop_cli-0.1.0/fluxloop_cli/runner.py +634 -0
- fluxloop_cli-0.1.0/fluxloop_cli/target_loader.py +95 -0
- fluxloop_cli-0.1.0/fluxloop_cli/templates.py +277 -0
- fluxloop_cli-0.1.0/fluxloop_cli/validators.py +31 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/PKG-INFO +86 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/SOURCES.txt +32 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/dependency_links.txt +1 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/entry_points.txt +2 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/requires.txt +21 -0
- fluxloop_cli-0.1.0/fluxloop_cli.egg-info/top_level.txt +1 -0
- fluxloop_cli-0.1.0/pyproject.toml +82 -0
- fluxloop_cli-0.1.0/setup.cfg +4 -0
- fluxloop_cli-0.1.0/tests/test_arg_binder.py +160 -0
- fluxloop_cli-0.1.0/tests/test_config_command.py +71 -0
- fluxloop_cli-0.1.0/tests/test_generate_from_recording.py +85 -0
- fluxloop_cli-0.1.0/tests/test_input_generator.py +160 -0
- fluxloop_cli-0.1.0/tests/test_target_loader.py +89 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxloop-cli
|
|
3
|
+
Version: 0.1.0
|
|
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/fluxloop/fluxloop
|
|
8
|
+
Project-URL: Documentation, https://docs.fluxloop.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/fluxloop/fluxloop
|
|
10
|
+
Project-URL: Issues, https://github.com/fluxloop/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 and managing FluxLoop workflows.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install fluxloop-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Initialize a new FluxLoop project
|
|
55
|
+
fluxloop init
|
|
56
|
+
|
|
57
|
+
# Run agent simulations
|
|
58
|
+
fluxloop run
|
|
59
|
+
|
|
60
|
+
# Generate test inputs
|
|
61
|
+
fluxloop generate
|
|
62
|
+
|
|
63
|
+
# Check status
|
|
64
|
+
fluxloop status
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- 🚀 **Easy Setup**: Initialize projects with a single command
|
|
70
|
+
- 🔄 **Simulation Runner**: Execute agent tests with various input scenarios
|
|
71
|
+
- 📝 **Input Generation**: LLM-powered test input generation
|
|
72
|
+
- 📊 **Rich Output**: Beautiful terminal UI with detailed progress tracking
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Python 3.8 or higher
|
|
77
|
+
- FluxLoop SDK
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
For detailed documentation, visit [https://docs.fluxloop.dev](https://docs.fluxloop.dev)
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
Apache License 2.0 - see LICENSE file for details
|
|
86
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# FluxLoop CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for running agent simulations and managing FluxLoop workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install fluxloop-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Initialize a new FluxLoop project
|
|
15
|
+
fluxloop init
|
|
16
|
+
|
|
17
|
+
# Run agent simulations
|
|
18
|
+
fluxloop run
|
|
19
|
+
|
|
20
|
+
# Generate test inputs
|
|
21
|
+
fluxloop generate
|
|
22
|
+
|
|
23
|
+
# Check status
|
|
24
|
+
fluxloop status
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- 🚀 **Easy Setup**: Initialize projects with a single command
|
|
30
|
+
- 🔄 **Simulation Runner**: Execute agent tests with various input scenarios
|
|
31
|
+
- 📝 **Input Generation**: LLM-powered test input generation
|
|
32
|
+
- 📊 **Rich Output**: Beautiful terminal UI with detailed progress tracking
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Python 3.8 or higher
|
|
37
|
+
- FluxLoop SDK
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
For detailed documentation, visit [https://docs.fluxloop.dev](https://docs.fluxloop.dev)
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
Apache License 2.0 - see LICENSE file for details
|
|
46
|
+
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _AwaitableNone:
|
|
14
|
+
"""Simple awaitable that resolves to ``None``."""
|
|
15
|
+
|
|
16
|
+
def __await__(self): # type: ignore[override]
|
|
17
|
+
async def _noop() -> None:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
return _noop().__await__()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArgBinder:
|
|
24
|
+
"""Bind call arguments using replay data when configured."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, experiment_config: ExperimentConfig) -> None:
|
|
27
|
+
self.config = experiment_config
|
|
28
|
+
self.replay_config: Optional[ReplayArgsConfig] = experiment_config.replay_args
|
|
29
|
+
self._recording: Optional[Dict[str, Any]] = None
|
|
30
|
+
|
|
31
|
+
if self.replay_config and self.replay_config.enabled:
|
|
32
|
+
self._load_recording()
|
|
33
|
+
|
|
34
|
+
def _load_recording(self) -> None:
|
|
35
|
+
replay = self.replay_config
|
|
36
|
+
assert replay is not None
|
|
37
|
+
|
|
38
|
+
if not replay.recording_file:
|
|
39
|
+
raise ValueError("replay_args.recording_file must be provided when replay is enabled")
|
|
40
|
+
|
|
41
|
+
file_path = Path(replay.recording_file)
|
|
42
|
+
if not file_path.is_absolute():
|
|
43
|
+
source_dir = self.config.get_source_dir()
|
|
44
|
+
if source_dir:
|
|
45
|
+
file_path = (source_dir / file_path).resolve()
|
|
46
|
+
else:
|
|
47
|
+
file_path = file_path.resolve()
|
|
48
|
+
|
|
49
|
+
if not file_path.exists():
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
f"Recording file not found: {file_path}. Make sure it is available locally."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
last_line: Optional[str] = None
|
|
55
|
+
with file_path.open("r", encoding="utf-8") as fp:
|
|
56
|
+
for line in fp:
|
|
57
|
+
if line.strip():
|
|
58
|
+
last_line = line
|
|
59
|
+
|
|
60
|
+
if not last_line:
|
|
61
|
+
raise ValueError(f"Recording file is empty: {file_path}")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self._recording = json.loads(last_line)
|
|
65
|
+
except json.JSONDecodeError as exc:
|
|
66
|
+
raise ValueError(f"Invalid JSON in recording file {file_path}: {exc}")
|
|
67
|
+
|
|
68
|
+
recording_target = self._recording.get("target")
|
|
69
|
+
config_target = self._resolve_config_target()
|
|
70
|
+
if recording_target and recording_target != config_target:
|
|
71
|
+
print(
|
|
72
|
+
"⚠️ Recording target mismatch:"
|
|
73
|
+
f" recording={recording_target}, config={config_target}. Proceeding anyway."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def bind_call_args(
|
|
77
|
+
self,
|
|
78
|
+
func: Callable,
|
|
79
|
+
*,
|
|
80
|
+
runtime_input: str,
|
|
81
|
+
iteration: int = 0,
|
|
82
|
+
) -> Dict[str, Any]:
|
|
83
|
+
"""Construct kwargs for calling *func* based on replay or inspection."""
|
|
84
|
+
|
|
85
|
+
if self._recording:
|
|
86
|
+
kwargs = self._recording.get("kwargs", {}).copy()
|
|
87
|
+
|
|
88
|
+
replay = self.replay_config
|
|
89
|
+
assert replay is not None
|
|
90
|
+
|
|
91
|
+
if replay.override_param_path:
|
|
92
|
+
try:
|
|
93
|
+
self._set_by_path(kwargs, replay.override_param_path, runtime_input)
|
|
94
|
+
except (KeyError, TypeError):
|
|
95
|
+
# If path missing, fall back to plain binding
|
|
96
|
+
return self._bind_by_signature(func, runtime_input)
|
|
97
|
+
|
|
98
|
+
self._restore_callables(kwargs, replay)
|
|
99
|
+
self._ensure_no_unmapped_callables(kwargs, replay)
|
|
100
|
+
return kwargs
|
|
101
|
+
|
|
102
|
+
return self._bind_by_signature(func, runtime_input)
|
|
103
|
+
|
|
104
|
+
def _bind_by_signature(self, func: Callable, runtime_input: str) -> Dict[str, Any]:
|
|
105
|
+
signature = inspect.signature(func)
|
|
106
|
+
parameters = list(signature.parameters.values())
|
|
107
|
+
|
|
108
|
+
if parameters and parameters[0].name == "self":
|
|
109
|
+
parameters = parameters[1:]
|
|
110
|
+
|
|
111
|
+
candidate_names = [
|
|
112
|
+
"input",
|
|
113
|
+
"input_text",
|
|
114
|
+
"message",
|
|
115
|
+
"query",
|
|
116
|
+
"text",
|
|
117
|
+
"content",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
for param in parameters:
|
|
121
|
+
if param.name in candidate_names:
|
|
122
|
+
return {param.name: runtime_input}
|
|
123
|
+
|
|
124
|
+
if parameters:
|
|
125
|
+
return {parameters[0].name: runtime_input}
|
|
126
|
+
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Cannot determine where to bind runtime input for function '{func.__name__}'."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _restore_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
|
|
132
|
+
for param_name, provider in replay.callable_providers.items():
|
|
133
|
+
if param_name not in kwargs:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
marker = kwargs[param_name]
|
|
137
|
+
if isinstance(marker, str) and marker.startswith("<"):
|
|
138
|
+
kwargs[param_name] = self._resolve_builtin_callable(provider, marker)
|
|
139
|
+
|
|
140
|
+
def _ensure_no_unmapped_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
|
|
141
|
+
callable_markers = {
|
|
142
|
+
key: value
|
|
143
|
+
for key, value in kwargs.items()
|
|
144
|
+
if isinstance(value, str) and value.startswith("<")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if not callable_markers:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
configured = set(replay.callable_providers.keys())
|
|
151
|
+
missing = [key for key in callable_markers if key not in configured]
|
|
152
|
+
if missing:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"Missing callable providers for recorded parameters: "
|
|
155
|
+
f"{', '.join(missing)}. Configure them under replay_args.callable_providers."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _resolve_builtin_callable(self, provider: str, marker: str) -> Callable:
|
|
159
|
+
is_async = marker.endswith(":async>")
|
|
160
|
+
|
|
161
|
+
if provider == "builtin:collector.send":
|
|
162
|
+
messages: list = []
|
|
163
|
+
|
|
164
|
+
def _record(args: Any, kwargs: Any) -> None:
|
|
165
|
+
messages.append((args, kwargs))
|
|
166
|
+
pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
|
|
167
|
+
|
|
168
|
+
def send(*args: Any, **kwargs: Any) -> _AwaitableNone:
|
|
169
|
+
_record(args, kwargs)
|
|
170
|
+
return _AwaitableNone()
|
|
171
|
+
|
|
172
|
+
async def send_async(*args: Any, **kwargs: Any) -> None:
|
|
173
|
+
_record(args, kwargs)
|
|
174
|
+
|
|
175
|
+
send.messages = messages
|
|
176
|
+
send_async.messages = messages
|
|
177
|
+
send.__fluxloop_builtin__ = "collector.send"
|
|
178
|
+
send_async.__fluxloop_builtin__ = "collector.send:async"
|
|
179
|
+
return send_async if is_async else send
|
|
180
|
+
|
|
181
|
+
if provider == "builtin:collector.error":
|
|
182
|
+
errors: list = []
|
|
183
|
+
|
|
184
|
+
def _record_error(args: Any, kwargs: Any) -> None:
|
|
185
|
+
errors.append((args, kwargs))
|
|
186
|
+
pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
|
|
187
|
+
print(f"[ERROR] {pretty}")
|
|
188
|
+
|
|
189
|
+
def send_error(*args: Any, **kwargs: Any) -> _AwaitableNone:
|
|
190
|
+
_record_error(args, kwargs)
|
|
191
|
+
return _AwaitableNone()
|
|
192
|
+
|
|
193
|
+
async def send_error_async(*args: Any, **kwargs: Any) -> None:
|
|
194
|
+
_record_error(args, kwargs)
|
|
195
|
+
|
|
196
|
+
send_error.errors = errors
|
|
197
|
+
send_error_async.errors = errors
|
|
198
|
+
send_error.__fluxloop_builtin__ = "collector.error"
|
|
199
|
+
send_error_async.__fluxloop_builtin__ = "collector.error:async"
|
|
200
|
+
return send_error_async if is_async else send_error
|
|
201
|
+
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Unknown callable provider '{provider}'. Supported providers: "
|
|
204
|
+
"builtin:collector.send, builtin:collector.error."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _set_by_path(self, payload: Dict[str, Any], path: str, value: Any) -> None:
|
|
208
|
+
parts = path.split(".")
|
|
209
|
+
current = payload
|
|
210
|
+
for key in parts[:-1]:
|
|
211
|
+
current = current[key]
|
|
212
|
+
current[parts[-1]] = value
|
|
213
|
+
|
|
214
|
+
def _resolve_config_target(self) -> Optional[str]:
|
|
215
|
+
runner = self.config.runner
|
|
216
|
+
if runner.target:
|
|
217
|
+
return runner.target
|
|
218
|
+
return f"{runner.module_path}:{runner.function_name}"
|
|
219
|
+
|