fluxloop-cli 0.2.12__tar.gz → 0.2.14__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 → fluxloop_cli-0.2.14}/PKG-INFO +1 -1
  2. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/__init__.py +1 -1
  3. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/runner.py +146 -9
  4. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/PKG-INFO +1 -1
  5. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/pyproject.toml +1 -1
  6. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/README.md +0 -0
  7. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/arg_binder.py +0 -0
  8. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/__init__.py +0 -0
  9. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/config.py +0 -0
  10. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/generate.py +0 -0
  11. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/init.py +0 -0
  12. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/parse.py +0 -0
  13. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/record.py +0 -0
  14. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/run.py +0 -0
  15. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/commands/status.py +0 -0
  16. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/config_loader.py +0 -0
  17. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/config_schema.py +0 -0
  18. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/constants.py +0 -0
  19. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/input_generator.py +0 -0
  20. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/llm_generator.py +0 -0
  21. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/main.py +0 -0
  22. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/project_paths.py +0 -0
  23. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/target_loader.py +0 -0
  24. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/templates.py +0 -0
  25. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli/validators.py +0 -0
  26. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/SOURCES.txt +0 -0
  27. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  28. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  29. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/requires.txt +0 -0
  30. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/fluxloop_cli.egg-info/top_level.txt +0 -0
  31. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/setup.cfg +0 -0
  32. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/tests/test_arg_binder.py +0 -0
  33. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/tests/test_config_command.py +0 -0
  34. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/tests/test_input_generator.py +0 -0
  35. {fluxloop_cli-0.2.12 → fluxloop_cli-0.2.14}/tests/test_target_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.12"
5
+ __version__ = "0.2.14"
6
6
 
7
7
  from .main import app
8
8
 
@@ -57,8 +57,17 @@ class ExperimentRunner:
57
57
 
58
58
  offline_dir = output_base / "artifacts"
59
59
  offline_dir.mkdir(parents=True, exist_ok=True)
60
+ # Ensure downstream load_env() calls don't re-enable collector unintentionally
61
+ should_use_collector = (not no_collector) and bool(config.collector_url)
62
+ if not should_use_collector:
63
+ os.environ["FLUXLOOP_USE_COLLECTOR"] = "false"
64
+ else:
65
+ os.environ["FLUXLOOP_USE_COLLECTOR"] = "true"
66
+ # Pin offline store dir in env so refresh_config respects our path
67
+ os.environ.setdefault("FLUXLOOP_OFFLINE_DIR", str(offline_dir))
68
+ os.environ.setdefault("FLUXLOOP_OFFLINE_ENABLED", "true")
60
69
  fluxloop.configure(
61
- use_collector=not no_collector and bool(config.collector_url),
70
+ use_collector=should_use_collector,
62
71
  collector_url=config.collector_url or None,
63
72
  api_key=config.collector_api_key,
64
73
  offline_store_enabled=True,
@@ -347,6 +356,11 @@ class ExperimentRunner:
347
356
  final_output = self._extract_final_output(callback_messages, observations)
348
357
  if final_output is not None:
349
358
  result = final_output
359
+ else:
360
+ # As a fallback, coerce async streams or objects into text
361
+ coerced = await self._coerce_result_to_text(result)
362
+ if coerced is not None:
363
+ result = coerced
350
364
 
351
365
  # Mark successful
352
366
  self.results["successful"] += 1
@@ -498,11 +512,15 @@ class ExperimentRunner:
498
512
  """Derive the final output from callbacks or observations."""
499
513
  for observation in reversed(observations):
500
514
  if observation.get("name") == "agent_final_response" and observation.get("output") is not None:
501
- return observation.get("output")
515
+ val = observation.get("output")
516
+ if not ExperimentRunner._looks_like_generator_repr(val):
517
+ return val
502
518
 
503
519
  for observation in reversed(observations):
504
520
  if observation.get("type") == "agent" and observation.get("output") is not None:
505
- return observation.get("output")
521
+ val = observation.get("output")
522
+ if not ExperimentRunner._looks_like_generator_repr(val):
523
+ return val
506
524
 
507
525
  send_messages = callback_messages.get("send") if callback_messages else None
508
526
  if send_messages:
@@ -511,6 +529,13 @@ class ExperimentRunner:
511
529
 
512
530
  return None
513
531
 
532
+ @staticmethod
533
+ def _looks_like_generator_repr(value: Any) -> bool:
534
+ if not isinstance(value, str):
535
+ return False
536
+ s = value.strip().lower()
537
+ return s.startswith("<async_generator ") or s.startswith("<generator ") or " object at 0x" in s
538
+
514
539
  async def _consume_async_gen(self, func: Callable, kwargs: Dict[str, Any]) -> Any:
515
540
  """Consume an async generator function by joining text chunks resolved from events."""
516
541
  gen = func(**kwargs)
@@ -522,7 +547,8 @@ class ExperimentRunner:
522
547
  Uses a copied contextvars context to ensure FluxLoop context is preserved
523
548
  across async iteration boundaries created by upstream frameworks.
524
549
  """
525
- path = (getattr(self.config.runner, "stream_output_path", None) or "message.delta").split(".")
550
+ # Prefer ChatKit-like path by default; configurable via runner.stream_output_path
551
+ path = (getattr(self.config.runner, "stream_output_path", None) or "update.delta").split(".")
526
552
  chunks: List[str] = []
527
553
 
528
554
  ctx = contextvars.copy_context()
@@ -585,7 +611,58 @@ class ExperimentRunner:
585
611
  text_attr = getattr(event, "text", None)
586
612
  if isinstance(text_attr, str) and text_attr:
587
613
  return text_attr
614
+ # Final deep fallback: search common fields recursively
615
+ return ExperimentRunner._deep_extract_text(event)
588
616
 
617
+ @staticmethod
618
+ def _deep_extract_text(obj: Any, *, _depth: int = 0) -> Optional[str]:
619
+ if _depth > 3 or obj is None:
620
+ return None
621
+ if isinstance(obj, str):
622
+ return obj if obj else None
623
+ # dict-like
624
+ if isinstance(obj, dict):
625
+ for key in ("delta", "text"):
626
+ val = obj.get(key)
627
+ if isinstance(val, str) and val:
628
+ return val
629
+ # content as list of parts with text
630
+ content = obj.get("content")
631
+ if isinstance(content, list):
632
+ parts: List[str] = []
633
+ for piece in content:
634
+ txt = ExperimentRunner._deep_extract_text(piece, _depth=_depth + 1)
635
+ if txt:
636
+ parts.append(txt)
637
+ if parts:
638
+ return " ".join(parts)
639
+ # Recurse selected fields
640
+ for key in ("update", "message", "item", "data"):
641
+ val = obj.get(key)
642
+ txt = ExperimentRunner._deep_extract_text(val, _depth=_depth + 1)
643
+ if txt:
644
+ return txt
645
+ return None
646
+ # object with attributes
647
+ for attr in ("delta", "text"):
648
+ val = getattr(obj, attr, None)
649
+ if isinstance(val, str) and val:
650
+ return val
651
+ for attr in ("content",):
652
+ val = getattr(obj, attr, None)
653
+ if isinstance(val, list):
654
+ parts: List[str] = []
655
+ for piece in val:
656
+ txt = ExperimentRunner._deep_extract_text(piece, _depth=_depth + 1)
657
+ if txt:
658
+ parts.append(txt)
659
+ if parts:
660
+ return " ".join(parts)
661
+ for attr in ("update", "message", "item", "data"):
662
+ val = getattr(obj, attr, None)
663
+ txt = ExperimentRunner._deep_extract_text(val, _depth=_depth + 1)
664
+ if txt:
665
+ return txt
589
666
  return None
590
667
 
591
668
  @staticmethod
@@ -610,6 +687,33 @@ class ExperimentRunner:
610
687
  "args": list(args),
611
688
  "kwargs": kwargs,
612
689
  }
690
+
691
+ async def _coerce_result_to_text(self, value: Any) -> Optional[str]:
692
+ """Best-effort conversion of agent result into text for summaries.
693
+
694
+ Handles async generators/iterables, known streaming item shapes, and strings.
695
+ """
696
+ import inspect as _inspect
697
+
698
+ if value is None:
699
+ return None
700
+
701
+ if isinstance(value, str):
702
+ return value
703
+
704
+ # Async iterable/generator → consume to text
705
+ if _inspect.isasyncgen(value) or hasattr(value, "__aiter__"):
706
+ try:
707
+ return await self._consume_async_iterable(value)
708
+ except Exception:
709
+ return None
710
+
711
+ # Try extracting text from known event-like objects
712
+ fallback = self._extract_stream_text(value)
713
+ if fallback:
714
+ return fallback
715
+
716
+ return None
613
717
 
614
718
  def _save_results(self) -> None:
615
719
  """Save results to output directory."""
@@ -676,16 +780,43 @@ class ExperimentRunner:
676
780
  if not trace_ids:
677
781
  return
678
782
 
679
- source_path = self.offline_dir / "observations.jsonl"
680
- if not source_path.exists():
783
+ # Gather candidate offline stores
784
+ candidates = []
785
+ try:
786
+ from fluxloop import get_config as _get_sdk_config # type: ignore
787
+ sdk_dir = Path(_get_sdk_config().offline_store_dir)
788
+ candidates.append(sdk_dir / "observations.jsonl")
789
+ except Exception:
790
+ pass
791
+
792
+ candidates.append(self.offline_dir / "observations.jsonl")
793
+ # Legacy fallback removed now that SDK defaults to experiments/artifacts
794
+
795
+ # Unique existing paths in priority order
796
+ seen = set()
797
+ existing: list[Path] = []
798
+ for p in candidates:
799
+ try:
800
+ rp = p.resolve()
801
+ except Exception:
802
+ continue
803
+ if rp in seen:
804
+ continue
805
+ seen.add(rp)
806
+ if rp.exists():
807
+ existing.append(rp)
808
+
809
+ if not existing:
681
810
  return
682
811
 
683
812
  destination = self.output_dir / "observations.jsonl"
684
813
  copied = 0
814
+ seen_lines = set()
685
815
 
686
- with source_path.open("r", encoding="utf-8") as src, destination.open(
687
- "w", encoding="utf-8"
688
- ) as dst:
816
+ with destination.open("w", encoding="utf-8") as dst:
817
+ for source_path in existing:
818
+ try:
819
+ with source_path.open("r", encoding="utf-8") as src:
689
820
  for line in src:
690
821
  if not line.strip():
691
822
  continue
@@ -694,8 +825,14 @@ class ExperimentRunner:
694
825
  except json.JSONDecodeError:
695
826
  continue
696
827
  if record.get("trace_id") in trace_ids:
828
+ key = (record.get("id"), record.get("start_time"))
829
+ if key in seen_lines:
830
+ continue
697
831
  dst.write(json.dumps(record) + "\n")
832
+ seen_lines.add(key)
698
833
  copied += 1
834
+ except OSError:
835
+ continue
699
836
 
700
837
  console.print(
701
838
  f"[green]✅ Saved {copied} observations to {destination.name}[/green]"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fluxloop-cli"
7
- version = "0.2.12"
7
+ version = "0.2.14"
8
8
  description = "FluxLoop CLI for running agent simulations"
9
9
  authors = [
10
10
  {name = "FluxLoop Team", email = "team@fluxloop.dev"}
File without changes
File without changes