reactifact 0.6.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.
- reactifact/__init__.py +96 -0
- reactifact/__main__.py +10 -0
- reactifact/_extras.py +36 -0
- reactifact/agents.py +173 -0
- reactifact/artifacts.py +130 -0
- reactifact/branching.py +255 -0
- reactifact/budget.py +41 -0
- reactifact/chat.py +373 -0
- reactifact/checkpoints.py +329 -0
- reactifact/cli/__init__.py +73 -0
- reactifact/cli/branch.py +77 -0
- reactifact/cli/common.py +67 -0
- reactifact/cli/context.py +53 -0
- reactifact/cli/graph.py +21 -0
- reactifact/cli/replay.py +69 -0
- reactifact/cli/scenario.py +94 -0
- reactifact/cli/trace.py +45 -0
- reactifact/commit.py +97 -0
- reactifact/commit_log.py +235 -0
- reactifact/consume.py +96 -0
- reactifact/context.py +599 -0
- reactifact/effects.py +232 -0
- reactifact/eval.py +319 -0
- reactifact/events.py +34 -0
- reactifact/interrupt.py +22 -0
- reactifact/llm_agent.py +172 -0
- reactifact/operations.py +192 -0
- reactifact/patches.py +112 -0
- reactifact/produce.py +226 -0
- reactifact/prompts.py +111 -0
- reactifact/providers/__init__.py +153 -0
- reactifact/providers/_retry.py +61 -0
- reactifact/providers/anthropic.py +182 -0
- reactifact/providers/azure.py +31 -0
- reactifact/providers/cerebras.py +11 -0
- reactifact/providers/chat.py +417 -0
- reactifact/providers/contracts.py +105 -0
- reactifact/providers/deepseek.py +11 -0
- reactifact/providers/fake.py +40 -0
- reactifact/providers/fireworks.py +17 -0
- reactifact/providers/gemini.py +284 -0
- reactifact/providers/github_models.py +13 -0
- reactifact/providers/groq.py +18 -0
- reactifact/providers/image.py +157 -0
- reactifact/providers/mistral.py +17 -0
- reactifact/providers/nvidia.py +18 -0
- reactifact/providers/ollama.py +18 -0
- reactifact/providers/openai.py +44 -0
- reactifact/providers/openrouter.py +70 -0
- reactifact/providers/perplexity.py +11 -0
- reactifact/providers/qwen.py +17 -0
- reactifact/providers/speech.py +347 -0
- reactifact/providers/together.py +17 -0
- reactifact/providers/video.py +407 -0
- reactifact/providers/xai.py +11 -0
- reactifact/providers/zai.py +11 -0
- reactifact/py.typed +0 -0
- reactifact/recipes/__init__.py +63 -0
- reactifact/recipes/inputs.py +34 -0
- reactifact/recipes/memory.py +166 -0
- reactifact/recipes/resolve.py +51 -0
- reactifact/recipes/rollback.py +87 -0
- reactifact/recipes/search.py +81 -0
- reactifact/recipes/skills.py +108 -0
- reactifact/recipes/status.py +79 -0
- reactifact/recipes/text.py +202 -0
- reactifact/relations.py +104 -0
- reactifact/replay.py +187 -0
- reactifact/resources.py +45 -0
- reactifact/runtime.py +498 -0
- reactifact/scheduler.py +188 -0
- reactifact/session.py +75 -0
- reactifact/sources.py +498 -0
- reactifact/streaming.py +58 -0
- reactifact/structured.py +245 -0
- reactifact/testing/__init__.py +48 -0
- reactifact/testing/assertions.py +326 -0
- reactifact/testing/exceptions.py +27 -0
- reactifact/testing/fault.py +164 -0
- reactifact/testing/lab.py +350 -0
- reactifact/testing/mock.py +166 -0
- reactifact/testing/record.py +50 -0
- reactifact/testing/registry.py +87 -0
- reactifact/tool_use.py +528 -0
- reactifact/tools.py +111 -0
- reactifact/tracing/__init__.py +29 -0
- reactifact/tracing/langfuse.py +125 -0
- reactifact/tracing/models.py +93 -0
- reactifact/tracing/postgres.py +220 -0
- reactifact/tracing/store.py +254 -0
- reactifact/tracing/templates/ui.html +196 -0
- reactifact/tracing/templates/ui_run.html +264 -0
- reactifact/tracing/tracer.py +370 -0
- reactifact/tracing/web.py +117 -0
- reactifact/triggers.py +41 -0
- reactifact/viz.py +248 -0
- reactifact/web.py +117 -0
- reactifact-0.6.0.dist-info/METADATA +226 -0
- reactifact-0.6.0.dist-info/RECORD +103 -0
- reactifact-0.6.0.dist-info/WHEEL +5 -0
- reactifact-0.6.0.dist-info/entry_points.txt +2 -0
- reactifact-0.6.0.dist-info/licenses/LICENSE +21 -0
- reactifact-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Video generation providers (submit → poll → download).
|
|
2
|
+
|
|
3
|
+
Video backends (Sora, Runway, ...) expose an async, long-running task API:
|
|
4
|
+
you submit a prompt, get a task id, poll until the job completes, then fetch
|
|
5
|
+
the finished mp4. The base `_HttpVideoProvider` keeps that contract uniform
|
|
6
|
+
and testable (inject `transport` like the chat/image providers).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import time
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from ._retry import with_retry
|
|
20
|
+
from .chat import _network_knobs
|
|
21
|
+
from .contracts import auth_value
|
|
22
|
+
|
|
23
|
+
STATUS_MAP = {
|
|
24
|
+
"queued": "pending",
|
|
25
|
+
"queued_processing": "processing",
|
|
26
|
+
"in_progress": "processing",
|
|
27
|
+
"processing": "processing",
|
|
28
|
+
"running": "processing",
|
|
29
|
+
"dreaming": "processing",
|
|
30
|
+
"completed": "completed",
|
|
31
|
+
"succeeded": "completed",
|
|
32
|
+
"success": "completed",
|
|
33
|
+
"failed": "failed",
|
|
34
|
+
"error": "failed",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class VideoResult:
|
|
40
|
+
"""State of a video generation task."""
|
|
41
|
+
|
|
42
|
+
id: str
|
|
43
|
+
status: str = "pending" # pending | processing | completed | failed
|
|
44
|
+
url: str | None = None
|
|
45
|
+
error: str | None = None
|
|
46
|
+
data: bytes | None = None
|
|
47
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VideoProvider(ABC):
|
|
51
|
+
"""Async video generation: submit → poll → download.
|
|
52
|
+
|
|
53
|
+
Subclasses that can fetch the finished file from `result.url` override
|
|
54
|
+
`download` with an HTTP client; the embedded-`data` shortcut lives here.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_client: httpx.AsyncClient | None = None
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
async def generate(self, prompt: str, **params: Any) -> str:
|
|
61
|
+
"""Submits a generation and returns the task id (str)."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def fetch(self, task_id: str) -> VideoResult:
|
|
65
|
+
"""Current state of the task (pending/processing/completed/failed)."""
|
|
66
|
+
|
|
67
|
+
async def poll(
|
|
68
|
+
self, task_id: str, timeout: float = 600.0, interval: float = 5.0
|
|
69
|
+
) -> VideoResult:
|
|
70
|
+
"""Polls until completed/failed or the timeout elapses (best effort).
|
|
71
|
+
|
|
72
|
+
A `fetch()` that still raises after its own internal retry (a longer
|
|
73
|
+
outage, not a single blip) does not abort the poll — a several-
|
|
74
|
+
minute video job in progress is not worth abandoning over a
|
|
75
|
+
transient network problem, so the loop just waits for the next
|
|
76
|
+
interval and tries again. Only running out of `timeout` while every
|
|
77
|
+
recent fetch failed gives up, with an honest `status="failed"`
|
|
78
|
+
result rather than raising out of a best-effort poll.
|
|
79
|
+
"""
|
|
80
|
+
deadline = time.monotonic() + timeout
|
|
81
|
+
last_result: VideoResult | None = None
|
|
82
|
+
last_error: str | None = None
|
|
83
|
+
while True:
|
|
84
|
+
try:
|
|
85
|
+
last_result = await self.fetch(task_id)
|
|
86
|
+
last_error = None
|
|
87
|
+
except (httpx.HTTPStatusError, httpx.TransportError) as exc:
|
|
88
|
+
last_error = str(exc)
|
|
89
|
+
else:
|
|
90
|
+
if last_result.status in ("completed", "failed"):
|
|
91
|
+
return last_result
|
|
92
|
+
if time.monotonic() >= deadline:
|
|
93
|
+
if last_result is not None and last_error is None:
|
|
94
|
+
return last_result
|
|
95
|
+
return VideoResult(
|
|
96
|
+
id=task_id,
|
|
97
|
+
status="failed",
|
|
98
|
+
error=f"polling timed out after repeated fetch failures: {last_error}",
|
|
99
|
+
)
|
|
100
|
+
await asyncio.sleep(interval)
|
|
101
|
+
|
|
102
|
+
async def download(self, result: VideoResult) -> bytes | None:
|
|
103
|
+
"""Returns the finished video bytes, or None (no embed, no fetch)."""
|
|
104
|
+
return result.data
|
|
105
|
+
|
|
106
|
+
async def aclose(self) -> None:
|
|
107
|
+
if self._client is not None:
|
|
108
|
+
await self._client.aclose()
|
|
109
|
+
self._client = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _HttpVideoProvider(VideoProvider):
|
|
113
|
+
"""HTTP base for video providers: shared client + auth/proxy knobs."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
base_url: str,
|
|
118
|
+
api_key: str | None = None,
|
|
119
|
+
model: str | None = None,
|
|
120
|
+
timeout: float = 300.0,
|
|
121
|
+
transport: Any | None = None,
|
|
122
|
+
proxy: str | None = None,
|
|
123
|
+
auth_header: str = "Authorization",
|
|
124
|
+
auth_scheme: str | None = "Bearer",
|
|
125
|
+
extra_headers: dict[str, str] | None = None,
|
|
126
|
+
retry_attempts: int = 3,
|
|
127
|
+
):
|
|
128
|
+
self.base_url = base_url.rstrip("/")
|
|
129
|
+
self.api_key = api_key
|
|
130
|
+
self.model = model
|
|
131
|
+
self._timeout = timeout
|
|
132
|
+
self._headers = dict(extra_headers or {"Content-Type": "application/json"})
|
|
133
|
+
if api_key:
|
|
134
|
+
self._headers.setdefault(auth_header, auth_value(api_key, auth_scheme))
|
|
135
|
+
self._transport = transport
|
|
136
|
+
self._proxy = proxy
|
|
137
|
+
self.retry_attempts = retry_attempts
|
|
138
|
+
self._client: httpx.AsyncClient | None = None
|
|
139
|
+
|
|
140
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
141
|
+
if self._client is None:
|
|
142
|
+
self._client = httpx.AsyncClient(
|
|
143
|
+
timeout=self._timeout,
|
|
144
|
+
transport=self._transport,
|
|
145
|
+
headers=self._headers,
|
|
146
|
+
proxy=self._proxy,
|
|
147
|
+
)
|
|
148
|
+
return self._client
|
|
149
|
+
|
|
150
|
+
async def download(self, result: VideoResult) -> bytes | None:
|
|
151
|
+
if result.data is not None:
|
|
152
|
+
return result.data
|
|
153
|
+
if not result.url:
|
|
154
|
+
return None
|
|
155
|
+
url = result.url
|
|
156
|
+
|
|
157
|
+
async def _call() -> bytes:
|
|
158
|
+
response = await self._get_client().get(url)
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
return response.content
|
|
161
|
+
|
|
162
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class SoraVideoProvider(_HttpVideoProvider):
|
|
166
|
+
"""OpenAI Sora — `POST /videos` (submit) and `GET /videos/{id}` (poll)."""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
api_key: str | None = None,
|
|
171
|
+
model: str = "sora-1",
|
|
172
|
+
base_url: str = "https://api.openai.com/v1",
|
|
173
|
+
**kwargs: Any,
|
|
174
|
+
):
|
|
175
|
+
super().__init__(base_url=base_url, api_key=api_key, model=model, **kwargs)
|
|
176
|
+
|
|
177
|
+
async def generate(self, prompt: str, **params: Any) -> str:
|
|
178
|
+
payload: dict[str, Any] = {"model": self.model, "prompt": prompt}
|
|
179
|
+
for key in ("size", "duration", "quality"):
|
|
180
|
+
if params.get(key):
|
|
181
|
+
payload[key] = params[key]
|
|
182
|
+
|
|
183
|
+
async def _call() -> str:
|
|
184
|
+
response = await self._get_client().post(
|
|
185
|
+
f"{self.base_url}/videos", json=payload
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
return str(response.json()["id"])
|
|
189
|
+
|
|
190
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
191
|
+
|
|
192
|
+
async def fetch(self, task_id: str) -> VideoResult:
|
|
193
|
+
async def _call() -> VideoResult:
|
|
194
|
+
response = await self._get_client().get(f"{self.base_url}/videos/{task_id}")
|
|
195
|
+
response.raise_for_status()
|
|
196
|
+
data = response.json()
|
|
197
|
+
out = data.get("output") or {}
|
|
198
|
+
return VideoResult(
|
|
199
|
+
id=task_id,
|
|
200
|
+
status=STATUS_MAP.get(
|
|
201
|
+
str(data.get("status", "")).lower(), "processing"
|
|
202
|
+
),
|
|
203
|
+
url=out.get("url"),
|
|
204
|
+
error=out.get("error") or data.get("error"),
|
|
205
|
+
extra=data,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class RunwayVideoProvider(_HttpVideoProvider):
|
|
212
|
+
"""Runway ML — `POST /v1/videos` (submit) and `GET /v1/videos/{id}` (poll)."""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
api_key: str | None = None,
|
|
217
|
+
model: str = "gen3a_turbo",
|
|
218
|
+
base_url: str = "https://api.dev.runwayml.com/v1",
|
|
219
|
+
**kwargs: Any,
|
|
220
|
+
):
|
|
221
|
+
super().__init__(base_url=base_url, api_key=api_key, model=model, **kwargs)
|
|
222
|
+
|
|
223
|
+
async def generate(self, prompt: str, **params: Any) -> str:
|
|
224
|
+
payload: dict[str, Any] = {"model": self.model, "promptText": prompt}
|
|
225
|
+
if params.get("image"):
|
|
226
|
+
payload["promptImage"] = params["image"]
|
|
227
|
+
if params.get("ratio"):
|
|
228
|
+
payload["ratio"] = params["ratio"]
|
|
229
|
+
|
|
230
|
+
async def _call() -> str:
|
|
231
|
+
response = await self._get_client().post(
|
|
232
|
+
f"{self.base_url}/v1/videos", json=payload
|
|
233
|
+
)
|
|
234
|
+
response.raise_for_status()
|
|
235
|
+
return str(response.json()["id"])
|
|
236
|
+
|
|
237
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
238
|
+
|
|
239
|
+
async def fetch(self, task_id: str) -> VideoResult:
|
|
240
|
+
async def _call() -> VideoResult:
|
|
241
|
+
response = await self._get_client().get(
|
|
242
|
+
f"{self.base_url}/v1/videos/{task_id}"
|
|
243
|
+
)
|
|
244
|
+
response.raise_for_status()
|
|
245
|
+
data = response.json()
|
|
246
|
+
output = data.get("output") or {}
|
|
247
|
+
return VideoResult(
|
|
248
|
+
id=task_id,
|
|
249
|
+
status=STATUS_MAP.get(
|
|
250
|
+
str(data.get("status", "")).lower(), "processing"
|
|
251
|
+
),
|
|
252
|
+
url=output.get("url"),
|
|
253
|
+
error=output.get("error"),
|
|
254
|
+
extra=data,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class LumaVideoProvider(_HttpVideoProvider):
|
|
261
|
+
"""Luma Dream Machine — `POST /v1/generations` (submit) + GET by id (poll)."""
|
|
262
|
+
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
api_key: str | None = None,
|
|
266
|
+
model: str = "ray-2",
|
|
267
|
+
base_url: str = "https://api.lumalabs.ai/dream-machine/v1",
|
|
268
|
+
**kwargs: Any,
|
|
269
|
+
):
|
|
270
|
+
super().__init__(base_url=base_url, api_key=api_key, model=model, **kwargs)
|
|
271
|
+
|
|
272
|
+
async def generate(self, prompt: str, **params: Any) -> str:
|
|
273
|
+
payload: dict[str, Any] = {"model": self.model, "prompt": prompt}
|
|
274
|
+
if params.get("image"):
|
|
275
|
+
payload["promptImage"] = params["image"]
|
|
276
|
+
if params.get("duration"):
|
|
277
|
+
payload["duration"] = params["duration"]
|
|
278
|
+
|
|
279
|
+
async def _call() -> str:
|
|
280
|
+
response = await self._get_client().post(
|
|
281
|
+
f"{self.base_url}/generations", json=payload
|
|
282
|
+
)
|
|
283
|
+
response.raise_for_status()
|
|
284
|
+
return str(response.json()["id"])
|
|
285
|
+
|
|
286
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
287
|
+
|
|
288
|
+
async def fetch(self, task_id: str) -> VideoResult:
|
|
289
|
+
async def _call() -> VideoResult:
|
|
290
|
+
response = await self._get_client().get(
|
|
291
|
+
f"{self.base_url}/generations/{task_id}"
|
|
292
|
+
)
|
|
293
|
+
response.raise_for_status()
|
|
294
|
+
data = response.json()
|
|
295
|
+
assets = data.get("assets") or {}
|
|
296
|
+
return VideoResult(
|
|
297
|
+
id=task_id,
|
|
298
|
+
status=STATUS_MAP.get(str(data.get("state", "")).lower(), "processing"),
|
|
299
|
+
url=assets.get("video"),
|
|
300
|
+
error=data.get("failure_reason"),
|
|
301
|
+
extra=data,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class OpenRouterVideoProvider(_HttpVideoProvider):
|
|
308
|
+
"""OpenRouter video — Generations API (`POST /api/v1/generations` + poll).
|
|
309
|
+
|
|
310
|
+
Same key as chat/images: `OPENROUTER_API_KEY`.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
api_key: str | None = None,
|
|
316
|
+
model: str = "openai/sora-2",
|
|
317
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
|
318
|
+
**kwargs: Any,
|
|
319
|
+
):
|
|
320
|
+
super().__init__(base_url=base_url, api_key=api_key, model=model, **kwargs)
|
|
321
|
+
|
|
322
|
+
async def generate(self, prompt: str, **params: Any) -> str:
|
|
323
|
+
payload: dict[str, Any] = {"model": self.model, "prompt": prompt}
|
|
324
|
+
for key in ("negative_prompt", "height", "width", "guidance_scale"):
|
|
325
|
+
if params.get(key):
|
|
326
|
+
payload[key] = params[key]
|
|
327
|
+
|
|
328
|
+
async def _call() -> str:
|
|
329
|
+
response = await self._get_client().post(
|
|
330
|
+
f"{self.base_url}/generations", json=payload
|
|
331
|
+
)
|
|
332
|
+
response.raise_for_status()
|
|
333
|
+
return str(response.json()["id"])
|
|
334
|
+
|
|
335
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
336
|
+
|
|
337
|
+
async def fetch(self, task_id: str) -> VideoResult:
|
|
338
|
+
async def _call() -> VideoResult:
|
|
339
|
+
response = await self._get_client().get(
|
|
340
|
+
f"{self.base_url}/generations/{task_id}"
|
|
341
|
+
)
|
|
342
|
+
response.raise_for_status()
|
|
343
|
+
data = response.json()
|
|
344
|
+
out = data.get("out") or []
|
|
345
|
+
if out:
|
|
346
|
+
first = out[0]
|
|
347
|
+
url = first.get("video_url") or first.get("url")
|
|
348
|
+
return VideoResult(id=task_id, status="completed", url=url, extra=data)
|
|
349
|
+
if data.get("error"):
|
|
350
|
+
return VideoResult(
|
|
351
|
+
id=task_id, status="failed", error=str(data["error"])
|
|
352
|
+
)
|
|
353
|
+
return VideoResult(id=task_id, status="processing", extra=data)
|
|
354
|
+
|
|
355
|
+
return await with_retry(_call, attempts=self.retry_attempts)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def video_from_env(**overrides: Any) -> VideoProvider | None:
|
|
359
|
+
"""Builds a video provider from env (VIDEO_PROVIDER=sora|runway|luma|openrouter).
|
|
360
|
+
|
|
361
|
+
Keys: VIDEO_API_KEY (or SORA_API_KEY / RUNWAY_API_KEY / LUMA_API_KEY /
|
|
362
|
+
OPENROUTER_API_KEY / OPENAI_API_KEY), VIDEO_BASE_URL, VIDEO_MODEL, plus the
|
|
363
|
+
usual VIDEO_PROXY / VIDEO_AUTH_HEADER / VIDEO_AUTH_SCHEME knobs.
|
|
364
|
+
"""
|
|
365
|
+
import os
|
|
366
|
+
|
|
367
|
+
provider_name = str(
|
|
368
|
+
overrides.get("provider") or os.getenv("VIDEO_PROVIDER") or "sora"
|
|
369
|
+
).lower()
|
|
370
|
+
api_key = (
|
|
371
|
+
overrides.get("api_key")
|
|
372
|
+
or os.getenv("VIDEO_API_KEY")
|
|
373
|
+
or os.getenv(f"{provider_name.upper()}_API_KEY")
|
|
374
|
+
or os.getenv("OPENAI_API_KEY")
|
|
375
|
+
)
|
|
376
|
+
if not api_key:
|
|
377
|
+
return None
|
|
378
|
+
model = overrides.get("model") or os.getenv("VIDEO_MODEL")
|
|
379
|
+
base_url = overrides.get("base_url") or os.getenv("VIDEO_BASE_URL")
|
|
380
|
+
merged = {**_network_knobs("VIDEO", overrides), **overrides}
|
|
381
|
+
if provider_name == "runway":
|
|
382
|
+
return RunwayVideoProvider(
|
|
383
|
+
api_key=api_key,
|
|
384
|
+
model=model or "gen3a_turbo",
|
|
385
|
+
base_url=base_url or "https://api.dev.runwayml.com/v1",
|
|
386
|
+
**merged,
|
|
387
|
+
)
|
|
388
|
+
if provider_name == "luma":
|
|
389
|
+
return LumaVideoProvider(
|
|
390
|
+
api_key=api_key,
|
|
391
|
+
model=model or "ray-2",
|
|
392
|
+
base_url=base_url or "https://api.lumalabs.ai/dream-machine/v1",
|
|
393
|
+
**merged,
|
|
394
|
+
)
|
|
395
|
+
if provider_name == "openrouter":
|
|
396
|
+
return OpenRouterVideoProvider(
|
|
397
|
+
api_key=api_key,
|
|
398
|
+
model=model or "openai/sora-2",
|
|
399
|
+
base_url=base_url or "https://openrouter.ai/api/v1",
|
|
400
|
+
**merged,
|
|
401
|
+
)
|
|
402
|
+
return SoraVideoProvider(
|
|
403
|
+
api_key=api_key,
|
|
404
|
+
model=model or "sora-1",
|
|
405
|
+
base_url=base_url or "https://api.openai.com/v1",
|
|
406
|
+
**merged,
|
|
407
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""z.ai (Zhipu GLM) — chat (OpenAI-compatible)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .chat import _openai_compat_llm
|
|
6
|
+
|
|
7
|
+
zai_llm = _openai_compat_llm(
|
|
8
|
+
env_prefix="ZAI",
|
|
9
|
+
default_model="glm-4.6",
|
|
10
|
+
default_base_url="https://api.z.ai/api/paas/v4",
|
|
11
|
+
)
|
reactifact/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Off-the-shelf building blocks for agents — reactive patterns and helpers.
|
|
2
|
+
|
|
3
|
+
These are not core primitives (Context/Artifact/Patch/Produce stay minimal, the
|
|
4
|
+
constitution's primitives-first rule). Instead they are ready-made patterns that
|
|
5
|
+
keep reappearing across agent codebases and the bundled examples:
|
|
6
|
+
|
|
7
|
+
- `find` / `find_all` — pick the typed artifact(s) out of a produce's
|
|
8
|
+
`inputs` without repeating `next(... isinstance ...)` — see `recipes.inputs`;
|
|
9
|
+
- `fan_out_sources` — query all configured sources, emit ranked, idempotent
|
|
10
|
+
`SourceRef`s tagged with an owner (§8, §24, §42) — see `recipes.search`;
|
|
11
|
+
- `materialize_doc` — lazily resolve a `SourceRef` into a document with a
|
|
12
|
+
provenance edge (Reference → Artifact, §6, §34) — see `recipes.resolve`;
|
|
13
|
+
- `StatusMachine` — a `Produce` that deterministically advances an artifact's
|
|
14
|
+
`status` lifecycle driven by a pure `next_status(context, key)` (§67, §69) —
|
|
15
|
+
see `recipes.status`;
|
|
16
|
+
- `WindowSummarizer` / `WindowPruner` / `llm_summarizer` — bounded
|
|
17
|
+
conversation memory: periodic summarization + pruning as two plain
|
|
18
|
+
`Produce`s, domain owns the summarizer callback and the summary artifact
|
|
19
|
+
shape (§27, §37) — see `recipes.memory`;
|
|
20
|
+
- `keyword_score` / `stem_words` — deterministic text scoring without
|
|
21
|
+
embedders (English and Russian) — see `recipes.text`;
|
|
22
|
+
- `changed_fields` / `earliest_stage` / `downstream_fields` — the
|
|
23
|
+
"change → rebuild" model for multi-stage flows — see `recipes.rollback`;
|
|
24
|
+
- `Skill` / `load_skills` / `match_skills` — keyword-triggered instruction
|
|
25
|
+
snippets (Claude-Skills-shaped: name/description frontmatter + body) loaded
|
|
26
|
+
into a prompt when their description matches the situation — see
|
|
27
|
+
`recipes.skills`.
|
|
28
|
+
|
|
29
|
+
Deterministic where it can be, LLM-free by design; expose the domain hook.
|
|
30
|
+
Extend by adding a module here (the package stays import-surface-flat).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from .inputs import find, find_all
|
|
36
|
+
from .memory import WindowPruner, WindowSummarizer, llm_summarizer
|
|
37
|
+
from .resolve import materialize_doc
|
|
38
|
+
from .rollback import changed_fields, downstream_fields, earliest_stage
|
|
39
|
+
from .search import fan_out_sources
|
|
40
|
+
from .skills import Skill, load_skills, match_skills
|
|
41
|
+
from .status import StatusMachine
|
|
42
|
+
from .text import EN_STOPWORDS, keyword_score, stem, stem_words
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"EN_STOPWORDS",
|
|
46
|
+
"Skill",
|
|
47
|
+
"StatusMachine",
|
|
48
|
+
"WindowPruner",
|
|
49
|
+
"WindowSummarizer",
|
|
50
|
+
"changed_fields",
|
|
51
|
+
"downstream_fields",
|
|
52
|
+
"earliest_stage",
|
|
53
|
+
"fan_out_sources",
|
|
54
|
+
"find",
|
|
55
|
+
"find_all",
|
|
56
|
+
"keyword_score",
|
|
57
|
+
"llm_summarizer",
|
|
58
|
+
"load_skills",
|
|
59
|
+
"match_skills",
|
|
60
|
+
"materialize_doc",
|
|
61
|
+
"stem",
|
|
62
|
+
"stem_words",
|
|
63
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""recipes — locating typed artifacts among a produce's `inputs`.
|
|
2
|
+
|
|
3
|
+
A produce whose agent declares more than one `Consume` type receives a flat
|
|
4
|
+
`list[Artifact[Any]]` merged across all of them; picking out "the one
|
|
5
|
+
Question" or "all the Evidence" is the same
|
|
6
|
+
`next((a for a in inputs if isinstance(a.data, X)), None)` boilerplate in
|
|
7
|
+
nearly every example. These two helpers replace it without hiding anything —
|
|
8
|
+
`find` is a typed `next(..., None)`, `find_all` is a typed filter.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, TypeVar
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from ..artifacts import Artifact
|
|
18
|
+
|
|
19
|
+
TData = TypeVar("TData", bound=BaseModel)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find(inputs: list[Artifact[Any]], data_type: type[TData]) -> Artifact[TData] | None:
|
|
23
|
+
"""First input artifact whose `.data` is an instance of `data_type`, else `None`."""
|
|
24
|
+
return next((a for a in inputs if isinstance(a.data, data_type)), None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_all(
|
|
28
|
+
inputs: list[Artifact[Any]], data_type: type[TData]
|
|
29
|
+
) -> list[Artifact[TData]]:
|
|
30
|
+
"""All input artifacts whose `.data` is an instance of `data_type`."""
|
|
31
|
+
return [a for a in inputs if isinstance(a.data, data_type)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["find", "find_all"]
|