fluxloop-cli 0.2.8__py3-none-any.whl → 0.2.11__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 +1 -1
- fluxloop_cli/arg_binder.py +37 -2
- fluxloop_cli/runner.py +68 -3
- {fluxloop_cli-0.2.8.dist-info → fluxloop_cli-0.2.11.dist-info}/METADATA +1 -1
- {fluxloop_cli-0.2.8.dist-info → fluxloop_cli-0.2.11.dist-info}/RECORD +8 -8
- {fluxloop_cli-0.2.8.dist-info → fluxloop_cli-0.2.11.dist-info}/WHEEL +0 -0
- {fluxloop_cli-0.2.8.dist-info → fluxloop_cli-0.2.11.dist-info}/entry_points.txt +0 -0
- {fluxloop_cli-0.2.8.dist-info → fluxloop_cli-0.2.11.dist-info}/top_level.txt +0 -0
fluxloop_cli/__init__.py
CHANGED
fluxloop_cli/arg_binder.py
CHANGED
|
@@ -7,6 +7,25 @@ import json
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Callable, Dict, Optional
|
|
9
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
|
+
|
|
10
29
|
from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig
|
|
11
30
|
|
|
12
31
|
|
|
@@ -97,7 +116,7 @@ class ArgBinder:
|
|
|
97
116
|
|
|
98
117
|
self._restore_callables(kwargs, replay)
|
|
99
118
|
self._ensure_no_unmapped_callables(kwargs, replay)
|
|
100
|
-
return kwargs
|
|
119
|
+
return self._hydrate_structures(kwargs)
|
|
101
120
|
|
|
102
121
|
return self._bind_by_signature(func, runtime_input)
|
|
103
122
|
|
|
@@ -141,7 +160,9 @@ class ArgBinder:
|
|
|
141
160
|
callable_markers = {
|
|
142
161
|
key: value
|
|
143
162
|
for key, value in kwargs.items()
|
|
144
|
-
if isinstance(value, str)
|
|
163
|
+
if isinstance(value, str)
|
|
164
|
+
and value.startswith("<")
|
|
165
|
+
and not value.startswith("<repr:")
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
if not callable_markers:
|
|
@@ -155,6 +176,20 @@ class ArgBinder:
|
|
|
155
176
|
f"{', '.join(missing)}. Configure them under replay_args.callable_providers."
|
|
156
177
|
)
|
|
157
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
|
+
|
|
158
193
|
def _resolve_builtin_callable(self, provider: str, marker: str) -> Callable:
|
|
159
194
|
is_async = marker.endswith(":async>")
|
|
160
195
|
|
fluxloop_cli/runner.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import inspect
|
|
10
|
+
import os
|
|
10
11
|
import time
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
from pathlib import Path
|
|
@@ -39,6 +40,9 @@ class ExperimentRunner:
|
|
|
39
40
|
self.config = config
|
|
40
41
|
self.no_collector = no_collector
|
|
41
42
|
|
|
43
|
+
# Apply project environment (.env + inline environment variables)
|
|
44
|
+
self._apply_environment()
|
|
45
|
+
|
|
42
46
|
# Configure output directories (respect config location for relative paths)
|
|
43
47
|
output_base = Path(config.output_directory)
|
|
44
48
|
if not output_base.is_absolute():
|
|
@@ -51,6 +55,7 @@ class ExperimentRunner:
|
|
|
51
55
|
output_base.mkdir(parents=True, exist_ok=True)
|
|
52
56
|
|
|
53
57
|
offline_dir = output_base / "artifacts"
|
|
58
|
+
offline_dir.mkdir(parents=True, exist_ok=True)
|
|
54
59
|
fluxloop.configure(
|
|
55
60
|
use_collector=not no_collector and bool(config.collector_url),
|
|
56
61
|
collector_url=config.collector_url or None,
|
|
@@ -78,6 +83,31 @@ class ExperimentRunner:
|
|
|
78
83
|
# Helpers for target loading and argument binding
|
|
79
84
|
self._arg_binder = ArgBinder(config)
|
|
80
85
|
|
|
86
|
+
def _apply_environment(self) -> None:
|
|
87
|
+
"""Load environment variables from .env and runner settings."""
|
|
88
|
+
|
|
89
|
+
source_dir = self.config.get_source_dir()
|
|
90
|
+
env_candidates: List[Path] = []
|
|
91
|
+
|
|
92
|
+
if source_dir:
|
|
93
|
+
env_candidates.append(source_dir / ".env")
|
|
94
|
+
parent = source_dir.parent
|
|
95
|
+
if parent != source_dir:
|
|
96
|
+
env_candidates.append(parent / ".env")
|
|
97
|
+
|
|
98
|
+
for candidate in env_candidates:
|
|
99
|
+
if candidate.exists():
|
|
100
|
+
try:
|
|
101
|
+
fluxloop.load_env(candidate, override=True, refresh_config=True)
|
|
102
|
+
except Exception:
|
|
103
|
+
console.log(f"[yellow]Warning:[/yellow] Failed to load environment from {candidate}")
|
|
104
|
+
else:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
env_vars = getattr(self.config.runner, "environment_vars", {}) or {}
|
|
108
|
+
for key, value in env_vars.items():
|
|
109
|
+
os.environ[key] = str(value)
|
|
110
|
+
|
|
81
111
|
def _load_agent(self) -> Callable:
|
|
82
112
|
"""Load the agent function from module path."""
|
|
83
113
|
loader = TargetLoader(self.config.runner, source_dir=self.config.get_source_dir())
|
|
@@ -487,6 +517,11 @@ class ExperimentRunner:
|
|
|
487
517
|
val = self._get_by_path(item, path)
|
|
488
518
|
if isinstance(val, str) and val:
|
|
489
519
|
chunks.append(val)
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
fallback = self._extract_stream_text(item)
|
|
523
|
+
if fallback:
|
|
524
|
+
chunks.append(fallback)
|
|
490
525
|
return "".join(chunks) if chunks else None
|
|
491
526
|
|
|
492
527
|
@staticmethod
|
|
@@ -501,6 +536,39 @@ class ExperimentRunner:
|
|
|
501
536
|
cur = getattr(cur, key, None)
|
|
502
537
|
return cur
|
|
503
538
|
|
|
539
|
+
@staticmethod
|
|
540
|
+
def _extract_stream_text(event: Any) -> Optional[str]:
|
|
541
|
+
"""Best-effort extraction of text payloads from streaming events."""
|
|
542
|
+
|
|
543
|
+
update = getattr(event, "update", None)
|
|
544
|
+
if update is not None:
|
|
545
|
+
delta = getattr(update, "delta", None)
|
|
546
|
+
if isinstance(delta, str) and delta:
|
|
547
|
+
return delta
|
|
548
|
+
|
|
549
|
+
content = getattr(update, "content", None)
|
|
550
|
+
text = getattr(content, "text", None) if content is not None else None
|
|
551
|
+
if isinstance(text, str) and text:
|
|
552
|
+
return text
|
|
553
|
+
|
|
554
|
+
item = getattr(event, "item", None)
|
|
555
|
+
if item is not None:
|
|
556
|
+
content = getattr(item, "content", None)
|
|
557
|
+
if isinstance(content, list):
|
|
558
|
+
parts: List[str] = []
|
|
559
|
+
for piece in content:
|
|
560
|
+
text = getattr(piece, "text", None)
|
|
561
|
+
if isinstance(text, str) and text:
|
|
562
|
+
parts.append(text)
|
|
563
|
+
if parts:
|
|
564
|
+
return " ".join(parts)
|
|
565
|
+
|
|
566
|
+
text_attr = getattr(event, "text", None)
|
|
567
|
+
if isinstance(text_attr, str) and text_attr:
|
|
568
|
+
return text_attr
|
|
569
|
+
|
|
570
|
+
return None
|
|
571
|
+
|
|
504
572
|
@staticmethod
|
|
505
573
|
def _extract_payload(args: Sequence[Any], kwargs: Dict[str, Any]) -> Any:
|
|
506
574
|
if kwargs:
|
|
@@ -591,9 +659,6 @@ class ExperimentRunner:
|
|
|
591
659
|
|
|
592
660
|
source_path = self.offline_dir / "observations.jsonl"
|
|
593
661
|
if not source_path.exists():
|
|
594
|
-
console.print(
|
|
595
|
-
f"[yellow]⚠️ Observations file not found: {source_path}[/yellow]"
|
|
596
|
-
)
|
|
597
662
|
return
|
|
598
663
|
|
|
599
664
|
destination = self.output_dir / "observations.jsonl"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
fluxloop_cli/__init__.py,sha256=
|
|
2
|
-
fluxloop_cli/arg_binder.py,sha256=
|
|
1
|
+
fluxloop_cli/__init__.py,sha256=NgsS7Nxhi0nRomvzdHVsIubTKzIZKUykYNcexsVQb1o,143
|
|
2
|
+
fluxloop_cli/arg_binder.py,sha256=yaK79zRNBEyTO1CB76WLLKQwYBW404HEKeDC2Y1QlLk,9725
|
|
3
3
|
fluxloop_cli/config_loader.py,sha256=PYy0CfGVbU8jpPbx4sJzOu7i3BbrkQMNaRiSOp_uX9g,10307
|
|
4
4
|
fluxloop_cli/config_schema.py,sha256=JZJRcMFun5hp3vKLAyek7W3NvISyzRzZt0BZAeSU38I,2415
|
|
5
5
|
fluxloop_cli/constants.py,sha256=oMYCkUUqy2LORNY99gDRCOkgLOPrT1FY_Vrylo-QSSw,719
|
|
@@ -7,7 +7,7 @@ fluxloop_cli/input_generator.py,sha256=ldlVdPSDfGsP9zO2RALk7QmZjkIvUzTaxDgwOjuPB
|
|
|
7
7
|
fluxloop_cli/llm_generator.py,sha256=SosP5DeZuhBLEM6bj7BDp-7mckvVhtNJMEk2ZgV143M,12894
|
|
8
8
|
fluxloop_cli/main.py,sha256=xJgrKMv6cN6Q1SNz0rbL4owHsN5CSiLkAaAd747WYds,2584
|
|
9
9
|
fluxloop_cli/project_paths.py,sha256=FoHp-g3aY1nytxGys85Oy3wJ6gmiKU6FVOwkgTtlHNA,4128
|
|
10
|
-
fluxloop_cli/runner.py,sha256=
|
|
10
|
+
fluxloop_cli/runner.py,sha256=cb9e8lhK0NVIhAfhyMejgNVSDHVWwBiYCOIMQGhXqkw,26387
|
|
11
11
|
fluxloop_cli/target_loader.py,sha256=ACCu2izqGKoOrEiNnAajH0FgLZcw3j1pWn5rAhEuWFU,5528
|
|
12
12
|
fluxloop_cli/templates.py,sha256=Y7sE_hSm8q2IGXwXDLq-jCEBS_eCUzDPdQSrQELTzWk,11230
|
|
13
13
|
fluxloop_cli/validators.py,sha256=_bLXmxUSzVrDtLjqyTba0bDqamRIaOUHhV4xZ7K36Xw,1155
|
|
@@ -19,8 +19,8 @@ fluxloop_cli/commands/parse.py,sha256=AVPYi59ejFWx4TYyM7JuI69koxDVkIBxy4LBRDMMbF
|
|
|
19
19
|
fluxloop_cli/commands/record.py,sha256=56ASu3Np6TX8_F8caMBJArv10ag5M96kJ-sII8df-5Q,4803
|
|
20
20
|
fluxloop_cli/commands/run.py,sha256=NLkBN2puqDLdHhKhilLriXsZnm7pMDMcoWeBSEXPM-o,9660
|
|
21
21
|
fluxloop_cli/commands/status.py,sha256=ERZrWoSP3V7dz5A_TEE5b8E0nGwsPggP4nXw4tLOzxE,7841
|
|
22
|
-
fluxloop_cli-0.2.
|
|
23
|
-
fluxloop_cli-0.2.
|
|
24
|
-
fluxloop_cli-0.2.
|
|
25
|
-
fluxloop_cli-0.2.
|
|
26
|
-
fluxloop_cli-0.2.
|
|
22
|
+
fluxloop_cli-0.2.11.dist-info/METADATA,sha256=RhgDfMz7r9-YfhZRQDmNvqut1k1KHcZqycbBM1tzGkI,3665
|
|
23
|
+
fluxloop_cli-0.2.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
fluxloop_cli-0.2.11.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
|
|
25
|
+
fluxloop_cli-0.2.11.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
|
|
26
|
+
fluxloop_cli-0.2.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|