fluxloop-cli 0.2.3__py3-none-any.whl → 0.2.4__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/runner.py +40 -3
- fluxloop_cli/target_loader.py +66 -26
- {fluxloop_cli-0.2.3.dist-info → fluxloop_cli-0.2.4.dist-info}/METADATA +13 -1
- {fluxloop_cli-0.2.3.dist-info → fluxloop_cli-0.2.4.dist-info}/RECORD +8 -8
- {fluxloop_cli-0.2.3.dist-info → fluxloop_cli-0.2.4.dist-info}/WHEEL +0 -0
- {fluxloop_cli-0.2.3.dist-info → fluxloop_cli-0.2.4.dist-info}/entry_points.txt +0 -0
- {fluxloop_cli-0.2.3.dist-info → fluxloop_cli-0.2.4.dist-info}/top_level.txt +0 -0
fluxloop_cli/__init__.py
CHANGED
fluxloop_cli/runner.py
CHANGED
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
|
+
import inspect
|
|
9
10
|
import time
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from pathlib import Path
|
|
@@ -397,11 +398,20 @@ class ExperimentRunner:
|
|
|
397
398
|
if callable(error_cb) and hasattr(error_cb, "errors"):
|
|
398
399
|
callback_store["error"] = error_cb.errors
|
|
399
400
|
|
|
401
|
+
if inspect.isasyncgenfunction(agent_func):
|
|
402
|
+
return await self._consume_async_gen(agent_func, kwargs)
|
|
403
|
+
|
|
400
404
|
if asyncio.iscoroutinefunction(agent_func):
|
|
401
|
-
|
|
405
|
+
result = await agent_func(**kwargs)
|
|
406
|
+
else:
|
|
407
|
+
loop = asyncio.get_event_loop()
|
|
408
|
+
result = await loop.run_in_executor(None, lambda: agent_func(**kwargs))
|
|
402
409
|
|
|
403
|
-
|
|
404
|
-
|
|
410
|
+
# If an async generator/iterable is returned, consume it into a string
|
|
411
|
+
if inspect.isasyncgen(result) or hasattr(result, "__aiter__"):
|
|
412
|
+
return await self._consume_async_iterable(result)
|
|
413
|
+
|
|
414
|
+
return result
|
|
405
415
|
|
|
406
416
|
async def _wait_for_callbacks(
|
|
407
417
|
self,
|
|
@@ -464,6 +474,33 @@ class ExperimentRunner:
|
|
|
464
474
|
|
|
465
475
|
return None
|
|
466
476
|
|
|
477
|
+
async def _consume_async_gen(self, func: Callable, kwargs: Dict[str, Any]) -> Any:
|
|
478
|
+
"""Consume an async generator function by joining text chunks resolved from events."""
|
|
479
|
+
gen = func(**kwargs)
|
|
480
|
+
return await self._consume_async_iterable(gen)
|
|
481
|
+
|
|
482
|
+
async def _consume_async_iterable(self, agen: Any) -> Any:
|
|
483
|
+
"""Consume async iterable items, extracting text via runner.stream_output_path."""
|
|
484
|
+
path = (getattr(self.config.runner, "stream_output_path", None) or "message.delta").split(".")
|
|
485
|
+
chunks: List[str] = []
|
|
486
|
+
async for item in agen:
|
|
487
|
+
val = self._get_by_path(item, path)
|
|
488
|
+
if isinstance(val, str) and val:
|
|
489
|
+
chunks.append(val)
|
|
490
|
+
return "".join(chunks) if chunks else None
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def _get_by_path(obj: Any, parts: List[str]) -> Any:
|
|
494
|
+
cur: Any = obj
|
|
495
|
+
for key in parts:
|
|
496
|
+
if cur is None:
|
|
497
|
+
return None
|
|
498
|
+
if isinstance(cur, dict):
|
|
499
|
+
cur = cur.get(key)
|
|
500
|
+
else:
|
|
501
|
+
cur = getattr(cur, key, None)
|
|
502
|
+
return cur
|
|
503
|
+
|
|
467
504
|
@staticmethod
|
|
468
505
|
def _extract_payload(args: Sequence[Any], kwargs: Dict[str, Any]) -> Any:
|
|
469
506
|
if kwargs:
|
fluxloop_cli/target_loader.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import importlib
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Callable, Optional
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
9
|
|
|
10
10
|
from fluxloop.schemas import RunnerConfig
|
|
11
11
|
|
|
@@ -51,45 +51,85 @@ class TargetLoader:
|
|
|
51
51
|
return str(path)
|
|
52
52
|
|
|
53
53
|
def _load_from_target(self, target: str) -> Callable:
|
|
54
|
+
"""Resolve a callable from target string.
|
|
55
|
+
|
|
56
|
+
Supports:
|
|
57
|
+
- module:function
|
|
58
|
+
- module:variable
|
|
59
|
+
- module:Class.method
|
|
60
|
+
- module:variable.method
|
|
61
|
+
If the first attribute is a class, attempts to construct an instance via:
|
|
62
|
+
- runner.factory if provided (module:callable)
|
|
63
|
+
- zero-argument constructor fallback
|
|
64
|
+
"""
|
|
65
|
+
|
|
54
66
|
if ":" not in target:
|
|
55
67
|
raise ValueError(
|
|
56
|
-
"Invalid runner.target format. Expected 'module:
|
|
68
|
+
"Invalid runner.target format. Expected 'module:symbol[.attr]'."
|
|
57
69
|
)
|
|
58
70
|
|
|
59
|
-
module_name,
|
|
71
|
+
module_name, attribute_chain = target.split(":", 1)
|
|
60
72
|
|
|
61
73
|
try:
|
|
62
74
|
module = importlib.import_module(module_name)
|
|
63
75
|
except ImportError as exc:
|
|
64
|
-
raise ValueError(
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Failed to import module '{module_name}' for target '{target}': {exc}"
|
|
78
|
+
)
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cls = getattr(module, class_name)
|
|
70
|
-
except AttributeError as exc:
|
|
71
|
-
raise ValueError(
|
|
72
|
-
f"Class '{class_name}' not found in module '{module_name}' for target '{target}'."
|
|
73
|
-
) from exc
|
|
80
|
+
parts = attribute_chain.split(".") if attribute_chain else []
|
|
81
|
+
if not parts:
|
|
82
|
+
raise ValueError(f"Invalid target '{target}': missing attribute path")
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
# Resolve first symbol from module
|
|
85
|
+
try:
|
|
86
|
+
obj: Any = getattr(module, parts[0])
|
|
87
|
+
except AttributeError as exc:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Symbol '{parts[0]}' not found in module '{module_name}' for target '{target}'."
|
|
90
|
+
) from exc
|
|
81
91
|
|
|
92
|
+
# If it's a class, construct instance
|
|
93
|
+
if isinstance(obj, type):
|
|
94
|
+
factory = getattr(self.config, "factory", None)
|
|
95
|
+
factory_kwargs = getattr(self.config, "factory_kwargs", {}) or {}
|
|
96
|
+
if factory:
|
|
97
|
+
if ":" not in factory:
|
|
98
|
+
raise ValueError("runner.factory must be in 'module:callable' format")
|
|
99
|
+
fmod, fname = factory.split(":", 1)
|
|
100
|
+
try:
|
|
101
|
+
fac_mod = importlib.import_module(fmod)
|
|
102
|
+
fac = getattr(fac_mod, fname)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Failed to import factory '{factory}' for target '{target}': {exc}"
|
|
106
|
+
) from exc
|
|
107
|
+
try:
|
|
108
|
+
obj = fac(**factory_kwargs)
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Factory '{factory}' failed to construct instance: {exc}"
|
|
112
|
+
) from exc
|
|
113
|
+
else:
|
|
114
|
+
try:
|
|
115
|
+
obj = obj()
|
|
116
|
+
except TypeError as exc:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"Cannot construct class without zero-argument constructor. "
|
|
119
|
+
"Provide runner.factory to construct the instance."
|
|
120
|
+
) from exc
|
|
121
|
+
|
|
122
|
+
# Traverse remaining attributes
|
|
123
|
+
for attr in parts[1:]:
|
|
82
124
|
try:
|
|
83
|
-
|
|
125
|
+
obj = getattr(obj, attr)
|
|
84
126
|
except AttributeError as exc:
|
|
85
127
|
raise ValueError(
|
|
86
|
-
f"
|
|
128
|
+
f"Attribute '{attr}' not found while resolving target '{target}'."
|
|
87
129
|
) from exc
|
|
88
130
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
f"Function or attribute '{attribute_part}' not found in module '{module_name}' for target '{target}'."
|
|
94
|
-
) from exc
|
|
131
|
+
if not callable(obj):
|
|
132
|
+
raise ValueError(f"Resolved target '{target}' is not callable.")
|
|
133
|
+
|
|
134
|
+
return obj
|
|
95
135
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fluxloop-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: FluxLoop CLI for running agent simulations
|
|
5
5
|
Author-email: FluxLoop Team <team@fluxloop.dev>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -71,6 +71,18 @@ The legacy `setting.yaml` is still supported, but new projects created with
|
|
|
71
71
|
|
|
72
72
|
Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
|
|
73
73
|
|
|
74
|
+
## Runner Integration Patterns
|
|
75
|
+
|
|
76
|
+
Configure how FluxLoop calls your code in `configs/simulation.yaml`:
|
|
77
|
+
|
|
78
|
+
- Module + function: `module_path`/`function_name` or `target: "module:function"`
|
|
79
|
+
- Class.method (zero-arg ctor): `target: "module:Class.method"`
|
|
80
|
+
- Module-scoped instance method: `target: "module:instance.method"`
|
|
81
|
+
- Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
|
|
82
|
+
- Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
|
|
83
|
+
|
|
84
|
+
See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
|
|
85
|
+
|
|
74
86
|
## Developing
|
|
75
87
|
|
|
76
88
|
Install dependencies and run tests:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
fluxloop_cli/__init__.py,sha256=
|
|
1
|
+
fluxloop_cli/__init__.py,sha256=FHpr2ZbHLVuqou57OtaIPfJQW-8mSfUeIRDZepsNWmk,142
|
|
2
2
|
fluxloop_cli/arg_binder.py,sha256=oluHrwe1nNVq7alxBhBEoZrLrYop-cRgXgSu59LJcw4,7827
|
|
3
3
|
fluxloop_cli/config_loader.py,sha256=IoOY39KxDWNjSWyN5a8n89ym7jiUDTmy7Id-o4E0Usk,9450
|
|
4
4
|
fluxloop_cli/config_schema.py,sha256=JZJRcMFun5hp3vKLAyek7W3NvISyzRzZt0BZAeSU38I,2415
|
|
@@ -7,8 +7,8 @@ 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=
|
|
11
|
-
fluxloop_cli/target_loader.py,sha256=
|
|
10
|
+
fluxloop_cli/runner.py,sha256=dzKv0OZiqBoFfO9LMP5rro9lBL3vkvNmlXqh0U-z9vU,24046
|
|
11
|
+
fluxloop_cli/target_loader.py,sha256=TdMBnuD7qkv71C48y5gCaK5sMFyPCoNyOt-JprL0jOI,4734
|
|
12
12
|
fluxloop_cli/templates.py,sha256=_QJxAq3JnylGryRjFwLVzaPmYMLsIl5eyVBNfkgGOeA,11207
|
|
13
13
|
fluxloop_cli/validators.py,sha256=_bLXmxUSzVrDtLjqyTba0bDqamRIaOUHhV4xZ7K36Xw,1155
|
|
14
14
|
fluxloop_cli/commands/__init__.py,sha256=sxJX1mJoOSJnH_iIuCqYT8tjh7_yxlJB702j_B_GPUw,164
|
|
@@ -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.4.dist-info/METADATA,sha256=TYbYk8XqV7NQCAfFAgm4HEWFfAKRCYHX5qca8BLi9Kw,3664
|
|
23
|
+
fluxloop_cli-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
fluxloop_cli-0.2.4.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
|
|
25
|
+
fluxloop_cli-0.2.4.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
|
|
26
|
+
fluxloop_cli-0.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|