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.
Files changed (103) hide show
  1. reactifact/__init__.py +96 -0
  2. reactifact/__main__.py +10 -0
  3. reactifact/_extras.py +36 -0
  4. reactifact/agents.py +173 -0
  5. reactifact/artifacts.py +130 -0
  6. reactifact/branching.py +255 -0
  7. reactifact/budget.py +41 -0
  8. reactifact/chat.py +373 -0
  9. reactifact/checkpoints.py +329 -0
  10. reactifact/cli/__init__.py +73 -0
  11. reactifact/cli/branch.py +77 -0
  12. reactifact/cli/common.py +67 -0
  13. reactifact/cli/context.py +53 -0
  14. reactifact/cli/graph.py +21 -0
  15. reactifact/cli/replay.py +69 -0
  16. reactifact/cli/scenario.py +94 -0
  17. reactifact/cli/trace.py +45 -0
  18. reactifact/commit.py +97 -0
  19. reactifact/commit_log.py +235 -0
  20. reactifact/consume.py +96 -0
  21. reactifact/context.py +599 -0
  22. reactifact/effects.py +232 -0
  23. reactifact/eval.py +319 -0
  24. reactifact/events.py +34 -0
  25. reactifact/interrupt.py +22 -0
  26. reactifact/llm_agent.py +172 -0
  27. reactifact/operations.py +192 -0
  28. reactifact/patches.py +112 -0
  29. reactifact/produce.py +226 -0
  30. reactifact/prompts.py +111 -0
  31. reactifact/providers/__init__.py +153 -0
  32. reactifact/providers/_retry.py +61 -0
  33. reactifact/providers/anthropic.py +182 -0
  34. reactifact/providers/azure.py +31 -0
  35. reactifact/providers/cerebras.py +11 -0
  36. reactifact/providers/chat.py +417 -0
  37. reactifact/providers/contracts.py +105 -0
  38. reactifact/providers/deepseek.py +11 -0
  39. reactifact/providers/fake.py +40 -0
  40. reactifact/providers/fireworks.py +17 -0
  41. reactifact/providers/gemini.py +284 -0
  42. reactifact/providers/github_models.py +13 -0
  43. reactifact/providers/groq.py +18 -0
  44. reactifact/providers/image.py +157 -0
  45. reactifact/providers/mistral.py +17 -0
  46. reactifact/providers/nvidia.py +18 -0
  47. reactifact/providers/ollama.py +18 -0
  48. reactifact/providers/openai.py +44 -0
  49. reactifact/providers/openrouter.py +70 -0
  50. reactifact/providers/perplexity.py +11 -0
  51. reactifact/providers/qwen.py +17 -0
  52. reactifact/providers/speech.py +347 -0
  53. reactifact/providers/together.py +17 -0
  54. reactifact/providers/video.py +407 -0
  55. reactifact/providers/xai.py +11 -0
  56. reactifact/providers/zai.py +11 -0
  57. reactifact/py.typed +0 -0
  58. reactifact/recipes/__init__.py +63 -0
  59. reactifact/recipes/inputs.py +34 -0
  60. reactifact/recipes/memory.py +166 -0
  61. reactifact/recipes/resolve.py +51 -0
  62. reactifact/recipes/rollback.py +87 -0
  63. reactifact/recipes/search.py +81 -0
  64. reactifact/recipes/skills.py +108 -0
  65. reactifact/recipes/status.py +79 -0
  66. reactifact/recipes/text.py +202 -0
  67. reactifact/relations.py +104 -0
  68. reactifact/replay.py +187 -0
  69. reactifact/resources.py +45 -0
  70. reactifact/runtime.py +498 -0
  71. reactifact/scheduler.py +188 -0
  72. reactifact/session.py +75 -0
  73. reactifact/sources.py +498 -0
  74. reactifact/streaming.py +58 -0
  75. reactifact/structured.py +245 -0
  76. reactifact/testing/__init__.py +48 -0
  77. reactifact/testing/assertions.py +326 -0
  78. reactifact/testing/exceptions.py +27 -0
  79. reactifact/testing/fault.py +164 -0
  80. reactifact/testing/lab.py +350 -0
  81. reactifact/testing/mock.py +166 -0
  82. reactifact/testing/record.py +50 -0
  83. reactifact/testing/registry.py +87 -0
  84. reactifact/tool_use.py +528 -0
  85. reactifact/tools.py +111 -0
  86. reactifact/tracing/__init__.py +29 -0
  87. reactifact/tracing/langfuse.py +125 -0
  88. reactifact/tracing/models.py +93 -0
  89. reactifact/tracing/postgres.py +220 -0
  90. reactifact/tracing/store.py +254 -0
  91. reactifact/tracing/templates/ui.html +196 -0
  92. reactifact/tracing/templates/ui_run.html +264 -0
  93. reactifact/tracing/tracer.py +370 -0
  94. reactifact/tracing/web.py +117 -0
  95. reactifact/triggers.py +41 -0
  96. reactifact/viz.py +248 -0
  97. reactifact/web.py +117 -0
  98. reactifact-0.6.0.dist-info/METADATA +226 -0
  99. reactifact-0.6.0.dist-info/RECORD +103 -0
  100. reactifact-0.6.0.dist-info/WHEEL +5 -0
  101. reactifact-0.6.0.dist-info/entry_points.txt +2 -0
  102. reactifact-0.6.0.dist-info/licenses/LICENSE +21 -0
  103. reactifact-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,347 @@
1
+ """Speech: text-to-speech (synthesis) and speech-to-text (transcription).
2
+
3
+ Both are OpenAI-compatible (`/audio/speech` and `/audio/transcriptions`), so
4
+ they also cover vendors that mirror OpenAI (Groq Whisper, Azure Speech, ...)
5
+ by pointing `base_url` at their OpenAI-compatible endpoint. Auth header/scheme
6
+ and proxy are configurable like every other provider.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Callable
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ from ._retry import with_retry
18
+ from .chat import _network_knobs, _resolve_env_api_key
19
+ from .contracts import auth_value
20
+
21
+
22
+ class SpeechProvider(ABC):
23
+ """Text-to-speech: returns audio bytes from text."""
24
+
25
+ @abstractmethod
26
+ async def synthesize(self, text: str, **params: Any) -> bytes:
27
+ """Synthesizes speech; returns audio bytes (mp3/opus/wav/aac...)."""
28
+
29
+
30
+ class TranscriberProvider(ABC):
31
+ """Speech-to-text: returns a transcript from audio bytes."""
32
+
33
+ @abstractmethod
34
+ async def transcribe(self, audio: bytes, **params: Any) -> str:
35
+ """Transcribes audio; returns the text."""
36
+
37
+
38
+ class Connectable:
39
+ """Shared HTTP client builder for the OpenAI-compatible speech endpoints.
40
+
41
+ No global Content-Type: httpx sets it per request (JSON for `json=`,
42
+ multipart boundary for `files=`) — forcing it here would break the
43
+ multipart transcription upload.
44
+ """
45
+
46
+ def build_client(
47
+ self,
48
+ timeout: float,
49
+ transport: Any,
50
+ proxy: str | None,
51
+ api_key: str | None,
52
+ auth_header: str,
53
+ auth_scheme: str | None,
54
+ ) -> httpx.AsyncClient:
55
+ headers: dict[str, str] = {}
56
+ if api_key:
57
+ headers[auth_header] = auth_value(api_key, auth_scheme)
58
+ return httpx.AsyncClient(
59
+ timeout=timeout, transport=transport, headers=headers, proxy=proxy
60
+ )
61
+
62
+
63
+ class OpenAICompatSpeech(SpeechProvider):
64
+ """TTS via `{base}/audio/speech` (OpenAI tts-1/gpt-4o-mini-tts, Azure...)."""
65
+
66
+ def __init__(
67
+ self,
68
+ base_url: str = "https://api.openai.com/v1",
69
+ api_key: str | None = None,
70
+ model: str = "tts-1",
71
+ voice: str = "alloy",
72
+ timeout: float = 60.0,
73
+ transport: Any | None = None,
74
+ proxy: str | None = None,
75
+ auth_header: str = "Authorization",
76
+ auth_scheme: str | None = "Bearer",
77
+ extra_params: dict[str, Any] | None = None,
78
+ retry_attempts: int = 3,
79
+ ):
80
+ self.base_url = base_url.rstrip("/")
81
+ self.api_key = api_key
82
+ self.model = model
83
+ self.voice = voice
84
+ self._timeout = timeout
85
+ self._extra = dict(extra_params or {})
86
+ self._transport = transport
87
+ self._proxy = proxy
88
+ self._auth_header = auth_header
89
+ self._auth_scheme = auth_scheme
90
+ self.retry_attempts = retry_attempts
91
+ self._client: httpx.AsyncClient | None = None
92
+
93
+ def _get_client(self) -> httpx.AsyncClient:
94
+ if self._client is None:
95
+ self._client = Connectable().build_client(
96
+ self._timeout,
97
+ self._transport,
98
+ self._proxy,
99
+ self.api_key,
100
+ self._auth_header,
101
+ self._auth_scheme,
102
+ )
103
+ return self._client
104
+
105
+ async def synthesize(self, text: str, **params: Any) -> bytes:
106
+ payload: dict[str, Any] = {
107
+ "model": params.get("model") or self.model,
108
+ "input": text,
109
+ "voice": params.get("voice") or self.voice,
110
+ }
111
+ for key in ("response_format", "speed", "instructions"):
112
+ if params.get(key):
113
+ payload[key] = params[key]
114
+ payload.update(self._extra)
115
+
116
+ async def _call() -> bytes:
117
+ response = await self._get_client().post(
118
+ f"{self.base_url}/audio/speech", json=payload
119
+ )
120
+ response.raise_for_status()
121
+ return response.content
122
+
123
+ return await with_retry(_call, attempts=self.retry_attempts)
124
+
125
+ async def aclose(self) -> None:
126
+ if self._client is not None:
127
+ await self._client.aclose()
128
+ self._client = None
129
+
130
+
131
+ class OpenAICompatTranscriber(TranscriberProvider):
132
+ """STT via `{base}/audio/transcriptions` (OpenAI Whisper, Groq Whisper...)."""
133
+
134
+ def __init__(
135
+ self,
136
+ base_url: str = "https://api.openai.com/v1",
137
+ api_key: str | None = None,
138
+ model: str = "whisper-1",
139
+ timeout: float = 120.0,
140
+ transport: Any | None = None,
141
+ proxy: str | None = None,
142
+ auth_header: str = "Authorization",
143
+ auth_scheme: str | None = "Bearer",
144
+ mime_type: str = "audio/webm",
145
+ filename: str = "audio.webm",
146
+ retry_attempts: int = 3,
147
+ ):
148
+ self.base_url = base_url.rstrip("/")
149
+ self.api_key = api_key
150
+ self.model = model
151
+ self._timeout = timeout
152
+ self._transport = transport
153
+ self._proxy = proxy
154
+ self._auth_header = auth_header
155
+ self._auth_scheme = auth_scheme
156
+ self._mime_type = mime_type
157
+ self._filename = filename
158
+ self.retry_attempts = retry_attempts
159
+ self._client: httpx.AsyncClient | None = None
160
+
161
+ def _get_client(self) -> httpx.AsyncClient:
162
+ if self._client is None:
163
+ self._client = Connectable().build_client(
164
+ self._timeout,
165
+ self._transport,
166
+ self._proxy,
167
+ self.api_key,
168
+ self._auth_header,
169
+ self._auth_scheme,
170
+ )
171
+ return self._client
172
+
173
+ async def transcribe(self, audio: bytes, **params: Any) -> str:
174
+ model = params.get("model") or self.model
175
+ files = {
176
+ "file": (params.get("filename") or self._filename, audio, self._mime_type)
177
+ }
178
+ data: dict[str, str] = {"model": model}
179
+ for key in ("language", "prompt", "response_format"):
180
+ if params.get(key):
181
+ data[key] = str(params[key])
182
+
183
+ async def _call() -> str:
184
+ response = await self._get_client().post(
185
+ f"{self.base_url}/audio/transcriptions",
186
+ files=files,
187
+ data=data,
188
+ )
189
+ response.raise_for_status()
190
+ body = response.json()
191
+ if isinstance(body, dict):
192
+ return str(body.get("text", ""))
193
+ return str(body)
194
+
195
+ return await with_retry(_call, attempts=self.retry_attempts)
196
+
197
+ async def aclose(self) -> None:
198
+ if self._client is not None:
199
+ await self._client.aclose()
200
+ self._client = None
201
+
202
+
203
+ def _factory_extra(overrides: dict[str, Any], skip: tuple[str, ...]) -> dict[str, Any]:
204
+ return {k: v for k, v in overrides.items() if k not in skip}
205
+
206
+
207
+ def _openai_compat_speech(
208
+ *,
209
+ env_prefix: str,
210
+ default_model: str,
211
+ default_voice: str,
212
+ default_base_url: str,
213
+ env_api_key_vars: tuple[str, ...] | None = None,
214
+ name: str | None = None,
215
+ doc: str = "",
216
+ ) -> Callable[..., OpenAICompatSpeech]:
217
+ """Builds a `<vendor>_speech(model=..., voice=..., base_url=...,
218
+ api_key=None, **kwargs)` TTS factory for a vendor whose `/audio/speech`
219
+ endpoint is OpenAI-compatible — the speech counterpart of
220
+ `_openai_compat_llm` (`reactifact.providers.chat`).
221
+ """
222
+ key_vars = env_api_key_vars or (f"{env_prefix}_API_KEY",)
223
+
224
+ def factory(
225
+ model: str = default_model,
226
+ voice: str = default_voice,
227
+ base_url: str = default_base_url,
228
+ api_key: str | None = None,
229
+ **kwargs: Any,
230
+ ) -> OpenAICompatSpeech:
231
+ merged = {**_network_knobs(env_prefix, kwargs), **kwargs}
232
+ return OpenAICompatSpeech(
233
+ base_url=base_url,
234
+ api_key=_resolve_env_api_key(api_key, key_vars),
235
+ model=model,
236
+ voice=voice,
237
+ **merged,
238
+ )
239
+
240
+ factory.__name__ = name or f"{env_prefix.lower()}_speech"
241
+ factory.__qualname__ = factory.__name__
242
+ factory.__doc__ = doc or (
243
+ f"{env_prefix.title()} — OpenAI-compatible text-to-speech "
244
+ f"(key from {' or '.join(key_vars)})."
245
+ )
246
+ return factory
247
+
248
+
249
+ def _openai_compat_transcriber(
250
+ *,
251
+ env_prefix: str,
252
+ default_model: str,
253
+ default_base_url: str,
254
+ env_api_key_vars: tuple[str, ...] | None = None,
255
+ name: str | None = None,
256
+ doc: str = "",
257
+ ) -> Callable[..., OpenAICompatTranscriber]:
258
+ """Builds a `<vendor>_transcriber(model=..., base_url=..., api_key=None,
259
+ **kwargs)` STT factory for a vendor whose `/audio/transcriptions`
260
+ endpoint takes the same multipart-file request OpenAI's Whisper API
261
+ does (not every "OpenAI-compatible" STT endpoint does — OpenRouter's,
262
+ for one, takes base64 JSON instead, so it does *not* use this factory).
263
+ """
264
+ key_vars = env_api_key_vars or (f"{env_prefix}_API_KEY",)
265
+
266
+ def factory(
267
+ model: str = default_model,
268
+ base_url: str = default_base_url,
269
+ api_key: str | None = None,
270
+ **kwargs: Any,
271
+ ) -> OpenAICompatTranscriber:
272
+ merged = {**_network_knobs(env_prefix, kwargs), **kwargs}
273
+ return OpenAICompatTranscriber(
274
+ base_url=base_url,
275
+ api_key=_resolve_env_api_key(api_key, key_vars),
276
+ model=model,
277
+ **merged,
278
+ )
279
+
280
+ factory.__name__ = name or f"{env_prefix.lower()}_transcriber"
281
+ factory.__qualname__ = factory.__name__
282
+ factory.__doc__ = doc or (
283
+ f"{env_prefix.title()} — OpenAI-compatible transcription "
284
+ f"(key from {' or '.join(key_vars)})."
285
+ )
286
+ return factory
287
+
288
+
289
+ def speech_from_env(**overrides: Any) -> OpenAICompatSpeech | None:
290
+ """Builds a TTS provider from env (SPEECH_* or OPENAI_*).
291
+
292
+ Keys: SPEECH_BASE_URL / SPEECH_API_KEY / SPEECH_MODEL / SPEECH_VOICE plus
293
+ the usual SPEECH_PROXY / SPEECH_AUTH_HEADER / SPEECH_AUTH_SCHEME.
294
+ """
295
+ import os
296
+
297
+ api_key = (
298
+ overrides.get("api_key")
299
+ or os.getenv("SPEECH_API_KEY")
300
+ or os.getenv("OPENAI_API_KEY")
301
+ )
302
+ if not api_key:
303
+ return None
304
+ merged = {
305
+ **_network_knobs("SPEECH", overrides),
306
+ **_factory_extra(overrides, ("api_key", "base_url", "model", "voice")),
307
+ }
308
+ return OpenAICompatSpeech(
309
+ base_url=overrides.get("base_url")
310
+ or os.getenv("SPEECH_BASE_URL")
311
+ or "https://api.openai.com/v1",
312
+ api_key=api_key,
313
+ model=overrides.get("model") or os.getenv("SPEECH_MODEL") or "tts-1",
314
+ voice=overrides.get("voice") or os.getenv("SPEECH_VOICE") or "alloy",
315
+ **merged,
316
+ )
317
+
318
+
319
+ def transcriber_from_env(**overrides: Any) -> OpenAICompatTranscriber | None:
320
+ """Builds an STT provider from env (TRANSCRIBER_* or OPENAI_*).
321
+
322
+ Keys: TRANSCRIBER_BASE_URL / TRANSCRIBER_API_KEY / TRANSCRIBER_MODEL plus
323
+ the usual TRANSCRIBER_PROXY / TRANSCRIBER_AUTH_HEADER / AUTH_SCHEME.
324
+ Point TRANSCRIBER_BASE_URL at api.groq.com/openai/v1 and set
325
+ TRANSCRIBER_MODEL=whisper-large-v3-turbo for Groq Whisper.
326
+ """
327
+ import os
328
+
329
+ api_key = (
330
+ overrides.get("api_key")
331
+ or os.getenv("TRANSCRIBER_API_KEY")
332
+ or os.getenv("OPENAI_API_KEY")
333
+ )
334
+ if not api_key:
335
+ return None
336
+ merged = {
337
+ **_network_knobs("TRANSCRIBER", overrides),
338
+ **_factory_extra(overrides, ("api_key", "base_url", "model")),
339
+ }
340
+ return OpenAICompatTranscriber(
341
+ base_url=overrides.get("base_url")
342
+ or os.getenv("TRANSCRIBER_BASE_URL")
343
+ or "https://api.openai.com/v1",
344
+ api_key=api_key,
345
+ model=overrides.get("model") or os.getenv("TRANSCRIBER_MODEL") or "whisper-1",
346
+ **merged,
347
+ )
@@ -0,0 +1,17 @@
1
+ """Together AI — hosted open models (OpenAI-compatible), plus embeddings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .chat import _openai_compat_embedder, _openai_compat_llm
6
+
7
+ together_llm = _openai_compat_llm(
8
+ env_prefix="TOGETHER",
9
+ default_model="meta-llama/Llama-3.3-70B-Instruct-Turbo",
10
+ default_base_url="https://api.together.xyz/v1",
11
+ )
12
+
13
+ together_embedder = _openai_compat_embedder(
14
+ env_prefix="TOGETHER",
15
+ default_model="BAAI/bge-large-en-v1.5",
16
+ default_base_url="https://api.together.xyz/v1",
17
+ )