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.
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/PKG-INFO +40 -1
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/README.md +39 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/pyproject.toml +1 -1
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/__init__.py +4 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/client.py +81 -1
- flowforge_sdk-0.3.2/src/flowforge/streaming.py +66 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/.gitignore +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/agent.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/agent_def.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/ai/__init__.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/ai/providers.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/config.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/context.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/decorators.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/dev/__init__.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/dev/server.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/exceptions.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/execution.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/integrations/__init__.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/integrations/fastapi.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/network.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/router.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/steps.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/tools.py +0 -0
- {flowforge_sdk-0.3.0 → flowforge_sdk-0.3.2}/src/flowforge/triggers.py +0 -0
- {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.
|
|
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
|
+
```
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|