lionagi 0.12.2__py3-none-any.whl → 0.12.4__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.
- lionagi/config.py +123 -0
- lionagi/fields/file.py +1 -1
- lionagi/fields/reason.py +1 -1
- lionagi/libs/file/concat.py +1 -6
- lionagi/libs/file/concat_files.py +1 -5
- lionagi/libs/file/save.py +1 -1
- lionagi/libs/package/imports.py +8 -177
- lionagi/libs/parse.py +30 -0
- lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
- lionagi/libs/token_transform/perplexity.py +2 -4
- lionagi/libs/token_transform/synthlang_/resources/frameworks/framework_options.json +46 -46
- lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
- lionagi/operations/chat/chat.py +2 -2
- lionagi/operations/communicate/communicate.py +20 -5
- lionagi/operations/parse/parse.py +131 -43
- lionagi/protocols/generic/log.py +1 -2
- lionagi/protocols/generic/pile.py +18 -4
- lionagi/protocols/messages/assistant_response.py +20 -1
- lionagi/protocols/messages/templates/README.md +6 -10
- lionagi/service/connections/__init__.py +15 -0
- lionagi/service/connections/api_calling.py +230 -0
- lionagi/service/connections/endpoint.py +410 -0
- lionagi/service/connections/endpoint_config.py +137 -0
- lionagi/service/connections/header_factory.py +56 -0
- lionagi/service/connections/match_endpoint.py +49 -0
- lionagi/service/connections/providers/__init__.py +3 -0
- lionagi/service/connections/providers/anthropic_.py +87 -0
- lionagi/service/connections/providers/exa_.py +33 -0
- lionagi/service/connections/providers/oai_.py +166 -0
- lionagi/service/connections/providers/ollama_.py +122 -0
- lionagi/service/connections/providers/perplexity_.py +29 -0
- lionagi/service/imodel.py +36 -144
- lionagi/service/manager.py +1 -7
- lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
- lionagi/service/resilience.py +545 -0
- lionagi/service/third_party/README.md +71 -0
- lionagi/service/third_party/__init__.py +0 -0
- lionagi/service/third_party/anthropic_models.py +159 -0
- lionagi/service/third_party/exa_models.py +165 -0
- lionagi/service/third_party/openai_models.py +18241 -0
- lionagi/service/third_party/pplx_models.py +156 -0
- lionagi/service/types.py +5 -4
- lionagi/session/branch.py +12 -7
- lionagi/tools/file/reader.py +1 -1
- lionagi/tools/memory/tools.py +497 -0
- lionagi/utils.py +921 -123
- lionagi/version.py +1 -1
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/METADATA +33 -16
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/RECORD +53 -63
- lionagi/libs/file/create_path.py +0 -80
- lionagi/libs/file/file_util.py +0 -358
- lionagi/libs/parse/__init__.py +0 -3
- lionagi/libs/parse/fuzzy_parse_json.py +0 -117
- lionagi/libs/parse/to_dict.py +0 -336
- lionagi/libs/parse/to_json.py +0 -61
- lionagi/libs/parse/to_num.py +0 -378
- lionagi/libs/parse/to_xml.py +0 -57
- lionagi/libs/parse/xml_parser.py +0 -148
- lionagi/libs/schema/breakdown_pydantic_annotation.py +0 -48
- lionagi/service/endpoints/__init__.py +0 -3
- lionagi/service/endpoints/base.py +0 -706
- lionagi/service/endpoints/chat_completion.py +0 -116
- lionagi/service/endpoints/match_endpoint.py +0 -72
- lionagi/service/providers/__init__.py +0 -3
- lionagi/service/providers/anthropic_/__init__.py +0 -3
- lionagi/service/providers/anthropic_/messages.py +0 -99
- lionagi/service/providers/exa_/models.py +0 -3
- lionagi/service/providers/exa_/search.py +0 -80
- lionagi/service/providers/exa_/types.py +0 -7
- lionagi/service/providers/groq_/__init__.py +0 -3
- lionagi/service/providers/groq_/chat_completions.py +0 -56
- lionagi/service/providers/ollama_/__init__.py +0 -3
- lionagi/service/providers/ollama_/chat_completions.py +0 -134
- lionagi/service/providers/openai_/__init__.py +0 -3
- lionagi/service/providers/openai_/chat_completions.py +0 -101
- lionagi/service/providers/openai_/spec.py +0 -14
- lionagi/service/providers/openrouter_/__init__.py +0 -3
- lionagi/service/providers/openrouter_/chat_completions.py +0 -62
- lionagi/service/providers/perplexity_/__init__.py +0 -3
- lionagi/service/providers/perplexity_/chat_completions.py +0 -44
- lionagi/service/providers/perplexity_/models.py +0 -5
- lionagi/service/providers/types.py +0 -17
- /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
- /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
- {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,706 +0,0 @@
|
|
1
|
-
# Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
import json
|
7
|
-
import logging
|
8
|
-
from abc import ABC
|
9
|
-
from collections.abc import AsyncGenerator
|
10
|
-
from typing import Any, Literal
|
11
|
-
|
12
|
-
import aiohttp
|
13
|
-
from aiocache import cached
|
14
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
15
|
-
from typing_extensions import Self
|
16
|
-
|
17
|
-
from lionagi._errors import ExecutionError, RateLimitError
|
18
|
-
from lionagi.protocols.types import Event, EventStatus
|
19
|
-
from lionagi.settings import Settings
|
20
|
-
|
21
|
-
from .token_calculator import TokenCalculator
|
22
|
-
|
23
|
-
|
24
|
-
class EndpointConfig(BaseModel):
|
25
|
-
"""Represents configuration data for an API endpoint.
|
26
|
-
|
27
|
-
Attributes:
|
28
|
-
provider (str | None):
|
29
|
-
The name of the API provider (e.g., "openai").
|
30
|
-
base_url (str | None):
|
31
|
-
The base URL for the endpoint, if any.
|
32
|
-
endpoint (str):
|
33
|
-
The endpoint path or identifier (e.g., "/v1/chat/completions").
|
34
|
-
endpoint_params (dict | None):
|
35
|
-
Key-value pairs for dynamic endpoint formatting.
|
36
|
-
method (Literal["get","post","put","delete"]):
|
37
|
-
The HTTP method used when calling this endpoint.
|
38
|
-
openai_compatible (bool):
|
39
|
-
If True, indicates that the endpoint expects OpenAI-style requests.
|
40
|
-
required_kwargs (set[str]):
|
41
|
-
The names of required parameters for this endpoint.
|
42
|
-
optional_kwargs (set[str]):
|
43
|
-
The names of optional parameters for this endpoint.
|
44
|
-
deprecated_kwargs (set[str]):
|
45
|
-
The names of parameters that may still be accepted but are
|
46
|
-
deprecated.
|
47
|
-
is_invokeable (bool):
|
48
|
-
Whether this endpoint supports direct invocation.
|
49
|
-
is_streamable (bool):
|
50
|
-
Whether this endpoint supports streaming responses.
|
51
|
-
requires_tokens (bool):
|
52
|
-
Whether tokens must be calculated before sending a request.
|
53
|
-
api_version (str | None):
|
54
|
-
An optional version string for the API.
|
55
|
-
allowed_roles (list[str] | None):
|
56
|
-
If set, only these roles are allowed in message or conversation
|
57
|
-
data.
|
58
|
-
"""
|
59
|
-
|
60
|
-
model_config = ConfigDict(
|
61
|
-
arbitrary_types_allowed=True,
|
62
|
-
extra="allow",
|
63
|
-
populate_by_name=True,
|
64
|
-
use_enum_values=True,
|
65
|
-
)
|
66
|
-
|
67
|
-
name: str | None = None
|
68
|
-
provider: str | None = None
|
69
|
-
base_url: str | None = None
|
70
|
-
endpoint: str
|
71
|
-
endpoint_params: dict | None = None
|
72
|
-
method: Literal["get", "post", "put", "delete"] = Field("post")
|
73
|
-
openai_compatible: bool = False
|
74
|
-
required_kwargs: set[str] = Field(default_factory=set)
|
75
|
-
optional_kwargs: set[str] = Field(default_factory=set)
|
76
|
-
deprecated_kwargs: set[str] = Field(default_factory=set)
|
77
|
-
is_invokeable: bool = False
|
78
|
-
is_streamable: bool = False
|
79
|
-
requires_tokens: bool = False
|
80
|
-
api_version: str | None = None
|
81
|
-
allowed_roles: list[str] | None = None
|
82
|
-
request_options: type | None = Field(None, exclude=True)
|
83
|
-
invoke_with_endpoint: bool | None = None
|
84
|
-
|
85
|
-
|
86
|
-
class EndPoint(ABC):
|
87
|
-
"""Abstract base class representing an API endpoint.
|
88
|
-
|
89
|
-
This class wraps an `EndpointConfig` and provides methods for creating and
|
90
|
-
invoking API payloads, optionally with caching or streaming. Concrete
|
91
|
-
implementations should override `_invoke` and `_stream` to perform actual
|
92
|
-
HTTP requests.
|
93
|
-
"""
|
94
|
-
|
95
|
-
def __init__(
|
96
|
-
self, config: dict | EndpointConfig | type[EndpointConfig], **kwargs
|
97
|
-
) -> None:
|
98
|
-
"""Initializes the EndPoint with a given configuration.
|
99
|
-
|
100
|
-
Args:
|
101
|
-
config (dict | EndpointConfig): Configuration data that matches the EndpointConfig
|
102
|
-
schema.
|
103
|
-
"""
|
104
|
-
if isinstance(config, dict):
|
105
|
-
self.config = EndpointConfig(**config)
|
106
|
-
if isinstance(config, EndpointConfig):
|
107
|
-
self.config = config
|
108
|
-
if isinstance(config, type) and issubclass(config, EndpointConfig):
|
109
|
-
self.config = config()
|
110
|
-
if kwargs:
|
111
|
-
self.update_config(**kwargs)
|
112
|
-
|
113
|
-
def update_config(self, **kwargs):
|
114
|
-
config = self.config.model_dump()
|
115
|
-
config.update(kwargs)
|
116
|
-
self.config = self.config.model_validate(config)
|
117
|
-
|
118
|
-
@property
|
119
|
-
def name(self) -> str | None:
|
120
|
-
"""str | None: The name of the endpoint, if any."""
|
121
|
-
return self.config.name or self.endpoint
|
122
|
-
|
123
|
-
@property
|
124
|
-
def is_streamable(self) -> bool:
|
125
|
-
"""bool: Whether this endpoint supports streaming responses."""
|
126
|
-
return self.config.is_streamable
|
127
|
-
|
128
|
-
@property
|
129
|
-
def requires_tokens(self) -> bool:
|
130
|
-
"""bool: Indicates if token calculation is needed before requests."""
|
131
|
-
return self.config.requires_tokens
|
132
|
-
|
133
|
-
@property
|
134
|
-
def openai_compatible(self) -> bool:
|
135
|
-
"""bool: Whether requests conform to OpenAI's API style."""
|
136
|
-
return self.config.openai_compatible
|
137
|
-
|
138
|
-
@property
|
139
|
-
def is_invokeable(self) -> bool:
|
140
|
-
"""bool: Whether this endpoint supports direct invocation."""
|
141
|
-
return self.config.is_invokeable
|
142
|
-
|
143
|
-
@property
|
144
|
-
def required_kwargs(self) -> set[str]:
|
145
|
-
"""set[str]: A set of parameter names required by this endpoint."""
|
146
|
-
return self.config.required_kwargs
|
147
|
-
|
148
|
-
@property
|
149
|
-
def optional_kwargs(self) -> set[str]:
|
150
|
-
"""set[str]: A set of parameter names optionally accepted."""
|
151
|
-
return self.config.optional_kwargs
|
152
|
-
|
153
|
-
@property
|
154
|
-
def deprecated_kwargs(self) -> set[str]:
|
155
|
-
"""set[str]: A set of deprecated parameter names."""
|
156
|
-
return self.config.deprecated_kwargs
|
157
|
-
|
158
|
-
@property
|
159
|
-
def endpoint_params(self) -> dict | None:
|
160
|
-
"""dict | None: Additional parameters to format the endpoint path."""
|
161
|
-
return self.config.endpoint_params
|
162
|
-
|
163
|
-
@property
|
164
|
-
def method(self) -> str:
|
165
|
-
"""str: The HTTP method used when invoking this endpoint."""
|
166
|
-
return self.config.method
|
167
|
-
|
168
|
-
@property
|
169
|
-
def endpoint(self) -> str:
|
170
|
-
"""str: The endpoint path or identifier."""
|
171
|
-
return self.config.endpoint
|
172
|
-
|
173
|
-
@property
|
174
|
-
def acceptable_kwargs(self) -> set[str]:
|
175
|
-
"""set[str]: All parameters that are not explicitly prohibited."""
|
176
|
-
return (
|
177
|
-
self.required_kwargs
|
178
|
-
| self.optional_kwargs
|
179
|
-
| self.deprecated_kwargs
|
180
|
-
)
|
181
|
-
|
182
|
-
@property
|
183
|
-
def full_url(self) -> str:
|
184
|
-
"""str: The complete URL, including base_url and any parameters."""
|
185
|
-
if self.config.endpoint_params:
|
186
|
-
return self.config.base_url + self.config.endpoint.format(
|
187
|
-
**self.endpoint_params
|
188
|
-
)
|
189
|
-
return self.config.base_url + "/" + self.config.endpoint
|
190
|
-
|
191
|
-
@property
|
192
|
-
def allowed_roles(self) -> list[str] | None:
|
193
|
-
"""list[str] | None: A list of allowed roles, if any."""
|
194
|
-
return self.config.allowed_roles
|
195
|
-
|
196
|
-
@property
|
197
|
-
def sequential_exchange(self) -> bool:
|
198
|
-
"""bool: True if this endpoint requires exactly two roles (e.g., user & assistant)."""
|
199
|
-
if self.allowed_roles:
|
200
|
-
return len(self.allowed_roles) == 2
|
201
|
-
return False
|
202
|
-
|
203
|
-
@property
|
204
|
-
def roled(self) -> bool:
|
205
|
-
"""bool: Indicates if this endpoint uses role-based messages."""
|
206
|
-
return self.allowed_roles is not None
|
207
|
-
|
208
|
-
@property
|
209
|
-
def request_options(self) -> type | None:
|
210
|
-
return self.config.request_options
|
211
|
-
|
212
|
-
def create_payload(self, **kwargs) -> dict:
|
213
|
-
"""Generates a request payload (and headers) for this endpoint.
|
214
|
-
|
215
|
-
Args:
|
216
|
-
**kwargs:
|
217
|
-
Arbitrary parameters passed by the caller.
|
218
|
-
|
219
|
-
Returns:
|
220
|
-
dict:
|
221
|
-
A dictionary containing:
|
222
|
-
- "payload": A dict with filtered parameters for the request.
|
223
|
-
- "headers": A dict of additional headers (e.g., `Authorization`).
|
224
|
-
- "is_cached": Whether the request is to be cached.
|
225
|
-
"""
|
226
|
-
payload = {}
|
227
|
-
is_cached = kwargs.get("is_cached", False)
|
228
|
-
headers = kwargs.get("headers", {})
|
229
|
-
for k, v in kwargs.items():
|
230
|
-
if k in self.acceptable_kwargs:
|
231
|
-
payload[k] = v
|
232
|
-
if "api_key" in kwargs:
|
233
|
-
headers["Authorization"] = f"Bearer {kwargs['api_key']}"
|
234
|
-
return {
|
235
|
-
"payload": payload,
|
236
|
-
"headers": headers,
|
237
|
-
"is_cached": is_cached,
|
238
|
-
}
|
239
|
-
|
240
|
-
async def invoke(
|
241
|
-
self,
|
242
|
-
payload: dict,
|
243
|
-
headers: dict,
|
244
|
-
is_cached: bool = False,
|
245
|
-
**kwargs,
|
246
|
-
):
|
247
|
-
"""Invokes this endpoint with the given payload and headers.
|
248
|
-
|
249
|
-
Args:
|
250
|
-
payload (dict):
|
251
|
-
The request data to send.
|
252
|
-
headers (dict):
|
253
|
-
Extra HTTP headers for the request.
|
254
|
-
is_cached (bool):
|
255
|
-
Whether caching should be applied to this request.
|
256
|
-
**kwargs:
|
257
|
-
Additional arguments for the invocation.
|
258
|
-
|
259
|
-
Returns:
|
260
|
-
The result of the `_invoke` or `_cached_invoke` method.
|
261
|
-
"""
|
262
|
-
if is_cached:
|
263
|
-
return await self._cached_invoke(payload, headers, **kwargs)
|
264
|
-
return await self._invoke(payload, headers, **kwargs)
|
265
|
-
|
266
|
-
async def _invoke(self, payload: dict, headers: dict, **kwargs) -> Any:
|
267
|
-
"""Performs the actual HTTP request for non-streaming endpoints.
|
268
|
-
|
269
|
-
Subclasses should implement this to make an HTTP request using
|
270
|
-
`payload` and `headers`.
|
271
|
-
|
272
|
-
Args:
|
273
|
-
payload (dict): The JSON body or form data for the request.
|
274
|
-
headers (dict): Any additional headers (e.g., auth tokens).
|
275
|
-
**kwargs: Additional arguments.
|
276
|
-
|
277
|
-
Returns:
|
278
|
-
Any: The response data from the API.
|
279
|
-
|
280
|
-
Raises:
|
281
|
-
NotImplementedError: If the subclass has not overridden this method.
|
282
|
-
"""
|
283
|
-
raise NotImplementedError
|
284
|
-
|
285
|
-
async def _stream(self, payload: dict, headers: dict, **kwargs) -> Any:
|
286
|
-
"""Streams data from the endpoint if supported.
|
287
|
-
|
288
|
-
Subclasses should implement this if streaming is supported.
|
289
|
-
|
290
|
-
Args:
|
291
|
-
payload (dict): The data to send.
|
292
|
-
headers (dict): Additional headers.
|
293
|
-
|
294
|
-
Raises:
|
295
|
-
NotImplementedError:
|
296
|
-
If the subclass has not overridden this for streaming endpoints.
|
297
|
-
"""
|
298
|
-
raise NotImplementedError
|
299
|
-
|
300
|
-
@cached(**Settings.API.CACHED_CONFIG)
|
301
|
-
async def _cached_invoke(self, payload: dict, headers: dict, **kwargs):
|
302
|
-
"""Cached version of `_invoke` using aiocache.
|
303
|
-
|
304
|
-
Args:
|
305
|
-
payload (dict): The data to send in the request.
|
306
|
-
headers (dict): Extra headers to include.
|
307
|
-
**kwargs: Additional arguments for `_invoke`.
|
308
|
-
|
309
|
-
Returns:
|
310
|
-
Any: Cached or newly obtained response data.
|
311
|
-
"""
|
312
|
-
return await self._invoke(payload, headers, **kwargs)
|
313
|
-
|
314
|
-
def calculate_tokens(self, payload: dict) -> int:
|
315
|
-
"""Calculates the number of tokens needed for a request.
|
316
|
-
|
317
|
-
Uses the `TokenCalculator` if the endpoint requires token counting.
|
318
|
-
|
319
|
-
Args:
|
320
|
-
payload (dict):
|
321
|
-
The request data, possibly containing "messages" for chat
|
322
|
-
or an "embed" request.
|
323
|
-
|
324
|
-
Returns:
|
325
|
-
int: The estimated number of tokens used.
|
326
|
-
"""
|
327
|
-
if self.requires_tokens:
|
328
|
-
if "messages" in payload:
|
329
|
-
return TokenCalculator.calculate_message_tokens(
|
330
|
-
payload["messages"]
|
331
|
-
)
|
332
|
-
if "embed" in self.full_url:
|
333
|
-
return TokenCalculator.calcualte_embed_token(**payload)
|
334
|
-
return 0
|
335
|
-
|
336
|
-
|
337
|
-
class APICalling(Event):
|
338
|
-
"""Represents an API call event, storing payload, headers, and endpoint info.
|
339
|
-
|
340
|
-
This class extends `Event` and provides methods to invoke or stream the
|
341
|
-
request asynchronously. It also can track token usage and stores the
|
342
|
-
result or any errors in `execution`.
|
343
|
-
|
344
|
-
Attributes:
|
345
|
-
payload (dict):
|
346
|
-
The body or parameters for the API request.
|
347
|
-
headers (dict):
|
348
|
-
Additional headers (excluded from serialization).
|
349
|
-
endpoint (EndPoint):
|
350
|
-
The endpoint to which this request will be sent.
|
351
|
-
is_cached (bool):
|
352
|
-
Whether to use cached responses.
|
353
|
-
should_invoke_endpoint (bool):
|
354
|
-
If False, the request may not actually call the API.
|
355
|
-
"""
|
356
|
-
|
357
|
-
payload: dict
|
358
|
-
headers: dict = Field(exclude=True)
|
359
|
-
endpoint: EndPoint = Field(exclude=True)
|
360
|
-
is_cached: bool = Field(default=False, exclude=True)
|
361
|
-
should_invoke_endpoint: bool = Field(default=True, exclude=True)
|
362
|
-
include_token_usage_to_model: bool = Field(
|
363
|
-
default=False,
|
364
|
-
exclude=True,
|
365
|
-
description="Whether to include token usage information into instruction messages",
|
366
|
-
)
|
367
|
-
response_obj: BaseModel | None = Field(None, exclude=True)
|
368
|
-
|
369
|
-
@model_validator(mode="after")
|
370
|
-
def _validate_streaming(self) -> Self:
|
371
|
-
if self.payload.get("stream") is True:
|
372
|
-
self.streaming = True
|
373
|
-
|
374
|
-
if self.include_token_usage_to_model and self.endpoint.requires_tokens:
|
375
|
-
if isinstance(self.payload["messages"][-1], dict):
|
376
|
-
required_tokens = self.required_tokens
|
377
|
-
content = self.payload["messages"][-1]["content"]
|
378
|
-
token_msg = (
|
379
|
-
f"\n\nEstimated Current Token Usage: {required_tokens}"
|
380
|
-
)
|
381
|
-
|
382
|
-
if "model" in self.payload:
|
383
|
-
if (
|
384
|
-
self.payload["model"].startswith("gpt-4")
|
385
|
-
or "o1mini" in self.payload["model"]
|
386
|
-
or "o1-preview" in self.payload["model"]
|
387
|
-
):
|
388
|
-
token_msg += "/128_000"
|
389
|
-
elif "o1" in self.payload["model"]:
|
390
|
-
token_msg += "/200_000"
|
391
|
-
elif "sonnet" in self.payload["model"]:
|
392
|
-
token_msg += "/200_000"
|
393
|
-
elif "haiku" in self.payload["model"]:
|
394
|
-
token_msg += "/200_000"
|
395
|
-
elif "gemini" in self.payload["model"]:
|
396
|
-
token_msg += "/1_000_000"
|
397
|
-
elif "qwen-turbo" in self.payload["model"]:
|
398
|
-
token_msg += "/1_000_000"
|
399
|
-
|
400
|
-
if isinstance(content, str):
|
401
|
-
content += token_msg
|
402
|
-
elif isinstance(content, dict):
|
403
|
-
if "text" in content:
|
404
|
-
content["text"] += token_msg
|
405
|
-
elif isinstance(content, list):
|
406
|
-
for i in reversed(content):
|
407
|
-
if "text" in i:
|
408
|
-
i["text"] += token_msg
|
409
|
-
break
|
410
|
-
self.payload["messages"][-1]["content"] = content
|
411
|
-
|
412
|
-
return self
|
413
|
-
|
414
|
-
@property
|
415
|
-
def required_tokens(self) -> int | None:
|
416
|
-
"""int | None: The number of tokens required for this request."""
|
417
|
-
if self.endpoint.requires_tokens:
|
418
|
-
return self.endpoint.calculate_tokens(self.payload)
|
419
|
-
return None
|
420
|
-
|
421
|
-
async def _inner(self, **kwargs) -> Any:
|
422
|
-
"""
|
423
|
-
Performs the actual HTTP call using aiohttp, ignoring caching logic.
|
424
|
-
|
425
|
-
- Retries on RateLimitError up to 3 times.
|
426
|
-
- Distinguishes CancelledError so we can gracefully abort if the user cancels.
|
427
|
-
|
428
|
-
Raises:
|
429
|
-
ValueError: If required endpoint parameters are missing.
|
430
|
-
RateLimitError: If repeated 'Rate limit' errors encountered.
|
431
|
-
ExecutionError: For other API call failures.
|
432
|
-
asyncio.CancelledError: If the operation is cancelled externally.
|
433
|
-
"""
|
434
|
-
if not self.endpoint.required_kwargs.issubset(
|
435
|
-
set(self.payload.keys())
|
436
|
-
):
|
437
|
-
raise ValueError(
|
438
|
-
f"Required kwargs not provided: {self.endpoint.required_kwargs}"
|
439
|
-
)
|
440
|
-
|
441
|
-
for k in list(self.payload.keys()):
|
442
|
-
if k not in self.endpoint.acceptable_kwargs:
|
443
|
-
self.payload.pop(k)
|
444
|
-
|
445
|
-
async def retry_in():
|
446
|
-
async with aiohttp.ClientSession() as session:
|
447
|
-
try:
|
448
|
-
method_func = getattr(session, self.endpoint.method, None)
|
449
|
-
if method_func is None:
|
450
|
-
raise ValueError(
|
451
|
-
f"Invalid HTTP method: {self.endpoint.method}"
|
452
|
-
)
|
453
|
-
async with method_func(
|
454
|
-
self.endpoint.full_url, **kwargs
|
455
|
-
) as response:
|
456
|
-
response_json = await response.json()
|
457
|
-
|
458
|
-
if "error" not in response_json:
|
459
|
-
return response_json
|
460
|
-
|
461
|
-
# Check for rate limit
|
462
|
-
if "Rate limit" in response_json["error"].get(
|
463
|
-
"message", ""
|
464
|
-
):
|
465
|
-
await asyncio.sleep(5)
|
466
|
-
raise RateLimitError(
|
467
|
-
f"Rate limit exceeded: {response_json['error']}"
|
468
|
-
)
|
469
|
-
# Otherwise some other error
|
470
|
-
raise ExecutionError(
|
471
|
-
f"API call failed with error: {response_json['error']}"
|
472
|
-
)
|
473
|
-
|
474
|
-
except asyncio.CancelledError:
|
475
|
-
# Gracefully handle user cancellation
|
476
|
-
logging.warning("API call canceled by external request.")
|
477
|
-
raise # re-raise so caller knows it was cancelled
|
478
|
-
|
479
|
-
except aiohttp.ClientError as e:
|
480
|
-
logging.error(f"API call failed: {e}")
|
481
|
-
# Return None or raise ExecutionError? Keep consistent
|
482
|
-
return None
|
483
|
-
|
484
|
-
# Attempt up to 3 times if RateLimitError
|
485
|
-
for i in range(3):
|
486
|
-
try:
|
487
|
-
return await retry_in()
|
488
|
-
except asyncio.CancelledError:
|
489
|
-
# On cancel, just re-raise
|
490
|
-
raise
|
491
|
-
except RateLimitError as e:
|
492
|
-
if i == 2:
|
493
|
-
raise e
|
494
|
-
wait = 2 ** (i + 1) * 0.5
|
495
|
-
logging.warning(f"RateLimitError: {e}, retrying in {wait}s.")
|
496
|
-
await asyncio.sleep(wait)
|
497
|
-
|
498
|
-
@cached(**Settings.API.CACHED_CONFIG)
|
499
|
-
async def _cached_inner(self, **kwargs) -> Any:
|
500
|
-
"""Cached version of `_inner` using aiocache.
|
501
|
-
|
502
|
-
Args:
|
503
|
-
**kwargs: Passed to `_inner`.
|
504
|
-
|
505
|
-
Returns:
|
506
|
-
Any: The result of `_inner`, possibly from cache.
|
507
|
-
"""
|
508
|
-
return await self._inner(**kwargs)
|
509
|
-
|
510
|
-
async def _stream(
|
511
|
-
self,
|
512
|
-
verbose: bool = True,
|
513
|
-
output_file: str = None,
|
514
|
-
with_response_header: bool = False,
|
515
|
-
) -> AsyncGenerator:
|
516
|
-
async with aiohttp.ClientSession() as client:
|
517
|
-
async with client.request(
|
518
|
-
method=self.endpoint.method.upper(),
|
519
|
-
url=self.endpoint.full_url,
|
520
|
-
headers=self.headers,
|
521
|
-
json=self.payload,
|
522
|
-
) as response:
|
523
|
-
if response.status != 200:
|
524
|
-
try:
|
525
|
-
error_text = await response.json()
|
526
|
-
except Exception:
|
527
|
-
error_text = await response.text()
|
528
|
-
raise aiohttp.ClientResponseError(
|
529
|
-
request_info=response.request_info,
|
530
|
-
history=response.history,
|
531
|
-
status=response.status,
|
532
|
-
message=f"{error_text}",
|
533
|
-
headers=response.headers,
|
534
|
-
)
|
535
|
-
|
536
|
-
file_handle = None
|
537
|
-
|
538
|
-
if output_file:
|
539
|
-
try:
|
540
|
-
file_handle = open(output_file, "w")
|
541
|
-
except Exception as e:
|
542
|
-
raise ValueError(
|
543
|
-
f"Invalid to output the response "
|
544
|
-
f"to {output_file}. Error:{e}"
|
545
|
-
)
|
546
|
-
|
547
|
-
try:
|
548
|
-
async for chunk in response.content:
|
549
|
-
chunk_str = chunk.decode("utf-8")
|
550
|
-
chunk_list = chunk_str.split("data:")
|
551
|
-
for c in chunk_list:
|
552
|
-
c = c.strip()
|
553
|
-
if c and c != "[DONE]":
|
554
|
-
try:
|
555
|
-
if file_handle:
|
556
|
-
file_handle.write(c + "\n")
|
557
|
-
c_dict = json.loads(c)
|
558
|
-
if verbose:
|
559
|
-
if c_dict.get("choices"):
|
560
|
-
if content := c_dict["choices"][0][
|
561
|
-
"delta"
|
562
|
-
].get("content"):
|
563
|
-
print(
|
564
|
-
content, end="", flush=True
|
565
|
-
)
|
566
|
-
yield c_dict
|
567
|
-
except json.JSONDecodeError:
|
568
|
-
yield c
|
569
|
-
except asyncio.CancelledError as e:
|
570
|
-
raise e
|
571
|
-
|
572
|
-
if with_response_header:
|
573
|
-
yield response.headers
|
574
|
-
|
575
|
-
finally:
|
576
|
-
if file_handle:
|
577
|
-
file_handle.close()
|
578
|
-
|
579
|
-
async def stream(
|
580
|
-
self,
|
581
|
-
verbose: bool = True,
|
582
|
-
output_file: str = None,
|
583
|
-
with_response_header: bool = False,
|
584
|
-
**kwargs,
|
585
|
-
) -> AsyncGenerator:
|
586
|
-
"""Performs a streaming request, if supported by the endpoint.
|
587
|
-
|
588
|
-
Args:
|
589
|
-
verbose (bool):
|
590
|
-
If True, prints the response content to the console.
|
591
|
-
output_file (str):
|
592
|
-
If set, writes the response content to this file. (only applies to non-endpoint invoke)
|
593
|
-
with_response_header (bool):
|
594
|
-
If True, yields the response headers as well. (only applies to non-endpoint invoke)
|
595
|
-
**kwargs: Additional parameters for the streaming call.
|
596
|
-
|
597
|
-
Yields:
|
598
|
-
The streamed chunks of data, if any.
|
599
|
-
|
600
|
-
Raises:
|
601
|
-
ValueError: If the endpoint does not support streaming.
|
602
|
-
"""
|
603
|
-
start = asyncio.get_event_loop().time()
|
604
|
-
response = []
|
605
|
-
e1 = None
|
606
|
-
try:
|
607
|
-
if self.should_invoke_endpoint and self.endpoint.is_streamable:
|
608
|
-
async for i in self.endpoint._stream(
|
609
|
-
self.payload, self.headers, **kwargs
|
610
|
-
):
|
611
|
-
content = i.choices[0].delta.content
|
612
|
-
if verbose:
|
613
|
-
if content is not None:
|
614
|
-
print(content, end="", flush=True)
|
615
|
-
response.append(i)
|
616
|
-
yield i
|
617
|
-
else:
|
618
|
-
async for i in self._stream(
|
619
|
-
verbose=verbose,
|
620
|
-
output_file=output_file,
|
621
|
-
with_response_header=with_response_header,
|
622
|
-
):
|
623
|
-
response.append(i)
|
624
|
-
yield i
|
625
|
-
except Exception as e:
|
626
|
-
e1 = e
|
627
|
-
finally:
|
628
|
-
self.execution.duration = asyncio.get_event_loop().time() - start
|
629
|
-
if not response and e1:
|
630
|
-
self.execution.error = str(e1)
|
631
|
-
self.execution.status = EventStatus.FAILED
|
632
|
-
logging.error(
|
633
|
-
f"API call to {self.endpoint.full_url} failed: {e1}"
|
634
|
-
)
|
635
|
-
else:
|
636
|
-
self.execution.response = response
|
637
|
-
self.execution.status = EventStatus.COMPLETED
|
638
|
-
|
639
|
-
async def invoke(self) -> None:
|
640
|
-
"""Invokes the API call, updating the execution state with results.
|
641
|
-
|
642
|
-
Raises:
|
643
|
-
Exception: If any error occurs, the status is set to FAILED and
|
644
|
-
the error is logged.
|
645
|
-
"""
|
646
|
-
start = asyncio.get_event_loop().time()
|
647
|
-
kwargs = {"headers": self.headers, "json": self.payload}
|
648
|
-
response = None
|
649
|
-
e1 = None
|
650
|
-
|
651
|
-
try:
|
652
|
-
if self.should_invoke_endpoint and self.endpoint.is_invokeable:
|
653
|
-
response = await self.endpoint.invoke(
|
654
|
-
payload=self.payload,
|
655
|
-
headers=self.headers,
|
656
|
-
is_cached=self.is_cached,
|
657
|
-
)
|
658
|
-
else:
|
659
|
-
if self.is_cached:
|
660
|
-
response = await self._cached_inner(**kwargs)
|
661
|
-
else:
|
662
|
-
response = await self._inner(**kwargs)
|
663
|
-
|
664
|
-
except asyncio.CancelledError as ce:
|
665
|
-
e1 = ce
|
666
|
-
logging.warning("invoke() canceled by external request.")
|
667
|
-
raise
|
668
|
-
except Exception as ex:
|
669
|
-
e1 = ex
|
670
|
-
|
671
|
-
finally:
|
672
|
-
self.execution.duration = asyncio.get_event_loop().time() - start
|
673
|
-
if not response and e1:
|
674
|
-
self.execution.error = str(e1)
|
675
|
-
self.execution.status = EventStatus.FAILED
|
676
|
-
logging.error(
|
677
|
-
f"API call to {self.endpoint.full_url} failed: {e1}"
|
678
|
-
)
|
679
|
-
else:
|
680
|
-
self.response_obj = response
|
681
|
-
self.execution.response = (
|
682
|
-
response.model_dump()
|
683
|
-
if isinstance(response, BaseModel)
|
684
|
-
else response
|
685
|
-
)
|
686
|
-
self.execution.status = EventStatus.COMPLETED
|
687
|
-
|
688
|
-
def __str__(self) -> str:
|
689
|
-
return (
|
690
|
-
f"APICalling(id={self.id}, status={self.status}, duration="
|
691
|
-
f"{self.execution.duration}, response={self.execution.response}"
|
692
|
-
f", error={self.execution.error})"
|
693
|
-
)
|
694
|
-
|
695
|
-
__repr__ = __str__
|
696
|
-
|
697
|
-
@property
|
698
|
-
def request(self) -> dict:
|
699
|
-
"""dict: A partial dictionary with data about the request (e.g. tokens).
|
700
|
-
|
701
|
-
Returns:
|
702
|
-
dict: Contains 'required_tokens' if applicable.
|
703
|
-
"""
|
704
|
-
return {
|
705
|
-
"required_tokens": self.required_tokens,
|
706
|
-
}
|