Skip to content

Commit 44ecbf2

Browse files
authored
Merge pull request #1072 from parea-ai/add-retry-backoff
add broader retry on http client
2 parents 4fd8e40 + 6b6a0ba commit 44ecbf2

File tree

4 files changed

+91
-64
lines changed

4 files changed

+91
-64
lines changed

cookbook/evals_and_experiments/parea_evaluation_deepdive.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@
705705
"\n",
706706
"\n",
707707
"dataset = to_simple_dictionary(comments_df)\n",
708-
"dataset[0]"
708+
"dataset"
709709
]
710710
},
711711
{

cookbook/parea_llm_proxy/deployments/tracing_with_deployed_prompt.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from dotenv import load_dotenv
99

1010
from parea import Parea, get_current_trace_id, trace
11-
from parea.schemas import Completion, CompletionResponse, LLMInputs, Message, Role
11+
from parea.schemas import Completion, CompletionResponse, FeedbackRequest, LLMInputs, Message, Role
1212

1313
load_dotenv()
1414

@@ -101,10 +101,10 @@ def deployed_argument_chain_tags_metadata(query: str, additional_description: st
101101
additional_description="Provide a concise, few sentence argument on why coffee is good for you.",
102102
)
103103
print(json.dumps(asdict(result2), indent=2))
104-
# p.record_feedback(
105-
# FeedbackRequest(
106-
# trace_id=trace_id,
107-
# score=0.7, # 0.0 (bad) to 1.0 (good)
108-
# target="Coffee is wonderful. End of story.",
109-
# )
110-
# )
104+
p.record_feedback(
105+
FeedbackRequest(
106+
trace_id=trace_id,
107+
score=0.7, # 0.0 (bad) to 1.0 (good)
108+
target="Coffee is wonderful. End of story.",
109+
)
110+
)

parea/api_client.py

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,17 @@
1-
from typing import Any, AsyncIterable, Callable, Dict, List, Optional
1+
from typing import Any, AsyncIterable, Dict, List, Optional
22

3-
import asyncio
43
import json
4+
import logging
55
import os
6-
import time
7-
from functools import wraps
86
from importlib import metadata as importlib_metadata
97

108
import httpx
119
from dotenv import load_dotenv
10+
from tenacity import retry, stop_after_attempt, wait_exponential
1211

1312
load_dotenv()
1413

15-
MAX_RETRIES = 8
16-
BACKOFF_FACTOR = 0.5
17-
18-
19-
def retry_on_502(func: Callable[..., Any]) -> Callable[..., Any]:
20-
"""
21-
A decorator to retry a function or coroutine on encountering a 502 error.
22-
Parameters:
23-
- func: The function or coroutine to be decorated.
24-
Returns:
25-
- A wrapper function that incorporates retry logic.
26-
"""
27-
28-
@wraps(func)
29-
async def async_wrapper(*args, **kwargs):
30-
for retry in range(MAX_RETRIES):
31-
try:
32-
return await func(*args, **kwargs)
33-
except httpx.HTTPError as e:
34-
if not _should_retry(e, retry):
35-
raise
36-
await asyncio.sleep(BACKOFF_FACTOR * (2**retry))
37-
38-
@wraps(func)
39-
def sync_wrapper(*args, **kwargs):
40-
for retry in range(MAX_RETRIES):
41-
try:
42-
return func(*args, **kwargs)
43-
except httpx.HTTPError as e:
44-
if not _should_retry(e, retry):
45-
raise
46-
time.sleep(BACKOFF_FACTOR * (2**retry))
47-
48-
def _should_retry(error, current_retry):
49-
"""Determines if the function should retry on error."""
50-
is_502_error = isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 502
51-
is_last_retry = current_retry == MAX_RETRIES - 1
52-
return not is_last_retry and (isinstance(error, (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError)) or is_502_error)
53-
54-
if asyncio.iscoroutinefunction(func):
55-
return async_wrapper
56-
else:
57-
return sync_wrapper
14+
logger = logging.getLogger()
5815

5916

6017
class HTTPClient:
@@ -87,7 +44,7 @@ def _get_headers(self, api_key: Optional[str] = None) -> Dict[str, str]:
8744
headers["x-sdk-integrations"] = ",".join(self.integrations)
8845
return headers
8946

90-
@retry_on_502
47+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10))
9148
def request(
9249
self,
9350
method: str,
@@ -108,9 +65,25 @@ def request(
10865
if e.response.status_code == 422:
10966
# update the error message to include the validation errors
11067
e.args = (f"{e.args[0]}: {e.response.json()}",)
68+
logger.error(
69+
f"HTTP error {e.response.status_code} for {e.request.method} with: {e.args}",
70+
extra={"request_data": data, "request_params": params},
71+
)
72+
raise
73+
except httpx.TimeoutException as e:
74+
logger.error(
75+
f"Timeout error for {e.request.method} {e.request.url}",
76+
extra={"request_data": data, "request_params": params},
77+
)
78+
raise
79+
except httpx.RequestError as e:
80+
logger.error(
81+
f"Request error for {e.request.method} {e.request.url}: {str(e)}",
82+
extra={"request_data": data, "request_params": params},
83+
)
11184
raise
11285

113-
@retry_on_502
86+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10))
11487
async def request_async(
11588
self,
11689
method: str,
@@ -128,10 +101,28 @@ async def request_async(
128101
response.raise_for_status()
129102
return response
130103
except httpx.HTTPStatusError as e:
131-
print(f"HTTP Error {e.response.status_code} for {e.request.url}: {e.response.text}")
104+
if e.response.status_code == 422:
105+
# update the error message to include the validation errors
106+
e.args = (f"{e.args[0]}: {e.response.json()}",)
107+
logger.error(
108+
f"HTTP error {e.response.status_code} for {e.request.method} with: {e.args}",
109+
extra={"request_data": data, "request_params": params},
110+
)
111+
raise
112+
except httpx.TimeoutException as e:
113+
logger.error(
114+
f"Timeout error for {e.request.method} {e.request.url}",
115+
extra={"request_data": data, "request_params": params},
116+
)
117+
raise
118+
except httpx.RequestError as e:
119+
logger.error(
120+
f"Request error for {e.request.method} {e.request.url}: {str(e)}",
121+
extra={"request_data": data, "request_params": params},
122+
)
132123
raise
133124

134-
@retry_on_502
125+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10))
135126
def stream_request(
136127
self,
137128
method: str,
@@ -151,10 +142,28 @@ def stream_request(
151142
for chunk in response.iter_bytes(chunk_size):
152143
yield parse_event_data(chunk)
153144
except httpx.HTTPStatusError as e:
154-
print(f"HTTP Error {e.response.status_code} for {e.request.url}: {e.response.text}")
145+
if e.response.status_code == 422:
146+
# update the error message to include the validation errors
147+
e.args = (f"{e.args[0]}: {e.response.json()}",)
148+
logger.error(
149+
f"HTTP error {e.response.status_code} for {e.request.method} with: {e.args}",
150+
extra={"request_data": data, "request_params": params},
151+
)
152+
raise
153+
except httpx.TimeoutException as e:
154+
logger.error(
155+
f"Timeout error for {e.request.method} {e.request.url}",
156+
extra={"request_data": data, "request_params": params},
157+
)
158+
raise
159+
except httpx.RequestError as e:
160+
logger.error(
161+
f"Request error for {e.request.method} {e.request.url}: {str(e)}",
162+
extra={"request_data": data, "request_params": params},
163+
)
155164
raise
156165

157-
@retry_on_502
166+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10))
158167
async def stream_request_async(
159168
self,
160169
method: str,
@@ -174,7 +183,25 @@ async def stream_request_async(
174183
async for chunk in response.aiter_bytes(chunk_size):
175184
yield parse_event_data(chunk)
176185
except httpx.HTTPStatusError as e:
177-
print(f"HTTP Error {e.response.status_code} for {e.request.url}: {e.response.text}")
186+
if e.response.status_code == 422:
187+
# update the error message to include the validation errors
188+
e.args = (f"{e.args[0]}: {e.response.json()}",)
189+
logger.error(
190+
f"HTTP error {e.response.status_code} for {e.request.method} with: {e.args}",
191+
extra={"request_data": data, "request_params": params},
192+
)
193+
raise
194+
except httpx.TimeoutException as e:
195+
logger.error(
196+
f"Timeout error for {e.request.method} {e.request.url}",
197+
extra={"request_data": data, "request_params": params},
198+
)
199+
raise
200+
except httpx.RequestError as e:
201+
logger.error(
202+
f"Request error for {e.request.method} {e.request.url}: {str(e)}",
203+
extra={"request_data": data, "request_params": params},
204+
)
178205
raise
179206

180207
def close(self):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
66
[tool.poetry]
77
name = "parea-ai"
88
packages = [{ include = "parea" }]
9-
version = "0.2.209"
9+
version = "0.2.210"
1010
description = "Parea python sdk"
1111
readme = "README.md"
1212
authors = ["joel-parea-ai <[email protected]>"]

0 commit comments

Comments
 (0)