flowforge-sdk 0.3.0__tar.gz → 0.3.2__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 (26) hide show
  1. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/PKG-INFO +40 -1
  2. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/README.md +39 -0
  3. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/pyproject.toml +1 -1
  4. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/__init__.py +4 -0
  5. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/client.py +81 -1
  6. flowforge_sdk-0.3.2/src/flowforge/streaming.py +66 -0
  7. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/.gitignore +0 -0
  8. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/agent.py +0 -0
  9. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/agent_def.py +0 -0
  10. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/ai/__init__.py +0 -0
  11. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/ai/providers.py +0 -0
  12. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/config.py +0 -0
  13. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/context.py +0 -0
  14. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/decorators.py +0 -0
  15. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/dev/__init__.py +0 -0
  16. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/dev/server.py +0 -0
  17. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/exceptions.py +0 -0
  18. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/execution.py +0 -0
  19. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/integrations/__init__.py +0 -0
  20. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/integrations/fastapi.py +0 -0
  21. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/network.py +0 -0
  22. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/router.py +0 -0
  23. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/steps.py +0 -0
  24. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/tools.py +0 -0
  25. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/triggers.py +0 -0
  26. {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowforge-sdk
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Python SDK for FlowForge - AI workflow orchestration
5
5
  Project-URL: Homepage, https://github.com/flowforge/flowforge
6
6
  Project-URL: Documentation, https://flowforge.dev/docs
@@ -144,3 +144,42 @@ result = await flowforge.cancel_run("761c0321-...")
144
144
  `retry_run` is different from replaying: it preserves the memoized results of
145
145
  all completed steps so execution resumes from where it failed rather than
146
146
  starting over from scratch.
147
+
148
+ ## Streaming Run Events (SSE)
149
+
150
+ Stream real-time events from a running workflow via Server-Sent Events:
151
+
152
+ ```python
153
+ from flowforge import FlowForge, RunEvent
154
+
155
+ flowforge = FlowForge(app_id="my-app", api_key="ff_live_...")
156
+
157
+ # Async iterator
158
+ async for event in flowforge.stream_run("run-uuid"):
159
+ print(f"[{event.event_type.value}] {event.data}")
160
+
161
+ # With callback
162
+ async for event in flowforge.stream_run("run-uuid", on_event=lambda e: print(e)):
163
+ pass
164
+ ```
165
+
166
+ The stream automatically closes when the run completes or fails. Available event types:
167
+
168
+ - `step_started`, `step_completed`, `step_failed`
169
+ - `thinking`, `thinking_chunk`
170
+ - `tool_call_started`, `tool_call_completed`
171
+ - `approval_required`, `approval_resolved`
172
+ - `run_started`, `run_paused`, `run_resumed`, `run_completed`, `run_failed`
173
+
174
+ Options:
175
+
176
+ ```python
177
+ async for event in flowforge.stream_run(
178
+ "run-uuid",
179
+ include_history=True, # Include past events on connect (default: True)
180
+ timeout=300.0, # Server-side stream timeout in seconds (default: 300)
181
+ on_event=my_callback, # Optional callback for each event
182
+ ):
183
+ if event.is_terminal:
184
+ print("Run finished:", event.data)
185
+ ```
@@ -92,3 +92,42 @@ result = await flowforge.cancel_run("761c0321-...")
92
92
  `retry_run` is different from replaying: it preserves the memoized results of
93
93
  all completed steps so execution resumes from where it failed rather than
94
94
  starting over from scratch.
95
+
96
+ ## Streaming Run Events (SSE)
97
+
98
+ Stream real-time events from a running workflow via Server-Sent Events:
99
+
100
+ ```python
101
+ from flowforge import FlowForge, RunEvent
102
+
103
+ flowforge = FlowForge(app_id="my-app", api_key="ff_live_...")
104
+
105
+ # Async iterator
106
+ async for event in flowforge.stream_run("run-uuid"):
107
+ print(f"[{event.event_type.value}] {event.data}")
108
+
109
+ # With callback
110
+ async for event in flowforge.stream_run("run-uuid", on_event=lambda e: print(e)):
111
+ pass
112
+ ```
113
+
114
+ The stream automatically closes when the run completes or fails. Available event types:
115
+
116
+ - `step_started`, `step_completed`, `step_failed`
117
+ - `thinking`, `thinking_chunk`
118
+ - `tool_call_started`, `tool_call_completed`
119
+ - `approval_required`, `approval_resolved`
120
+ - `run_started`, `run_paused`, `run_resumed`, `run_completed`, `run_failed`
121
+
122
+ Options:
123
+
124
+ ```python
125
+ async for event in flowforge.stream_run(
126
+ "run-uuid",
127
+ include_history=True, # Include past events on connect (default: True)
128
+ timeout=300.0, # Server-side stream timeout in seconds (default: 300)
129
+ on_event=my_callback, # Optional callback for each event
130
+ ):
131
+ if event.is_terminal:
132
+ print("Run finished:", event.data)
133
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowforge-sdk"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Python SDK for FlowForge - AI workflow orchestration"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,6 +26,7 @@ from flowforge.exceptions import (
26
26
  )
27
27
  from flowforge.network import Network, NetworkResult, NetworkState, RouterContext, network
28
28
  from flowforge.steps import step
29
+ from flowforge.streaming import RunEvent, RunEventType
29
30
  from flowforge.tools import Tool, tool
30
31
  from flowforge.triggers import Trigger, trigger
31
32
 
@@ -68,6 +69,9 @@ __all__ = [
68
69
  "concurrency",
69
70
  "rate_limit",
70
71
  "throttle",
72
+ # Streaming
73
+ "RunEvent",
74
+ "RunEventType",
71
75
  # Exceptions
72
76
  "FlowForgeError",
73
77
  "StepError",
@@ -5,7 +5,7 @@ import hmac
5
5
  import json
6
6
  import os
7
7
  import uuid
8
- from collections.abc import Awaitable, Callable
8
+ from collections.abc import AsyncIterator, Awaitable, Callable
9
9
  from datetime import datetime
10
10
  from typing import Any, TypeVar
11
11
 
@@ -33,6 +33,7 @@ from flowforge.context import Context, Event
33
33
  from flowforge.decorators import FlowForgeFunction
34
34
  from flowforge.decorators import function as make_function
35
35
  from flowforge.execution import ExecutionEngine, FunctionDefinition
36
+ from flowforge.streaming import RunEvent, RunEventType
36
37
  from flowforge.triggers import TriggerBuilder
37
38
 
38
39
  T = TypeVar("T")
@@ -448,6 +449,85 @@ class FlowForge:
448
449
  response.raise_for_status()
449
450
  return response.json()
450
451
 
452
+ async def stream_run(
453
+ self,
454
+ run_id: str,
455
+ *,
456
+ include_history: bool = True,
457
+ timeout: float = 300.0,
458
+ on_event: Callable[[RunEvent], None] | None = None,
459
+ ) -> AsyncIterator[RunEvent]:
460
+ """
461
+ Stream real-time SSE events for a run.
462
+
463
+ Args:
464
+ run_id: The run UUID to stream.
465
+ include_history: Whether to include past events on connect.
466
+ timeout: Server-side stream timeout in seconds.
467
+ on_event: Optional callback invoked for each event.
468
+
469
+ Yields:
470
+ RunEvent for each SSE event. Stops on terminal events
471
+ (run_completed, run_failed).
472
+
473
+ Example:
474
+ async for event in flowforge.stream_run("run-uuid"):
475
+ print(f"[{event.event_type.value}] {event.data}")
476
+ """
477
+ params = {
478
+ "include_history": str(include_history).lower(),
479
+ "timeout": str(int(timeout)),
480
+ }
481
+ url = f"{self.api_url}/api/v1/runs/{run_id}/stream"
482
+
483
+ headers: dict[str, str] = {"Accept": "text/event-stream"}
484
+ if self.api_key:
485
+ headers["X-FlowForge-API-Key"] = self.api_key
486
+
487
+ # Use a dedicated client with extended read timeout for streaming
488
+ async with httpx.AsyncClient(
489
+ timeout=httpx.Timeout(connect=10.0, read=timeout + 30.0, write=10.0, pool=10.0),
490
+ ) as client:
491
+ async with client.stream(
492
+ "GET", url, params=params, headers=headers,
493
+ ) as response:
494
+ response.raise_for_status()
495
+
496
+ event_type: str | None = None
497
+ data_buf: list[str] = []
498
+
499
+ async for line in response.aiter_lines():
500
+ # Skip keepalive comments
501
+ if line.startswith(":"):
502
+ continue
503
+
504
+ # Blank line = dispatch event
505
+ if not line:
506
+ if event_type and data_buf:
507
+ raw_data = "\n".join(data_buf)
508
+ try:
509
+ evt = RunEvent.from_raw(event_type, raw_data, run_id)
510
+ except (ValueError, json.JSONDecodeError):
511
+ event_type = None
512
+ data_buf = []
513
+ continue
514
+
515
+ if on_event:
516
+ on_event(evt)
517
+ yield evt
518
+
519
+ if evt.is_terminal:
520
+ return
521
+
522
+ event_type = None
523
+ data_buf = []
524
+ continue
525
+
526
+ if line.startswith("event:"):
527
+ event_type = line[len("event:"):].strip()
528
+ elif line.startswith("data:"):
529
+ data_buf.append(line[len("data:"):].strip())
530
+
451
531
  async def close(self) -> None:
452
532
  """Close the HTTP client."""
453
533
  if self._http_client:
@@ -0,0 +1,66 @@
1
+ """SSE streaming types for FlowForge run events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+
12
+ class RunEventType(str, Enum):
13
+ """Event types emitted by the SSE run stream."""
14
+
15
+ STEP_STARTED = "step_started"
16
+ STEP_COMPLETED = "step_completed"
17
+ STEP_FAILED = "step_failed"
18
+ THINKING = "thinking"
19
+ THINKING_CHUNK = "thinking_chunk"
20
+ TOOL_CALL_STARTED = "tool_call_started"
21
+ TOOL_CALL_COMPLETED = "tool_call_completed"
22
+ APPROVAL_REQUIRED = "approval_required"
23
+ APPROVAL_RESOLVED = "approval_resolved"
24
+ RUN_STARTED = "run_started"
25
+ RUN_PAUSED = "run_paused"
26
+ RUN_RESUMED = "run_resumed"
27
+ RUN_COMPLETED = "run_completed"
28
+ RUN_FAILED = "run_failed"
29
+
30
+
31
+ _TERMINAL_EVENTS = {RunEventType.RUN_COMPLETED, RunEventType.RUN_FAILED}
32
+
33
+
34
+ @dataclass
35
+ class RunEvent:
36
+ """A single event from an SSE run stream."""
37
+
38
+ event_type: RunEventType
39
+ data: dict[str, Any]
40
+ run_id: str
41
+ timestamp: datetime
42
+
43
+ @classmethod
44
+ def from_raw(cls, event_type_str: str, json_data: str, run_id: str) -> RunEvent:
45
+ """Parse a raw SSE event into a RunEvent."""
46
+ parsed = json.loads(json_data) if isinstance(json_data, str) else json_data
47
+ ts = parsed.get("timestamp") or parsed.get("ts")
48
+ if isinstance(ts, str):
49
+ try:
50
+ timestamp = datetime.fromisoformat(ts.replace("Z", "+00:00"))
51
+ except ValueError:
52
+ timestamp = datetime.utcnow()
53
+ else:
54
+ timestamp = datetime.utcnow()
55
+
56
+ return cls(
57
+ event_type=RunEventType(event_type_str),
58
+ data=parsed,
59
+ run_id=run_id,
60
+ timestamp=timestamp,
61
+ )
62
+
63
+ @property
64
+ def is_terminal(self) -> bool:
65
+ """True if this event signals the run has ended."""
66
+ return self.event_type in _TERMINAL_EVENTS
File without changes