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.

Files changed (34) hide show
  1. fluxloop_cli-0.1.0/PKG-INFO +86 -0
  2. fluxloop_cli-0.1.0/README.md +46 -0
  3. fluxloop_cli-0.1.0/fluxloop_cli/__init__.py +9 -0
  4. fluxloop_cli-0.1.0/fluxloop_cli/arg_binder.py +219 -0
  5. fluxloop_cli-0.1.0/fluxloop_cli/commands/__init__.py +5 -0
  6. fluxloop_cli-0.1.0/fluxloop_cli/commands/config.py +355 -0
  7. fluxloop_cli-0.1.0/fluxloop_cli/commands/generate.py +304 -0
  8. fluxloop_cli-0.1.0/fluxloop_cli/commands/init.py +225 -0
  9. fluxloop_cli-0.1.0/fluxloop_cli/commands/parse.py +293 -0
  10. fluxloop_cli-0.1.0/fluxloop_cli/commands/run.py +310 -0
  11. fluxloop_cli-0.1.0/fluxloop_cli/commands/status.py +227 -0
  12. fluxloop_cli-0.1.0/fluxloop_cli/config_loader.py +159 -0
  13. fluxloop_cli-0.1.0/fluxloop_cli/constants.py +12 -0
  14. fluxloop_cli-0.1.0/fluxloop_cli/input_generator.py +158 -0
  15. fluxloop_cli-0.1.0/fluxloop_cli/llm_generator.py +417 -0
  16. fluxloop_cli-0.1.0/fluxloop_cli/main.py +97 -0
  17. fluxloop_cli-0.1.0/fluxloop_cli/project_paths.py +80 -0
  18. fluxloop_cli-0.1.0/fluxloop_cli/runner.py +634 -0
  19. fluxloop_cli-0.1.0/fluxloop_cli/target_loader.py +95 -0
  20. fluxloop_cli-0.1.0/fluxloop_cli/templates.py +277 -0
  21. fluxloop_cli-0.1.0/fluxloop_cli/validators.py +31 -0
  22. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/PKG-INFO +86 -0
  23. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/SOURCES.txt +32 -0
  24. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/dependency_links.txt +1 -0
  25. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/entry_points.txt +2 -0
  26. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/requires.txt +21 -0
  27. fluxloop_cli-0.1.0/fluxloop_cli.egg-info/top_level.txt +1 -0
  28. fluxloop_cli-0.1.0/pyproject.toml +82 -0
  29. fluxloop_cli-0.1.0/setup.cfg +4 -0
  30. fluxloop_cli-0.1.0/tests/test_arg_binder.py +160 -0
  31. fluxloop_cli-0.1.0/tests/test_config_command.py +71 -0
  32. fluxloop_cli-0.1.0/tests/test_generate_from_recording.py +85 -0
  33. fluxloop_cli-0.1.0/tests/test_input_generator.py +160 -0
  34. 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,9 @@
1
+ """
2
+ FluxLoop CLI - Command-line interface for running agent simulations.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .main import app
8
+
9
+ __all__ = ["app"]
@@ -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
+
@@ -0,0 +1,5 @@
1
+ """CLI commands."""
2
+
3
+ from . import config, generate, init, parse, run, status
4
+
5
+ __all__ = ["config", "generate", "init", "parse", "run", "status"]