mirascope 2.1.1__py3-none-any.whl → 2.2.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.
- mirascope/api/_generated/functions/client.py +10 -0
- mirascope/api/_generated/functions/raw_client.py +8 -0
- mirascope/api/_generated/functions/types/functions_create_response.py +25 -8
- mirascope/api/_generated/functions/types/functions_find_by_hash_response.py +25 -10
- mirascope/api/_generated/functions/types/functions_get_by_env_response.py +1 -0
- mirascope/api/_generated/functions/types/functions_get_response.py +25 -8
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +1 -0
- mirascope/api/_generated/functions/types/functions_list_response_functions_item.py +22 -7
- mirascope/api/_generated/reference.md +9 -0
- mirascope/llm/__init__.py +42 -0
- mirascope/llm/calls/calls.py +38 -11
- mirascope/llm/exceptions.py +69 -0
- mirascope/llm/prompts/prompts.py +47 -9
- mirascope/llm/responses/response.py +217 -0
- mirascope/llm/responses/stream_response.py +234 -0
- mirascope/llm/retries/__init__.py +51 -0
- mirascope/llm/retries/retry_calls.py +159 -0
- mirascope/llm/retries/retry_config.py +168 -0
- mirascope/llm/retries/retry_decorator.py +258 -0
- mirascope/llm/retries/retry_models.py +1313 -0
- mirascope/llm/retries/retry_prompts.py +227 -0
- mirascope/llm/retries/retry_responses.py +340 -0
- mirascope/llm/retries/retry_stream_responses.py +571 -0
- mirascope/llm/retries/utils.py +159 -0
- mirascope/ops/_internal/versioned_calls.py +249 -9
- mirascope/ops/_internal/versioned_functions.py +2 -0
- {mirascope-2.1.1.dist-info → mirascope-2.2.0.dist-info}/METADATA +1 -1
- {mirascope-2.1.1.dist-info → mirascope-2.2.0.dist-info}/RECORD +30 -21
- {mirascope-2.1.1.dist-info → mirascope-2.2.0.dist-info}/WHEEL +0 -0
- {mirascope-2.1.1.dist-info → mirascope-2.2.0.dist-info}/licenses/LICENSE +0 -0
mirascope/llm/prompts/prompts.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Concrete Prompt classes for generating messages with tools and formatting."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
4
|
from typing import Any, Generic, TypeVar, overload
|
|
6
5
|
|
|
7
6
|
from ..._utils import copy_function_metadata
|
|
@@ -32,22 +31,20 @@ from .protocols import (
|
|
|
32
31
|
FunctionT = TypeVar("FunctionT", bound=Callable[..., Any])
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
@dataclass(kw_only=True)
|
|
36
34
|
class BasePrompt(Generic[FunctionT]):
|
|
37
35
|
"""Base class for all Prompt types with shared metadata functionality."""
|
|
38
36
|
|
|
39
37
|
fn: FunctionT
|
|
40
38
|
"""The underlying prompt function that generates message content."""
|
|
41
39
|
|
|
42
|
-
__name__: str =
|
|
40
|
+
__name__: str = ""
|
|
43
41
|
"""The name of the underlying function (preserved for decorator stacking)."""
|
|
44
42
|
|
|
45
|
-
def
|
|
46
|
-
|
|
43
|
+
def __init__(self, *, fn: FunctionT) -> None:
|
|
44
|
+
self.fn = fn
|
|
47
45
|
copy_function_metadata(self, self.fn)
|
|
48
46
|
|
|
49
47
|
|
|
50
|
-
@dataclass
|
|
51
48
|
class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
52
49
|
"""A prompt that can be called with a model to generate a response.
|
|
53
50
|
|
|
@@ -64,6 +61,17 @@ class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
|
64
61
|
format: FormatSpec[FormattableT] | None
|
|
65
62
|
"""The response format for the generated response."""
|
|
66
63
|
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
fn: MessageTemplate[P],
|
|
68
|
+
toolkit: Toolkit,
|
|
69
|
+
format: FormatSpec[FormattableT] | None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.toolkit = toolkit
|
|
72
|
+
self.format = format
|
|
73
|
+
super().__init__(fn=fn)
|
|
74
|
+
|
|
67
75
|
def messages(self, *args: P.args, **kwargs: P.kwargs) -> Sequence[Message]:
|
|
68
76
|
"""Return the `Messages` from invoking this prompt."""
|
|
69
77
|
return promote_to_messages(self.fn(*args, **kwargs))
|
|
@@ -141,7 +149,6 @@ class Prompt(BasePrompt[MessageTemplate[P]], Generic[P, FormattableT]):
|
|
|
141
149
|
return model.stream(messages, tools=self.toolkit, format=self.format)
|
|
142
150
|
|
|
143
151
|
|
|
144
|
-
@dataclass
|
|
145
152
|
class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT]):
|
|
146
153
|
"""An async prompt that can be called with a model to generate a response.
|
|
147
154
|
|
|
@@ -158,6 +165,17 @@ class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT])
|
|
|
158
165
|
format: FormatSpec[FormattableT] | None
|
|
159
166
|
"""The response format for the generated response."""
|
|
160
167
|
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
fn: AsyncMessageTemplate[P],
|
|
172
|
+
toolkit: AsyncToolkit,
|
|
173
|
+
format: FormatSpec[FormattableT] | None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self.toolkit = toolkit
|
|
176
|
+
self.format = format
|
|
177
|
+
super().__init__(fn=fn)
|
|
178
|
+
|
|
161
179
|
async def messages(self, *args: P.args, **kwargs: P.kwargs) -> Sequence[Message]:
|
|
162
180
|
"""Return the `Messages` from invoking this prompt."""
|
|
163
181
|
return promote_to_messages(await self.fn(*args, **kwargs))
|
|
@@ -237,7 +255,6 @@ class AsyncPrompt(BasePrompt[AsyncMessageTemplate[P]], Generic[P, FormattableT])
|
|
|
237
255
|
)
|
|
238
256
|
|
|
239
257
|
|
|
240
|
-
@dataclass
|
|
241
258
|
class ContextPrompt(
|
|
242
259
|
BasePrompt[ContextMessageTemplate[P, DepsT]], Generic[P, DepsT, FormattableT]
|
|
243
260
|
):
|
|
@@ -257,6 +274,17 @@ class ContextPrompt(
|
|
|
257
274
|
format: FormatSpec[FormattableT] | None
|
|
258
275
|
"""The response format for the generated response."""
|
|
259
276
|
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
fn: ContextMessageTemplate[P, DepsT],
|
|
281
|
+
toolkit: ContextToolkit[DepsT],
|
|
282
|
+
format: FormatSpec[FormattableT] | None,
|
|
283
|
+
) -> None:
|
|
284
|
+
self.toolkit = toolkit
|
|
285
|
+
self.format = format
|
|
286
|
+
super().__init__(fn=fn)
|
|
287
|
+
|
|
260
288
|
def messages(
|
|
261
289
|
self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
|
|
262
290
|
) -> Sequence[Message]:
|
|
@@ -360,7 +388,6 @@ class ContextPrompt(
|
|
|
360
388
|
)
|
|
361
389
|
|
|
362
390
|
|
|
363
|
-
@dataclass
|
|
364
391
|
class AsyncContextPrompt(
|
|
365
392
|
BasePrompt[AsyncContextMessageTemplate[P, DepsT]], Generic[P, DepsT, FormattableT]
|
|
366
393
|
):
|
|
@@ -380,6 +407,17 @@ class AsyncContextPrompt(
|
|
|
380
407
|
format: FormatSpec[FormattableT] | None
|
|
381
408
|
"""The response format for the generated response."""
|
|
382
409
|
|
|
410
|
+
def __init__(
|
|
411
|
+
self,
|
|
412
|
+
*,
|
|
413
|
+
fn: AsyncContextMessageTemplate[P, DepsT],
|
|
414
|
+
toolkit: AsyncContextToolkit[DepsT],
|
|
415
|
+
format: FormatSpec[FormattableT] | None,
|
|
416
|
+
) -> None:
|
|
417
|
+
self.toolkit = toolkit
|
|
418
|
+
self.format = format
|
|
419
|
+
super().__init__(fn=fn)
|
|
420
|
+
|
|
383
421
|
async def messages(
|
|
384
422
|
self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
|
|
385
423
|
) -> Sequence[Message]:
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Generic, overload
|
|
|
6
6
|
|
|
7
7
|
from ..content import ToolOutput
|
|
8
8
|
from ..context import Context, DepsT
|
|
9
|
+
from ..exceptions import ParseError
|
|
9
10
|
from ..formatting import Format, FormattableT
|
|
10
11
|
from ..messages import AssistantMessage, Message, UserContent
|
|
11
12
|
from ..tools import (
|
|
@@ -100,6 +101,56 @@ class Response(BaseResponse[Toolkit, FormattableT]):
|
|
|
100
101
|
content=content,
|
|
101
102
|
)
|
|
102
103
|
|
|
104
|
+
@overload
|
|
105
|
+
def validate(
|
|
106
|
+
self: "Response[None]", max_retries: int = 1
|
|
107
|
+
) -> tuple[None, "Response[None]"]: ...
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def validate(
|
|
111
|
+
self: "Response[FormattableT]", max_retries: int = 1
|
|
112
|
+
) -> tuple[FormattableT, "Response[FormattableT]"]: ...
|
|
113
|
+
|
|
114
|
+
def validate(
|
|
115
|
+
self, max_retries: int = 1
|
|
116
|
+
) -> tuple[FormattableT | None, "Response[FormattableT] | Response[None]"]:
|
|
117
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
118
|
+
|
|
119
|
+
Attempts to parse the response. On `ParseError`, asks the LLM to fix its
|
|
120
|
+
output by resuming with the error message. Returns both the parsed value
|
|
121
|
+
and the (potentially updated) response.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
125
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
129
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
130
|
+
(value, new_response) where new_response is the final successful response.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ValueError: If max_retries is negative.
|
|
134
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
135
|
+
Error: If the LLM call fails while generating a retry response.
|
|
136
|
+
"""
|
|
137
|
+
if max_retries < 0:
|
|
138
|
+
raise ValueError("max_retries must be non-negative")
|
|
139
|
+
|
|
140
|
+
if self.format is None:
|
|
141
|
+
return None, self
|
|
142
|
+
|
|
143
|
+
current_response: Response[FormattableT] = self
|
|
144
|
+
for attempt in range(max_retries + 1):
|
|
145
|
+
try:
|
|
146
|
+
return current_response.parse(), current_response
|
|
147
|
+
except ParseError as e:
|
|
148
|
+
if attempt == max_retries:
|
|
149
|
+
raise
|
|
150
|
+
current_response = current_response.resume(e.retry_message())
|
|
151
|
+
|
|
152
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
153
|
+
|
|
103
154
|
|
|
104
155
|
class AsyncResponse(BaseResponse[AsyncToolkit, FormattableT]):
|
|
105
156
|
"""The response generated by an LLM in async mode."""
|
|
@@ -180,6 +231,58 @@ class AsyncResponse(BaseResponse[AsyncToolkit, FormattableT]):
|
|
|
180
231
|
content=content,
|
|
181
232
|
)
|
|
182
233
|
|
|
234
|
+
@overload
|
|
235
|
+
async def validate(
|
|
236
|
+
self: "AsyncResponse[None]", max_retries: int = 1
|
|
237
|
+
) -> tuple[None, "AsyncResponse[None]"]: ...
|
|
238
|
+
|
|
239
|
+
@overload
|
|
240
|
+
async def validate(
|
|
241
|
+
self: "AsyncResponse[FormattableT]", max_retries: int = 1
|
|
242
|
+
) -> tuple[FormattableT, "AsyncResponse[FormattableT]"]: ...
|
|
243
|
+
|
|
244
|
+
async def validate(
|
|
245
|
+
self, max_retries: int = 1
|
|
246
|
+
) -> tuple[
|
|
247
|
+
FormattableT | None, "AsyncResponse[FormattableT] | AsyncResponse[None]"
|
|
248
|
+
]:
|
|
249
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
250
|
+
|
|
251
|
+
Attempts to parse the response. On `ParseError`, asks the LLM to fix its
|
|
252
|
+
output by resuming with the error message. Returns both the parsed value
|
|
253
|
+
and the (potentially updated) response.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
257
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
261
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
262
|
+
(value, new_response) where new_response is the final successful response.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ValueError: If max_retries is negative.
|
|
266
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
267
|
+
Error: If the LLM call fails while generating a retry response.
|
|
268
|
+
"""
|
|
269
|
+
if max_retries < 0:
|
|
270
|
+
raise ValueError("max_retries must be non-negative")
|
|
271
|
+
|
|
272
|
+
if self.format is None:
|
|
273
|
+
return None, self
|
|
274
|
+
|
|
275
|
+
current_response: AsyncResponse[FormattableT] = self
|
|
276
|
+
for attempt in range(max_retries + 1):
|
|
277
|
+
try:
|
|
278
|
+
return current_response.parse(), current_response
|
|
279
|
+
except ParseError as e:
|
|
280
|
+
if attempt == max_retries:
|
|
281
|
+
raise
|
|
282
|
+
current_response = await current_response.resume(e.retry_message())
|
|
283
|
+
|
|
284
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
285
|
+
|
|
183
286
|
|
|
184
287
|
class ContextResponse(
|
|
185
288
|
BaseResponse[ContextToolkit[DepsT], FormattableT], Generic[DepsT, FormattableT]
|
|
@@ -268,6 +371,62 @@ class ContextResponse(
|
|
|
268
371
|
content=content,
|
|
269
372
|
)
|
|
270
373
|
|
|
374
|
+
@overload
|
|
375
|
+
def validate(
|
|
376
|
+
self: "ContextResponse[DepsT, None]", ctx: Context[DepsT], max_retries: int = 1
|
|
377
|
+
) -> tuple[None, "ContextResponse[DepsT, None]"]: ...
|
|
378
|
+
|
|
379
|
+
@overload
|
|
380
|
+
def validate(
|
|
381
|
+
self: "ContextResponse[DepsT, FormattableT]",
|
|
382
|
+
ctx: Context[DepsT],
|
|
383
|
+
max_retries: int = 1,
|
|
384
|
+
) -> tuple[FormattableT, "ContextResponse[DepsT, FormattableT]"]: ...
|
|
385
|
+
|
|
386
|
+
def validate(
|
|
387
|
+
self, ctx: Context[DepsT], max_retries: int = 1
|
|
388
|
+
) -> tuple[
|
|
389
|
+
FormattableT | None,
|
|
390
|
+
"ContextResponse[DepsT, FormattableT] | ContextResponse[DepsT, None]",
|
|
391
|
+
]:
|
|
392
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
393
|
+
|
|
394
|
+
Attempts to parse the response. On `ParseError`, asks the LLM to fix its
|
|
395
|
+
output by resuming with the error message. Returns both the parsed value
|
|
396
|
+
and the (potentially updated) response.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
ctx: A `Context` with the required deps type.
|
|
400
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
401
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
405
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
406
|
+
(value, new_response) where new_response is the final successful response.
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
ValueError: If max_retries is negative.
|
|
410
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
411
|
+
Error: If the LLM call fails while generating a retry response.
|
|
412
|
+
"""
|
|
413
|
+
if max_retries < 0:
|
|
414
|
+
raise ValueError("max_retries must be non-negative")
|
|
415
|
+
|
|
416
|
+
if self.format is None:
|
|
417
|
+
return None, self
|
|
418
|
+
|
|
419
|
+
current_response: ContextResponse[DepsT, FormattableT] = self
|
|
420
|
+
for attempt in range(max_retries + 1):
|
|
421
|
+
try:
|
|
422
|
+
return current_response.parse(), current_response
|
|
423
|
+
except ParseError as e:
|
|
424
|
+
if attempt == max_retries:
|
|
425
|
+
raise
|
|
426
|
+
current_response = current_response.resume(ctx, e.retry_message())
|
|
427
|
+
|
|
428
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
429
|
+
|
|
271
430
|
|
|
272
431
|
class AsyncContextResponse(
|
|
273
432
|
BaseResponse[AsyncContextToolkit[DepsT], FormattableT], Generic[DepsT, FormattableT]
|
|
@@ -360,3 +519,61 @@ class AsyncContextResponse(
|
|
|
360
519
|
response=self,
|
|
361
520
|
content=content,
|
|
362
521
|
)
|
|
522
|
+
|
|
523
|
+
@overload
|
|
524
|
+
async def validate(
|
|
525
|
+
self: "AsyncContextResponse[DepsT, None]",
|
|
526
|
+
ctx: Context[DepsT],
|
|
527
|
+
max_retries: int = 1,
|
|
528
|
+
) -> tuple[None, "AsyncContextResponse[DepsT, None]"]: ...
|
|
529
|
+
|
|
530
|
+
@overload
|
|
531
|
+
async def validate(
|
|
532
|
+
self: "AsyncContextResponse[DepsT, FormattableT]",
|
|
533
|
+
ctx: Context[DepsT],
|
|
534
|
+
max_retries: int = 1,
|
|
535
|
+
) -> tuple[FormattableT, "AsyncContextResponse[DepsT, FormattableT]"]: ...
|
|
536
|
+
|
|
537
|
+
async def validate(
|
|
538
|
+
self, ctx: Context[DepsT], max_retries: int = 1
|
|
539
|
+
) -> tuple[
|
|
540
|
+
FormattableT | None,
|
|
541
|
+
"AsyncContextResponse[DepsT, FormattableT] | AsyncContextResponse[DepsT, None]",
|
|
542
|
+
]:
|
|
543
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
544
|
+
|
|
545
|
+
Attempts to parse the response. On `ParseError`, asks the LLM to fix its
|
|
546
|
+
output by resuming with the error message. Returns both the parsed value
|
|
547
|
+
and the (potentially updated) response.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
ctx: A `Context` with the required deps type.
|
|
551
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
552
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
556
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
557
|
+
(value, new_response) where new_response is the final successful response.
|
|
558
|
+
|
|
559
|
+
Raises:
|
|
560
|
+
ValueError: If max_retries is negative.
|
|
561
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
562
|
+
Error: If the LLM call fails while generating a retry response.
|
|
563
|
+
"""
|
|
564
|
+
if max_retries < 0:
|
|
565
|
+
raise ValueError("max_retries must be non-negative")
|
|
566
|
+
|
|
567
|
+
if self.format is None:
|
|
568
|
+
return None, self
|
|
569
|
+
|
|
570
|
+
current_response: AsyncContextResponse[DepsT, FormattableT] = self
|
|
571
|
+
for attempt in range(max_retries + 1):
|
|
572
|
+
try:
|
|
573
|
+
return current_response.parse(), current_response
|
|
574
|
+
except ParseError as e:
|
|
575
|
+
if attempt == max_retries:
|
|
576
|
+
raise
|
|
577
|
+
current_response = await current_response.resume(ctx, e.retry_message())
|
|
578
|
+
|
|
579
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Generic, overload
|
|
|
6
6
|
|
|
7
7
|
from ..content import ToolOutput
|
|
8
8
|
from ..context import Context, DepsT
|
|
9
|
+
from ..exceptions import ParseError
|
|
9
10
|
from ..formatting import Format, FormattableT
|
|
10
11
|
from ..messages import Message, UserContent
|
|
11
12
|
from ..tools import (
|
|
@@ -156,6 +157,61 @@ class StreamResponse(BaseSyncStreamResponse[Toolkit, FormattableT]):
|
|
|
156
157
|
content=content,
|
|
157
158
|
)
|
|
158
159
|
|
|
160
|
+
@overload
|
|
161
|
+
def validate(
|
|
162
|
+
self: "StreamResponse[None]", max_retries: int = 1
|
|
163
|
+
) -> tuple[None, "StreamResponse[None]"]: ...
|
|
164
|
+
|
|
165
|
+
@overload
|
|
166
|
+
def validate(
|
|
167
|
+
self: "StreamResponse[FormattableT]", max_retries: int = 1
|
|
168
|
+
) -> tuple[FormattableT, "StreamResponse[FormattableT]"]: ...
|
|
169
|
+
|
|
170
|
+
def validate(
|
|
171
|
+
self, max_retries: int = 1
|
|
172
|
+
) -> tuple[
|
|
173
|
+
FormattableT | None, "StreamResponse[FormattableT] | StreamResponse[None]"
|
|
174
|
+
]:
|
|
175
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
176
|
+
|
|
177
|
+
Consumes the stream (calls `finish()`) and then attempts to parse. On
|
|
178
|
+
`ParseError`, asks the LLM to fix its output by resuming with the error
|
|
179
|
+
message. Returns both the parsed value and the (potentially updated) response.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
183
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
187
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
188
|
+
(value, new_response) where new_response is the final successful response.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If max_retries is negative.
|
|
192
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
193
|
+
Error: If the LLM call fails while generating a retry response.
|
|
194
|
+
"""
|
|
195
|
+
if max_retries < 0:
|
|
196
|
+
raise ValueError("max_retries must be non-negative")
|
|
197
|
+
|
|
198
|
+
self.finish()
|
|
199
|
+
|
|
200
|
+
if self.format is None:
|
|
201
|
+
return None, self
|
|
202
|
+
|
|
203
|
+
current_response: StreamResponse[FormattableT] = self
|
|
204
|
+
for attempt in range(max_retries + 1):
|
|
205
|
+
try:
|
|
206
|
+
return current_response.parse(), current_response
|
|
207
|
+
except ParseError as e:
|
|
208
|
+
if attempt == max_retries:
|
|
209
|
+
raise
|
|
210
|
+
current_response = current_response.resume(e.retry_message())
|
|
211
|
+
current_response.finish()
|
|
212
|
+
|
|
213
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
214
|
+
|
|
159
215
|
|
|
160
216
|
class AsyncStreamResponse(BaseAsyncStreamResponse[AsyncToolkit, FormattableT]):
|
|
161
217
|
"""An `AsyncStreamResponse` wraps response content from the LLM with a streaming interface.
|
|
@@ -287,6 +343,62 @@ class AsyncStreamResponse(BaseAsyncStreamResponse[AsyncToolkit, FormattableT]):
|
|
|
287
343
|
content=content,
|
|
288
344
|
)
|
|
289
345
|
|
|
346
|
+
@overload
|
|
347
|
+
async def validate(
|
|
348
|
+
self: "AsyncStreamResponse[None]", max_retries: int = 1
|
|
349
|
+
) -> tuple[None, "AsyncStreamResponse[None]"]: ...
|
|
350
|
+
|
|
351
|
+
@overload
|
|
352
|
+
async def validate(
|
|
353
|
+
self: "AsyncStreamResponse[FormattableT]", max_retries: int = 1
|
|
354
|
+
) -> tuple[FormattableT, "AsyncStreamResponse[FormattableT]"]: ...
|
|
355
|
+
|
|
356
|
+
async def validate(
|
|
357
|
+
self, max_retries: int = 1
|
|
358
|
+
) -> tuple[
|
|
359
|
+
FormattableT | None,
|
|
360
|
+
"AsyncStreamResponse[FormattableT] | AsyncStreamResponse[None]",
|
|
361
|
+
]:
|
|
362
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
363
|
+
|
|
364
|
+
Consumes the stream (calls `finish()`) and then attempts to parse. On
|
|
365
|
+
`ParseError`, asks the LLM to fix its output by resuming with the error
|
|
366
|
+
message. Returns both the parsed value and the (potentially updated) response.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
370
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
374
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
375
|
+
(value, new_response) where new_response is the final successful response.
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
ValueError: If max_retries is negative.
|
|
379
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
380
|
+
Error: If the LLM call fails while generating a retry response.
|
|
381
|
+
"""
|
|
382
|
+
if max_retries < 0:
|
|
383
|
+
raise ValueError("max_retries must be non-negative")
|
|
384
|
+
|
|
385
|
+
await self.finish()
|
|
386
|
+
|
|
387
|
+
if self.format is None:
|
|
388
|
+
return None, self
|
|
389
|
+
|
|
390
|
+
current_response: AsyncStreamResponse[FormattableT] = self
|
|
391
|
+
for attempt in range(max_retries + 1):
|
|
392
|
+
try:
|
|
393
|
+
return current_response.parse(), current_response
|
|
394
|
+
except ParseError as e:
|
|
395
|
+
if attempt == max_retries:
|
|
396
|
+
raise
|
|
397
|
+
current_response = await current_response.resume(e.retry_message())
|
|
398
|
+
await current_response.finish()
|
|
399
|
+
|
|
400
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
401
|
+
|
|
290
402
|
|
|
291
403
|
class ContextStreamResponse(
|
|
292
404
|
BaseSyncStreamResponse[ContextToolkit[DepsT], FormattableT],
|
|
@@ -428,6 +540,67 @@ class ContextStreamResponse(
|
|
|
428
540
|
content=content,
|
|
429
541
|
)
|
|
430
542
|
|
|
543
|
+
@overload
|
|
544
|
+
def validate(
|
|
545
|
+
self: "ContextStreamResponse[DepsT, None]",
|
|
546
|
+
ctx: Context[DepsT],
|
|
547
|
+
max_retries: int = 1,
|
|
548
|
+
) -> tuple[None, "ContextStreamResponse[DepsT, None]"]: ...
|
|
549
|
+
|
|
550
|
+
@overload
|
|
551
|
+
def validate(
|
|
552
|
+
self: "ContextStreamResponse[DepsT, FormattableT]",
|
|
553
|
+
ctx: Context[DepsT],
|
|
554
|
+
max_retries: int = 1,
|
|
555
|
+
) -> tuple[FormattableT, "ContextStreamResponse[DepsT, FormattableT]"]: ...
|
|
556
|
+
|
|
557
|
+
def validate(
|
|
558
|
+
self, ctx: Context[DepsT], max_retries: int = 1
|
|
559
|
+
) -> tuple[
|
|
560
|
+
FormattableT | None,
|
|
561
|
+
"ContextStreamResponse[DepsT, FormattableT] | ContextStreamResponse[DepsT, None]",
|
|
562
|
+
]:
|
|
563
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
564
|
+
|
|
565
|
+
Consumes the stream (calls `finish()`) and then attempts to parse. On
|
|
566
|
+
`ParseError`, asks the LLM to fix its output by resuming with the error
|
|
567
|
+
message. Returns both the parsed value and the (potentially updated) response.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
ctx: A `Context` with the required deps type.
|
|
571
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
572
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
576
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
577
|
+
(value, new_response) where new_response is the final successful response.
|
|
578
|
+
|
|
579
|
+
Raises:
|
|
580
|
+
ValueError: If max_retries is negative.
|
|
581
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
582
|
+
Error: If the LLM call fails while generating a retry response.
|
|
583
|
+
"""
|
|
584
|
+
if max_retries < 0:
|
|
585
|
+
raise ValueError("max_retries must be non-negative")
|
|
586
|
+
|
|
587
|
+
self.finish()
|
|
588
|
+
|
|
589
|
+
if self.format is None:
|
|
590
|
+
return None, self
|
|
591
|
+
|
|
592
|
+
current_response: ContextStreamResponse[DepsT, FormattableT] = self
|
|
593
|
+
for attempt in range(max_retries + 1):
|
|
594
|
+
try:
|
|
595
|
+
return current_response.parse(), current_response
|
|
596
|
+
except ParseError as e:
|
|
597
|
+
if attempt == max_retries:
|
|
598
|
+
raise
|
|
599
|
+
current_response = current_response.resume(ctx, e.retry_message())
|
|
600
|
+
current_response.finish()
|
|
601
|
+
|
|
602
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
603
|
+
|
|
431
604
|
|
|
432
605
|
class AsyncContextStreamResponse(
|
|
433
606
|
BaseAsyncStreamResponse[AsyncContextToolkit[DepsT], FormattableT],
|
|
@@ -575,3 +748,64 @@ class AsyncContextStreamResponse(
|
|
|
575
748
|
response=self,
|
|
576
749
|
content=content,
|
|
577
750
|
)
|
|
751
|
+
|
|
752
|
+
@overload
|
|
753
|
+
async def validate(
|
|
754
|
+
self: "AsyncContextStreamResponse[DepsT, None]",
|
|
755
|
+
ctx: Context[DepsT],
|
|
756
|
+
max_retries: int = 1,
|
|
757
|
+
) -> tuple[None, "AsyncContextStreamResponse[DepsT, None]"]: ...
|
|
758
|
+
|
|
759
|
+
@overload
|
|
760
|
+
async def validate(
|
|
761
|
+
self: "AsyncContextStreamResponse[DepsT, FormattableT]",
|
|
762
|
+
ctx: Context[DepsT],
|
|
763
|
+
max_retries: int = 1,
|
|
764
|
+
) -> tuple[FormattableT, "AsyncContextStreamResponse[DepsT, FormattableT]"]: ...
|
|
765
|
+
|
|
766
|
+
async def validate(
|
|
767
|
+
self, ctx: Context[DepsT], max_retries: int = 1
|
|
768
|
+
) -> tuple[
|
|
769
|
+
FormattableT | None,
|
|
770
|
+
"AsyncContextStreamResponse[DepsT, FormattableT] | AsyncContextStreamResponse[DepsT, None]",
|
|
771
|
+
]:
|
|
772
|
+
"""Parse and validate the response, retrying on parse errors.
|
|
773
|
+
|
|
774
|
+
Consumes the stream (calls `finish()`) and then attempts to parse. On
|
|
775
|
+
`ParseError`, asks the LLM to fix its output by resuming with the error
|
|
776
|
+
message. Returns both the parsed value and the (potentially updated) response.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
ctx: A `Context` with the required deps type.
|
|
780
|
+
max_retries: Maximum number of retry attempts on parse failure.
|
|
781
|
+
Defaults to 1 (2 total attempts). Must be non-negative.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
A tuple of (parsed_value, response). If parsing succeeded on the first
|
|
785
|
+
attempt, returns (value, self). If retries were needed, returns
|
|
786
|
+
(value, new_response) where new_response is the final successful response.
|
|
787
|
+
|
|
788
|
+
Raises:
|
|
789
|
+
ValueError: If max_retries is negative.
|
|
790
|
+
ParseError: If parsing fails after exhausting all retry attempts.
|
|
791
|
+
Error: If the LLM call fails while generating a retry response.
|
|
792
|
+
"""
|
|
793
|
+
if max_retries < 0:
|
|
794
|
+
raise ValueError("max_retries must be non-negative")
|
|
795
|
+
|
|
796
|
+
await self.finish()
|
|
797
|
+
|
|
798
|
+
if self.format is None:
|
|
799
|
+
return None, self
|
|
800
|
+
|
|
801
|
+
current_response: AsyncContextStreamResponse[DepsT, FormattableT] = self
|
|
802
|
+
for attempt in range(max_retries + 1):
|
|
803
|
+
try:
|
|
804
|
+
return current_response.parse(), current_response
|
|
805
|
+
except ParseError as e:
|
|
806
|
+
if attempt == max_retries:
|
|
807
|
+
raise
|
|
808
|
+
current_response = await current_response.resume(ctx, e.retry_message())
|
|
809
|
+
await current_response.finish()
|
|
810
|
+
|
|
811
|
+
raise AssertionError("Unreachable") # pragma: no cover
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Retry functionality for reliable LLM interactions.
|
|
2
|
+
|
|
3
|
+
This module provides retry capabilities for LLM calls, including:
|
|
4
|
+
- Automatic retry on failures (connection errors, rate limits, etc.)
|
|
5
|
+
- Configurable retry strategies and backoff
|
|
6
|
+
- Fallback models
|
|
7
|
+
- Retry metadata tracking
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .retry_calls import AsyncRetryCall, BaseRetryCall, RetryCall
|
|
11
|
+
from .retry_config import RetryArgs, RetryConfig
|
|
12
|
+
from .retry_decorator import retry
|
|
13
|
+
from .retry_models import RetryModel, RetryModelParams, retry_model
|
|
14
|
+
from .retry_prompts import AsyncRetryPrompt, BaseRetryPrompt, RetryPrompt
|
|
15
|
+
from .retry_responses import (
|
|
16
|
+
AsyncContextRetryResponse,
|
|
17
|
+
AsyncRetryResponse,
|
|
18
|
+
ContextRetryResponse,
|
|
19
|
+
RetryResponse,
|
|
20
|
+
)
|
|
21
|
+
from .retry_stream_responses import (
|
|
22
|
+
AsyncContextRetryStreamResponse,
|
|
23
|
+
AsyncRetryStreamResponse,
|
|
24
|
+
ContextRetryStreamResponse,
|
|
25
|
+
RetryStreamResponse,
|
|
26
|
+
)
|
|
27
|
+
from .utils import RetryFailure
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AsyncContextRetryResponse",
|
|
31
|
+
"AsyncContextRetryStreamResponse",
|
|
32
|
+
"AsyncRetryCall",
|
|
33
|
+
"AsyncRetryPrompt",
|
|
34
|
+
"AsyncRetryResponse",
|
|
35
|
+
"AsyncRetryStreamResponse",
|
|
36
|
+
"BaseRetryCall",
|
|
37
|
+
"BaseRetryPrompt",
|
|
38
|
+
"ContextRetryResponse",
|
|
39
|
+
"ContextRetryStreamResponse",
|
|
40
|
+
"RetryArgs",
|
|
41
|
+
"RetryCall",
|
|
42
|
+
"RetryConfig",
|
|
43
|
+
"RetryFailure",
|
|
44
|
+
"RetryModel",
|
|
45
|
+
"RetryModelParams",
|
|
46
|
+
"RetryPrompt",
|
|
47
|
+
"RetryResponse",
|
|
48
|
+
"RetryStreamResponse",
|
|
49
|
+
"retry",
|
|
50
|
+
"retry_model",
|
|
51
|
+
]
|