penguiflow 1.0.3__py3-none-any.whl → 2.1.0__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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from . import testkit
6
+ from .bus import BusEnvelope, MessageBus
5
7
  from .core import (
6
8
  DEFAULT_QUEUE_MAXSIZE,
7
9
  Context,
@@ -10,12 +12,29 @@ from .core import (
10
12
  call_playbook,
11
13
  create,
12
14
  )
15
+ from .errors import FlowError, FlowErrorCode
16
+ from .metrics import FlowEvent
13
17
  from .middlewares import Middleware
14
18
  from .node import Node, NodePolicy
15
19
  from .patterns import join_k, map_concurrent, predicate_router, union_router
20
+ from .policies import DictRoutingPolicy, RoutingPolicy, RoutingRequest
16
21
  from .registry import ModelRegistry
17
- from .types import WM, FinalAnswer, Headers, Message, PlanStep, Thought
18
- from .viz import flow_to_mermaid
22
+ from .remote import (
23
+ RemoteCallRequest,
24
+ RemoteCallResult,
25
+ RemoteNode,
26
+ RemoteStreamEvent,
27
+ RemoteTransport,
28
+ )
29
+ from .state import RemoteBinding, StateStore, StoredEvent
30
+ from .streaming import (
31
+ chunk_to_ws_json,
32
+ emit_stream_events,
33
+ format_sse_event,
34
+ stream_flow,
35
+ )
36
+ from .types import WM, FinalAnswer, Headers, Message, PlanStep, StreamChunk, Thought
37
+ from .viz import flow_to_dot, flow_to_mermaid
19
38
 
20
39
  __all__ = [
21
40
  "__version__",
@@ -27,9 +46,15 @@ __all__ = [
27
46
  "NodePolicy",
28
47
  "ModelRegistry",
29
48
  "Middleware",
49
+ "FlowEvent",
50
+ "FlowError",
51
+ "FlowErrorCode",
52
+ "MessageBus",
53
+ "BusEnvelope",
30
54
  "call_playbook",
31
55
  "Headers",
32
56
  "Message",
57
+ "StreamChunk",
33
58
  "PlanStep",
34
59
  "Thought",
35
60
  "WM",
@@ -38,8 +63,25 @@ __all__ = [
38
63
  "join_k",
39
64
  "predicate_router",
40
65
  "union_router",
66
+ "DictRoutingPolicy",
67
+ "RoutingPolicy",
68
+ "RoutingRequest",
69
+ "format_sse_event",
70
+ "chunk_to_ws_json",
71
+ "stream_flow",
72
+ "emit_stream_events",
41
73
  "flow_to_mermaid",
74
+ "flow_to_dot",
42
75
  "create",
76
+ "testkit",
77
+ "StateStore",
78
+ "StoredEvent",
79
+ "RemoteBinding",
80
+ "RemoteTransport",
81
+ "RemoteCallRequest",
82
+ "RemoteCallResult",
83
+ "RemoteStreamEvent",
84
+ "RemoteNode",
43
85
  ]
44
86
 
45
- __version__ = "1.0.3"
87
+ __version__ = "2.1.0"
penguiflow/admin.py ADDED
@@ -0,0 +1,174 @@
1
+ """Developer CLI helpers for inspecting PenguiFlow trace history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import importlib
8
+ import json
9
+ import sys
10
+ from collections.abc import Callable, Sequence
11
+ from typing import Any
12
+
13
+ from .state import StateStore, StoredEvent
14
+
15
+ __all__ = ["load_state_store", "render_events", "main"]
16
+
17
+
18
+ class _Args(argparse.Namespace):
19
+ handler: Callable[[_Args], Any]
20
+ state_store: str
21
+ trace_id: str
22
+ tail: int | None
23
+ delay: float
24
+
25
+
26
+ def _resolve_factory(spec: str) -> Callable[[], Any]:
27
+ module_name, _, attr = spec.partition(":")
28
+ if not module_name or not attr:
29
+ raise ValueError(
30
+ "state store spec must be in the form 'package.module:callable'"
31
+ )
32
+ module = importlib.import_module(module_name)
33
+ try:
34
+ factory = getattr(module, attr)
35
+ except AttributeError as exc: # pragma: no cover - defensive guard
36
+ raise ValueError(f"{spec!r} does not resolve to a callable") from exc
37
+ if not callable(factory):
38
+ raise TypeError(f"{spec!r} resolved to {type(factory)!r}, not a callable")
39
+ return factory
40
+
41
+
42
+ async def load_state_store(spec: str) -> StateStore:
43
+ """Instantiate a :class:`StateStore` from ``module:callable`` spec."""
44
+
45
+ factory = _resolve_factory(spec)
46
+ instance = factory()
47
+ if asyncio.iscoroutine(instance):
48
+ instance = await instance
49
+ required = ("save_event", "load_history", "save_remote_binding")
50
+ if not all(hasattr(instance, attr) for attr in required): # pragma: no cover
51
+ raise TypeError(
52
+ "StateStore factories must implement "
53
+ "save_event/load_history/save_remote_binding"
54
+ )
55
+ return instance
56
+
57
+
58
+ def _trim_events(events: Sequence[StoredEvent], tail: int | None) -> list[StoredEvent]:
59
+ items = list(events)
60
+ if tail is None:
61
+ return items
62
+ if tail <= 0:
63
+ return []
64
+ return items[-tail:]
65
+
66
+
67
+ def render_events(
68
+ events: Sequence[StoredEvent], *, tail: int | None = None
69
+ ) -> list[str]:
70
+ """Return JSON line representations of ``events`` (optionally tail-truncated)."""
71
+
72
+ trimmed = _trim_events(events, tail)
73
+ lines: list[str] = []
74
+ for event in trimmed:
75
+ payload = dict(event.payload)
76
+ payload.setdefault("event", event.kind)
77
+ payload.setdefault("trace_id", event.trace_id)
78
+ payload.setdefault("node_name", event.node_name)
79
+ payload.setdefault("node_id", event.node_id)
80
+ payload.setdefault("ts", event.ts)
81
+ lines.append(json.dumps(payload, sort_keys=True, default=str))
82
+ return lines
83
+
84
+
85
+ async def _cmd_history(args: _Args) -> None:
86
+ store = await load_state_store(args.state_store)
87
+ events = await store.load_history(args.trace_id)
88
+ for line in render_events(events, tail=args.tail):
89
+ print(line)
90
+
91
+
92
+ async def _cmd_replay(args: _Args) -> None:
93
+ store = await load_state_store(args.state_store)
94
+ events = _trim_events(await store.load_history(args.trace_id), args.tail)
95
+ total = len(events)
96
+ if not total:
97
+ print(f"# trace {args.trace_id} has no stored events")
98
+ return
99
+ print(f"# replay trace={args.trace_id} events={total}")
100
+ for event in events:
101
+ payload = render_events([event])[0]
102
+ print(payload)
103
+ if args.delay > 0:
104
+ await asyncio.sleep(args.delay)
105
+
106
+
107
+ def _build_parser() -> argparse.ArgumentParser:
108
+ parser = argparse.ArgumentParser(
109
+ prog="penguiflow-admin",
110
+ description=(
111
+ "Inspect PenguiFlow trace history via configured StateStore "
112
+ "adapters."
113
+ ),
114
+ )
115
+ common = argparse.ArgumentParser(add_help=False)
116
+ common.add_argument(
117
+ "--state-store",
118
+ required=True,
119
+ help="Import path to a factory returning a StateStore (module:callable)",
120
+ )
121
+ common.add_argument(
122
+ "--tail",
123
+ type=int,
124
+ default=None,
125
+ help="Only show the last N events from the trace history.",
126
+ )
127
+ subparsers = parser.add_subparsers(dest="command", required=True)
128
+
129
+ history = subparsers.add_parser(
130
+ "history",
131
+ parents=[common],
132
+ help="Print stored events for a trace as JSON lines.",
133
+ )
134
+ history.add_argument("trace_id", help="Trace identifier to inspect")
135
+ history.set_defaults(handler=_cmd_history)
136
+
137
+ replay = subparsers.add_parser(
138
+ "replay",
139
+ parents=[common],
140
+ help="Replay events with optional delay to mimic runtime emission.",
141
+ )
142
+ replay.add_argument("trace_id", help="Trace identifier to replay")
143
+ replay.add_argument(
144
+ "--delay",
145
+ type=float,
146
+ default=0.0,
147
+ help="Sleep duration (seconds) between events when replaying.",
148
+ )
149
+ replay.set_defaults(handler=_cmd_replay)
150
+
151
+ return parser
152
+
153
+
154
+ def main(argv: Sequence[str] | None = None) -> int:
155
+ """Entry point for the ``penguiflow-admin`` CLI."""
156
+
157
+ parser = _build_parser()
158
+ args = parser.parse_args(argv)
159
+ handler = getattr(args, "handler", None)
160
+ if handler is None: # pragma: no cover - argparse guard
161
+ parser.print_help()
162
+ return 1
163
+
164
+ try:
165
+ asyncio.run(handler(args))
166
+ except Exception as exc: # pragma: no cover - runtime guard
167
+ print(f"error: {exc}", file=sys.stderr)
168
+ return 1
169
+ return 0
170
+
171
+
172
+ if __name__ == "__main__": # pragma: no cover - manual invocation
173
+ raise SystemExit(main())
174
+
penguiflow/bus.py ADDED
@@ -0,0 +1,30 @@
1
+ """Message bus protocol for distributed PenguiFlow edges."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any, Protocol
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class BusEnvelope:
12
+ """Structured payload published to a :class:`MessageBus`."""
13
+
14
+ edge: str
15
+ source: str | None
16
+ target: str | None
17
+ trace_id: str | None
18
+ payload: Any
19
+ headers: Mapping[str, Any] | None
20
+ meta: Mapping[str, Any] | None
21
+
22
+
23
+ class MessageBus(Protocol):
24
+ """Protocol for pluggable message bus adapters."""
25
+
26
+ async def publish(self, envelope: BusEnvelope) -> None:
27
+ """Publish an envelope for downstream workers."""
28
+
29
+
30
+ __all__ = ["BusEnvelope", "MessageBus"]