Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 5,514 Bytes
3530326 270d701 3530326 52d25f7 270d701 52d25f7 3719fca 3530326 52d25f7 3530326 52d25f7 3530326 52d25f7 3530326 52d25f7 3530326 3719fca 3530326 270d701 3530326 3719fca 3530326 3719fca 3530326 270d701 3530326 270d701 3530326 270d701 3530326 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | from __future__ import annotations
import base64
import os
import time
from dataclasses import dataclass
import requests
from parser_output import shape_parse_response
DEFAULT_ENVIRONMENT = "staging"
ENVIRONMENT_CONFIG = {
"production": ("COHERE_API_KEY", "https://api.cohere.com/v2/parse"),
"staging": ("COHERE_STG_API_KEY", "https://stg.api.cohere.ai/v2/parse"),
}
DEFAULT_MODEL = "parse-v5.0"
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
class ParserServiceError(RuntimeError):
"""A safe parser-service error that may be displayed to a user."""
def selected_environment() -> str:
environment = os.environ.get("COHERE_ENV", DEFAULT_ENVIRONMENT).strip().lower()
if environment not in ENVIRONMENT_CONFIG:
raise ParserServiceError(
"COHERE_ENV must be either 'staging' or 'production'."
)
return environment
def api_key_name_for_environment(environment: str | None = None) -> str:
selected = environment or selected_environment()
try:
return ENVIRONMENT_CONFIG[selected][0]
except KeyError as error:
raise ParserServiceError(
"COHERE_ENV must be either 'staging' or 'production'."
) from error
@dataclass(frozen=True)
class ParserConfig:
api_url: str
api_key: str
model: str
timeout: float
max_retries: int = 4
@classmethod
def from_environment(cls) -> "ParserConfig":
environment = selected_environment()
api_key_name, default_api_url = ENVIRONMENT_CONFIG[environment]
api_key = os.environ.get(api_key_name, "").strip()
if not api_key:
raise ParserServiceError(
f"Cohere Parse is not configured for {environment}."
)
return cls(
api_url=os.environ.get("COHERE_API_URL", "").strip()
or default_api_url,
api_key=api_key,
model=os.environ.get("COHERE_MODEL", DEFAULT_MODEL).strip(),
timeout=float(os.environ.get("COHERE_TIMEOUT", "300")),
)
def build_payload(model: str, png: bytes) -> dict:
encoded = base64.b64encode(png).decode("ascii")
return {
"model": model,
"document": {
"type": "image_url",
"image_url": f"data:image/png;base64,{encoded}",
},
"output_format": "blocks",
}
class ParserClient:
def __init__(
self,
config: ParserConfig | None = None,
*,
session: requests.Session | None = None,
sleeper=time.sleep,
) -> None:
self.config = config or ParserConfig.from_environment()
self.session = session or requests.Session()
self.sleeper = sleeper
def _retry_delay(self, response: requests.Response, attempt: int) -> float:
header = response.headers.get("Retry-After", "")
try:
return min(60.0, max(0.0, float(header)))
except ValueError:
return min(60.0, 2.0 ** (attempt + 1))
def parse_page(self, png: bytes) -> dict:
started = time.perf_counter()
headers = {
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {self.config.api_key}",
}
payload = build_payload(self.config.model, png)
for attempt in range(self.config.max_retries):
try:
response = self.session.post(
self.config.api_url,
headers=headers,
json=payload,
timeout=self.config.timeout,
)
except (requests.Timeout, requests.ConnectionError) as exc:
if attempt + 1 < self.config.max_retries:
self.sleeper(min(60.0, 2.0 ** (attempt + 1)))
continue
raise ParserServiceError(
"Cohere Parse could not be reached. Try again shortly."
) from exc
request_id = (
response.headers.get("x-request-id")
or response.headers.get("request-id")
)
if response.status_code in RETRYABLE_STATUSES:
if attempt + 1 < self.config.max_retries:
self.sleeper(self._retry_delay(response, attempt))
continue
raise ParserServiceError(
f"Cohere Parse is temporarily unavailable ({response.status_code})."
)
if not response.ok:
raise ParserServiceError(
f"Cohere Parse rejected the request ({response.status_code})."
)
try:
body = response.json()
except requests.JSONDecodeError as exc:
raise ParserServiceError(
"Cohere Parse returned an invalid response."
) from exc
try:
parsed = shape_parse_response(body)
except (TypeError, ValueError) as exc:
raise ParserServiceError(
"Cohere Parse returned an invalid response."
) from exc
return {
**parsed,
"elapsed_ms": round((time.perf_counter() - started) * 1000),
"model": self.config.model,
"usage": body.get("meta"),
"request_id": request_id or body.get("id"),
}
raise ParserServiceError("Cohere Parse is temporarily unavailable.")
|