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 CHANGED
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.8"
5
+ __version__ = "0.2.11"
6
6
 
7
7
  from .main import app
8
8
 
@@ -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) and value.startswith("<")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.8
3
+ Version: 0.2.11
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -1,5 +1,5 @@
1
- fluxloop_cli/__init__.py,sha256=cQ5jftUmZZBndl3qNpiGNyEQMmWGM_uxoRRiWjaQq0o,142
2
- fluxloop_cli/arg_binder.py,sha256=ogb7FPdnDK1VYYIUbBWAH-AgsV5xY6yLMhiI352dRjI,8437
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=dzKv0OZiqBoFfO9LMP5rro9lBL3vkvNmlXqh0U-z9vU,24046
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.8.dist-info/METADATA,sha256=aNfj7fU6xRDeEShjycEaTXIzFWo1DakGldEIeuFnyas,3664
23
- fluxloop_cli-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- fluxloop_cli-0.2.8.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
25
- fluxloop_cli-0.2.8.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
26
- fluxloop_cli-0.2.8.dist-info/RECORD,,
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,,