penguiflow 1.0.0__tar.gz → 1.0.3__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.

Potentially problematic release.


This version of penguiflow might be problematic. Click here for more details.

Files changed (25) hide show
  1. {penguiflow-1.0.0 → penguiflow-1.0.3}/PKG-INFO +35 -2
  2. {penguiflow-1.0.0 → penguiflow-1.0.3}/README.md +34 -1
  3. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/__init__.py +3 -1
  4. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/core.py +22 -0
  5. penguiflow-1.0.3/penguiflow/viz.py +76 -0
  6. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow.egg-info/PKG-INFO +35 -2
  7. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow.egg-info/SOURCES.txt +2 -1
  8. {penguiflow-1.0.0 → penguiflow-1.0.3}/pyproject.toml +1 -1
  9. {penguiflow-1.0.0 → penguiflow-1.0.3}/tests/test_core.py +43 -0
  10. penguiflow-1.0.3/tests/test_viz.py +29 -0
  11. penguiflow-1.0.0/penguiflow/viz.py +0 -5
  12. {penguiflow-1.0.0 → penguiflow-1.0.3}/LICENSE +0 -0
  13. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/middlewares.py +0 -0
  14. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/node.py +0 -0
  15. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/patterns.py +0 -0
  16. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/registry.py +0 -0
  17. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow/types.py +0 -0
  18. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow.egg-info/dependency_links.txt +0 -0
  19. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow.egg-info/requires.txt +0 -0
  20. {penguiflow-1.0.0 → penguiflow-1.0.3}/penguiflow.egg-info/top_level.txt +0 -0
  21. {penguiflow-1.0.0 → penguiflow-1.0.3}/setup.cfg +0 -0
  22. {penguiflow-1.0.0 → penguiflow-1.0.3}/tests/test_controller.py +0 -0
  23. {penguiflow-1.0.0 → penguiflow-1.0.3}/tests/test_patterns.py +0 -0
  24. {penguiflow-1.0.0 → penguiflow-1.0.3}/tests/test_registry.py +0 -0
  25. {penguiflow-1.0.0 → penguiflow-1.0.3}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: penguiflow
3
- Version: 1.0.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(m: QueryIn) -> QueryOut:
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
 
@@ -60,7 +60,7 @@ from penguiflow.node import Node
60
60
  class QueryOut(BaseModel):
61
61
  topic: str
62
62
 
63
- async def triage(m: QueryIn) -> QueryOut:
63
+ async def triage(msg: QueryIn, ctx) -> QueryOut:
64
64
  return QueryOut(topic="metrics")
65
65
 
66
66
  triage_node = Node(triage, name="triage")
@@ -108,11 +108,13 @@ await flow.stop()
108
108
  * Flows are orchestrators, mostly I/O-bound.
109
109
  * Async tasks are cheap, predictable, and cancellable.
110
110
  * Heavy CPU work should be offloaded inside a node (process pool, Ray, etc.), not in PenguiFlow itself.
111
+ * v1 intentionally stays in-process; scaling out or persisting state will arrive with future pluggable backends.
111
112
 
112
113
  2. **Typed contracts.**
113
114
 
114
115
  * In/out models per node are defined with Pydantic.
115
116
  * Validated at runtime via cached `TypeAdapter`s.
117
+ * `flow.run(registry=...)` verifies every validating node is registered so misconfigurations fail fast.
116
118
 
117
119
  3. **Reliability first.**
118
120
 
@@ -287,11 +289,42 @@ flow focused on high-level orchestration logic.
287
289
 
288
290
  ---
289
291
 
292
+ ### Visualization
293
+
294
+ Need a quick view of the flow topology? Call `flow_to_mermaid(flow)` to render the graph
295
+ as a Mermaid diagram ready for Markdown or docs tools:
296
+
297
+ ```python
298
+ from penguiflow import flow_to_mermaid
299
+
300
+ print(flow_to_mermaid(flow, direction="LR"))
301
+ ```
302
+
303
+ ---
304
+
290
305
  ## 🛡️ Reliability & Observability
291
306
 
292
307
  * **NodePolicy**: set validation scope plus per-node timeout, retries, and backoff curves.
293
308
  * **Structured logs**: enrich every node event with `{ts, trace_id, node_name, event, latency_ms, q_depth_in, attempt}`.
294
309
  * **Middleware hooks**: subscribe observers (e.g., MLflow) to the structured event stream.
310
+ * See `examples/reliability_middleware/` for a concrete timeout + retry walkthrough.
311
+
312
+ ---
313
+
314
+ ## ⚠️ Current Constraints
315
+
316
+ - **In-process runtime**: there is no built-in distribution layer yet. Long-running CPU work should be delegated to your own pools or services.
317
+ - **Registry-driven typing**: nodes default to validation. Provide a `ModelRegistry` when calling `flow.run(...)` or set `validate="none"` explicitly for untyped hops.
318
+ - **Observability**: structured logs + middleware hooks are available, but integrations with third-party stacks (OTel, Prometheus) are DIY for now.
319
+ - **Roadmap**: v2 targets streaming, distributed backends, richer observability, and test harnesses. Contributions and proposals are welcome!
320
+
321
+ ---
322
+
323
+ ## 📊 Benchmarks
324
+
325
+ Lightweight benchmarks live under `benchmarks/`. Run them via `uv run python benchmarks/<name>.py`
326
+ to capture baselines for fan-out throughput, retry/timeout overhead, and controller
327
+ playbook latency. Copy them into product repos to watch for regressions over time.
295
328
 
296
329
  ---
297
330
 
@@ -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.0"
45
+ __version__ = "1.0.3"
@@ -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
 
@@ -0,0 +1,76 @@
1
+ """Visualization helpers for PenguiFlow graphs."""
2
+
3
+ from __future__ import annotations
4
+
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.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(m: QueryIn) -> QueryOut:
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
 
@@ -18,4 +18,5 @@ tests/test_controller.py
18
18
  tests/test_core.py
19
19
  tests/test_patterns.py
20
20
  tests/test_registry.py
21
- tests/test_types.py
21
+ tests/test_types.py
22
+ tests/test_viz.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "penguiflow"
7
- version = "1.0.0"
7
+ version = "1.0.3"
8
8
  description = "Async agent orchestration primitives."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -6,9 +6,11 @@ import asyncio
6
6
  import logging
7
7
 
8
8
  import pytest
9
+ from pydantic import BaseModel
9
10
 
10
11
  from penguiflow.core import CycleError, PenguiFlow, create
11
12
  from penguiflow.node import Node, NodePolicy
13
+ from penguiflow.registry import ModelRegistry
12
14
 
13
15
 
14
16
  @pytest.mark.asyncio
@@ -249,3 +251,44 @@ async def test_middlewares_receive_events() -> None:
249
251
 
250
252
  events_names = [name for name, _ in events]
251
253
  assert "node_success" in events_names
254
+
255
+
256
+ @pytest.mark.asyncio
257
+ async def test_run_with_registry_requires_registered_nodes() -> None:
258
+ async def handler(msg: str, ctx) -> str:
259
+ return msg
260
+
261
+ node = Node(handler, name="handler")
262
+ flow = create(node.to())
263
+ registry = ModelRegistry()
264
+
265
+ with pytest.raises(RuntimeError) as exc:
266
+ flow.run(registry=registry)
267
+
268
+ assert "handler" in str(exc.value)
269
+
270
+
271
+ @pytest.mark.asyncio
272
+ async def test_run_with_registry_accepts_registered_nodes() -> None:
273
+ class EchoModel(BaseModel):
274
+ text: str
275
+
276
+ async def handler(msg: EchoModel, ctx) -> EchoModel:
277
+ return msg
278
+
279
+ node = Node(handler, name="echo")
280
+ flow = create(node.to())
281
+
282
+ registry = ModelRegistry()
283
+ registry.register("echo", EchoModel, EchoModel)
284
+
285
+ flow.run(registry=registry)
286
+
287
+ message = EchoModel(text="hi")
288
+ await flow.emit(message)
289
+ result = await flow.fetch()
290
+
291
+ assert isinstance(result, EchoModel)
292
+ assert result.text == "hi"
293
+
294
+ await flow.stop()
@@ -0,0 +1,29 @@
1
+ """Tests for visualization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from penguiflow import Node, NodePolicy, create
8
+ from penguiflow.viz import flow_to_mermaid
9
+
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_flow_to_mermaid_renders_edges() -> None:
13
+ async def fan(msg: str, ctx) -> str:
14
+ return msg
15
+
16
+ async def sink(msg: str, ctx) -> str:
17
+ return msg
18
+
19
+ fan_node = Node(fan, name="fan", policy=NodePolicy(validate="none"))
20
+ sink_node = Node(sink, name="sink", policy=NodePolicy(validate="none"))
21
+
22
+ flow = create(fan_node.to(sink_node))
23
+
24
+ mermaid = flow_to_mermaid(flow)
25
+
26
+ assert mermaid.startswith("graph TD")
27
+ assert "fan" in mermaid
28
+ assert "sink" in mermaid
29
+ assert "-->" in mermaid
@@ -1,5 +0,0 @@
1
- """Visualization helpers for PenguiFlow graphs."""
2
-
3
- from __future__ import annotations
4
-
5
- __all__: list[str] = []
File without changes
File without changes