penguiflow 1.0.0__py3-none-any.whl → 1.0.3__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 penguiflow might be problematic. Click here for more details.
- penguiflow/__init__.py +3 -1
- penguiflow/core.py +22 -0
- penguiflow/viz.py +72 -1
- {penguiflow-1.0.0.dist-info → penguiflow-1.0.3.dist-info}/METADATA +35 -2
- penguiflow-1.0.3.dist-info/RECORD +13 -0
- penguiflow-1.0.0.dist-info/RECORD +0 -13
- {penguiflow-1.0.0.dist-info → penguiflow-1.0.3.dist-info}/WHEEL +0 -0
- {penguiflow-1.0.0.dist-info → penguiflow-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {penguiflow-1.0.0.dist-info → penguiflow-1.0.3.dist-info}/top_level.txt +0 -0
penguiflow/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from .node import Node, NodePolicy
|
|
|
15
15
|
from .patterns import join_k, map_concurrent, predicate_router, union_router
|
|
16
16
|
from .registry import ModelRegistry
|
|
17
17
|
from .types import WM, FinalAnswer, Headers, Message, PlanStep, Thought
|
|
18
|
+
from .viz import flow_to_mermaid
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
20
21
|
"__version__",
|
|
@@ -37,7 +38,8 @@ __all__ = [
|
|
|
37
38
|
"join_k",
|
|
38
39
|
"predicate_router",
|
|
39
40
|
"union_router",
|
|
41
|
+
"flow_to_mermaid",
|
|
40
42
|
"create",
|
|
41
43
|
]
|
|
42
44
|
|
|
43
|
-
__version__ = "1.0.
|
|
45
|
+
__version__ = "1.0.3"
|
penguiflow/core.py
CHANGED
|
@@ -288,6 +288,8 @@ class PenguiFlow:
|
|
|
288
288
|
raise RuntimeError("PenguiFlow already running")
|
|
289
289
|
self._running = True
|
|
290
290
|
self._registry = registry
|
|
291
|
+
if registry is not None:
|
|
292
|
+
self._ensure_registry_covers_nodes(registry)
|
|
291
293
|
loop = asyncio.get_running_loop()
|
|
292
294
|
|
|
293
295
|
for node in self._nodes:
|
|
@@ -542,6 +544,26 @@ class PenguiFlow:
|
|
|
542
544
|
},
|
|
543
545
|
)
|
|
544
546
|
|
|
547
|
+
def _ensure_registry_covers_nodes(self, registry: ModelRegistry) -> None:
|
|
548
|
+
missing: list[str] = []
|
|
549
|
+
for node in self._nodes:
|
|
550
|
+
if node.policy.validate == "none":
|
|
551
|
+
continue
|
|
552
|
+
node_name = node.name
|
|
553
|
+
if node_name is None:
|
|
554
|
+
continue
|
|
555
|
+
try:
|
|
556
|
+
registry.adapters(node_name)
|
|
557
|
+
except KeyError:
|
|
558
|
+
missing.append(node_name)
|
|
559
|
+
|
|
560
|
+
if missing:
|
|
561
|
+
formatted = ", ".join(sorted(missing))
|
|
562
|
+
raise RuntimeError(
|
|
563
|
+
"ModelRegistry is missing entries for nodes requiring validation: "
|
|
564
|
+
f"{formatted}"
|
|
565
|
+
)
|
|
566
|
+
|
|
545
567
|
|
|
546
568
|
PlaybookFactory = Callable[[], tuple["PenguiFlow", ModelRegistry | None]]
|
|
547
569
|
|
penguiflow/viz.py
CHANGED
|
@@ -2,4 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .core import Endpoint
|
|
9
|
+
from .node import Node
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
12
|
+
from .core import PenguiFlow
|
|
13
|
+
|
|
14
|
+
__all__ = ["flow_to_mermaid"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def flow_to_mermaid(flow: PenguiFlow, *, direction: str = "TD") -> str:
|
|
18
|
+
"""Render the flow graph as a Mermaid diagram string.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
flow:
|
|
23
|
+
The `PenguiFlow` instance to visualize.
|
|
24
|
+
direction:
|
|
25
|
+
Mermaid graph direction ("TD", "LR", etc.). Defaults to top-down.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
lines: list[str] = [f"graph {direction}"]
|
|
29
|
+
nodes: set[object] = set()
|
|
30
|
+
|
|
31
|
+
for floe in flow._floes: # noqa: SLF001 - visualization accesses internals by design
|
|
32
|
+
if floe.source is not None:
|
|
33
|
+
nodes.add(floe.source)
|
|
34
|
+
if floe.target is not None:
|
|
35
|
+
nodes.add(floe.target)
|
|
36
|
+
|
|
37
|
+
id_lookup: dict[object, str] = {}
|
|
38
|
+
used_ids: set[str] = set()
|
|
39
|
+
|
|
40
|
+
for entity in nodes:
|
|
41
|
+
label = _display_label(entity)
|
|
42
|
+
node_id = _unique_id(label, used_ids)
|
|
43
|
+
used_ids.add(node_id)
|
|
44
|
+
id_lookup[entity] = node_id
|
|
45
|
+
lines.append(f" {node_id}[\"{label}\"]")
|
|
46
|
+
|
|
47
|
+
for floe in flow._floes: # noqa: SLF001
|
|
48
|
+
source = floe.source
|
|
49
|
+
target = floe.target
|
|
50
|
+
if source is None or target is None:
|
|
51
|
+
continue
|
|
52
|
+
src_id = id_lookup.get(source)
|
|
53
|
+
tgt_id = id_lookup.get(target)
|
|
54
|
+
if src_id is None or tgt_id is None:
|
|
55
|
+
continue
|
|
56
|
+
lines.append(f" {src_id} --> {tgt_id}")
|
|
57
|
+
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _display_label(entity: object) -> str:
|
|
62
|
+
if isinstance(entity, Node):
|
|
63
|
+
return entity.name or entity.node_id
|
|
64
|
+
if isinstance(entity, Endpoint):
|
|
65
|
+
return entity.name
|
|
66
|
+
return str(entity)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _unique_id(label: str, used: set[str]) -> str:
|
|
70
|
+
base = re.sub(r"[^0-9A-Za-z_]", "_", label) or "node"
|
|
71
|
+
candidate = base
|
|
72
|
+
index = 1
|
|
73
|
+
while candidate in used:
|
|
74
|
+
index += 1
|
|
75
|
+
candidate = f"{base}_{index}"
|
|
76
|
+
return candidate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: penguiflow
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Async agent orchestration primitives.
|
|
5
5
|
Author: PenguiFlow Team
|
|
6
6
|
License: MIT License
|
|
@@ -99,7 +99,7 @@ from penguiflow.node import Node
|
|
|
99
99
|
class QueryOut(BaseModel):
|
|
100
100
|
topic: str
|
|
101
101
|
|
|
102
|
-
async def triage(
|
|
102
|
+
async def triage(msg: QueryIn, ctx) -> QueryOut:
|
|
103
103
|
return QueryOut(topic="metrics")
|
|
104
104
|
|
|
105
105
|
triage_node = Node(triage, name="triage")
|
|
@@ -147,11 +147,13 @@ await flow.stop()
|
|
|
147
147
|
* Flows are orchestrators, mostly I/O-bound.
|
|
148
148
|
* Async tasks are cheap, predictable, and cancellable.
|
|
149
149
|
* Heavy CPU work should be offloaded inside a node (process pool, Ray, etc.), not in PenguiFlow itself.
|
|
150
|
+
* v1 intentionally stays in-process; scaling out or persisting state will arrive with future pluggable backends.
|
|
150
151
|
|
|
151
152
|
2. **Typed contracts.**
|
|
152
153
|
|
|
153
154
|
* In/out models per node are defined with Pydantic.
|
|
154
155
|
* Validated at runtime via cached `TypeAdapter`s.
|
|
156
|
+
* `flow.run(registry=...)` verifies every validating node is registered so misconfigurations fail fast.
|
|
155
157
|
|
|
156
158
|
3. **Reliability first.**
|
|
157
159
|
|
|
@@ -326,11 +328,42 @@ flow focused on high-level orchestration logic.
|
|
|
326
328
|
|
|
327
329
|
---
|
|
328
330
|
|
|
331
|
+
### Visualization
|
|
332
|
+
|
|
333
|
+
Need a quick view of the flow topology? Call `flow_to_mermaid(flow)` to render the graph
|
|
334
|
+
as a Mermaid diagram ready for Markdown or docs tools:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
from penguiflow import flow_to_mermaid
|
|
338
|
+
|
|
339
|
+
print(flow_to_mermaid(flow, direction="LR"))
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
329
344
|
## 🛡️ Reliability & Observability
|
|
330
345
|
|
|
331
346
|
* **NodePolicy**: set validation scope plus per-node timeout, retries, and backoff curves.
|
|
332
347
|
* **Structured logs**: enrich every node event with `{ts, trace_id, node_name, event, latency_ms, q_depth_in, attempt}`.
|
|
333
348
|
* **Middleware hooks**: subscribe observers (e.g., MLflow) to the structured event stream.
|
|
349
|
+
* See `examples/reliability_middleware/` for a concrete timeout + retry walkthrough.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## ⚠️ Current Constraints
|
|
354
|
+
|
|
355
|
+
- **In-process runtime**: there is no built-in distribution layer yet. Long-running CPU work should be delegated to your own pools or services.
|
|
356
|
+
- **Registry-driven typing**: nodes default to validation. Provide a `ModelRegistry` when calling `flow.run(...)` or set `validate="none"` explicitly for untyped hops.
|
|
357
|
+
- **Observability**: structured logs + middleware hooks are available, but integrations with third-party stacks (OTel, Prometheus) are DIY for now.
|
|
358
|
+
- **Roadmap**: v2 targets streaming, distributed backends, richer observability, and test harnesses. Contributions and proposals are welcome!
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 📊 Benchmarks
|
|
363
|
+
|
|
364
|
+
Lightweight benchmarks live under `benchmarks/`. Run them via `uv run python benchmarks/<name>.py`
|
|
365
|
+
to capture baselines for fan-out throughput, retry/timeout overhead, and controller
|
|
366
|
+
playbook latency. Copy them into product repos to watch for regressions over time.
|
|
334
367
|
|
|
335
368
|
---
|
|
336
369
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
penguiflow/__init__.py,sha256=fiQsp6-xYG2UjvuIhu71zvEiTeAjdfEjtoLRwZ8wROs,930
|
|
2
|
+
penguiflow/core.py,sha256=fO5GXF7Hih-gEcUPbyXVJilgUmwbvd72j337r5oOWME,20908
|
|
3
|
+
penguiflow/middlewares.py,sha256=LUlK4FrMScK3oaNSrAYNw3s4KcAZ716DTLAUqvsOkL8,319
|
|
4
|
+
penguiflow/node.py,sha256=0NOs3rU6t1tHNNwwJopqzM2ufGcp82JpzhckynWBRqs,3563
|
|
5
|
+
penguiflow/patterns.py,sha256=Ivuuy0on0OMsdYd5DRFZm1EgujXKPEaIIMH0ZWlJ1s0,4199
|
|
6
|
+
penguiflow/registry.py,sha256=4lrGDMFjM7c8pfZFc_YG0YHg-F80JyF4c-j0UbAf150,1419
|
|
7
|
+
penguiflow/types.py,sha256=QV2JvB_QnohfBATSaviPWm0HSR9B6dTc3UOwFIYyaqg,1154
|
|
8
|
+
penguiflow/viz.py,sha256=B9T2O5A6nHBLn7JuEeujqDC6ZcwP5s6M2rpsUrj5Ul0,2091
|
|
9
|
+
penguiflow-1.0.3.dist-info/licenses/LICENSE,sha256=JSvodvLXxSct_kI9IBsZOBpVKoESQTB_AGbkClwZ7HI,1065
|
|
10
|
+
penguiflow-1.0.3.dist-info/METADATA,sha256=GISwzSiycpzXy0kmoP3mWeMAeutgNITeTKVGz2YRb1A,13658
|
|
11
|
+
penguiflow-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
penguiflow-1.0.3.dist-info/top_level.txt,sha256=F-5jgzPP4Mo_ErgtzGDFJdRT4CIfFjFBnxxcn-RpWBU,11
|
|
13
|
+
penguiflow-1.0.3.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
penguiflow/__init__.py,sha256=wT0yYO6DsV4x1uLwSXbhcXPP3g9YI9x3mcUWxhX5kis,874
|
|
2
|
-
penguiflow/core.py,sha256=ljXFsmVtWj2UTCxWNok-ZRGrPho0wJN3OWP5IgnfhDU,20143
|
|
3
|
-
penguiflow/middlewares.py,sha256=LUlK4FrMScK3oaNSrAYNw3s4KcAZ716DTLAUqvsOkL8,319
|
|
4
|
-
penguiflow/node.py,sha256=0NOs3rU6t1tHNNwwJopqzM2ufGcp82JpzhckynWBRqs,3563
|
|
5
|
-
penguiflow/patterns.py,sha256=Ivuuy0on0OMsdYd5DRFZm1EgujXKPEaIIMH0ZWlJ1s0,4199
|
|
6
|
-
penguiflow/registry.py,sha256=4lrGDMFjM7c8pfZFc_YG0YHg-F80JyF4c-j0UbAf150,1419
|
|
7
|
-
penguiflow/types.py,sha256=QV2JvB_QnohfBATSaviPWm0HSR9B6dTc3UOwFIYyaqg,1154
|
|
8
|
-
penguiflow/viz.py,sha256=_puIEJevq2DrfNuydm3DG1V1o4PgICxd5pA-Es_IyWY,112
|
|
9
|
-
penguiflow-1.0.0.dist-info/licenses/LICENSE,sha256=JSvodvLXxSct_kI9IBsZOBpVKoESQTB_AGbkClwZ7HI,1065
|
|
10
|
-
penguiflow-1.0.0.dist-info/METADATA,sha256=MhOA10s6CxVM1UbbKPMEkA9FKS-DJMLU7MEYG748g1U,12143
|
|
11
|
-
penguiflow-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
penguiflow-1.0.0.dist-info/top_level.txt,sha256=F-5jgzPP4Mo_ErgtzGDFJdRT4CIfFjFBnxxcn-RpWBU,11
|
|
13
|
-
penguiflow-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|