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.
Files changed (86) hide show
  1. lionagi/config.py +123 -0
  2. lionagi/fields/file.py +1 -1
  3. lionagi/fields/reason.py +1 -1
  4. lionagi/libs/file/concat.py +1 -6
  5. lionagi/libs/file/concat_files.py +1 -5
  6. lionagi/libs/file/save.py +1 -1
  7. lionagi/libs/package/imports.py +8 -177
  8. lionagi/libs/parse.py +30 -0
  9. lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
  10. lionagi/libs/token_transform/perplexity.py +2 -4
  11. lionagi/libs/token_transform/synthlang_/resources/frameworks/framework_options.json +46 -46
  12. lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
  13. lionagi/operations/chat/chat.py +2 -2
  14. lionagi/operations/communicate/communicate.py +20 -5
  15. lionagi/operations/parse/parse.py +131 -43
  16. lionagi/protocols/generic/log.py +1 -2
  17. lionagi/protocols/generic/pile.py +18 -4
  18. lionagi/protocols/messages/assistant_response.py +20 -1
  19. lionagi/protocols/messages/templates/README.md +6 -10
  20. lionagi/service/connections/__init__.py +15 -0
  21. lionagi/service/connections/api_calling.py +230 -0
  22. lionagi/service/connections/endpoint.py +410 -0
  23. lionagi/service/connections/endpoint_config.py +137 -0
  24. lionagi/service/connections/header_factory.py +56 -0
  25. lionagi/service/connections/match_endpoint.py +49 -0
  26. lionagi/service/connections/providers/__init__.py +3 -0
  27. lionagi/service/connections/providers/anthropic_.py +87 -0
  28. lionagi/service/connections/providers/exa_.py +33 -0
  29. lionagi/service/connections/providers/oai_.py +166 -0
  30. lionagi/service/connections/providers/ollama_.py +122 -0
  31. lionagi/service/connections/providers/perplexity_.py +29 -0
  32. lionagi/service/imodel.py +36 -144
  33. lionagi/service/manager.py +1 -7
  34. lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
  35. lionagi/service/resilience.py +545 -0
  36. lionagi/service/third_party/README.md +71 -0
  37. lionagi/service/third_party/__init__.py +0 -0
  38. lionagi/service/third_party/anthropic_models.py +159 -0
  39. lionagi/service/third_party/exa_models.py +165 -0
  40. lionagi/service/third_party/openai_models.py +18241 -0
  41. lionagi/service/third_party/pplx_models.py +156 -0
  42. lionagi/service/types.py +5 -4
  43. lionagi/session/branch.py +12 -7
  44. lionagi/tools/file/reader.py +1 -1
  45. lionagi/tools/memory/tools.py +497 -0
  46. lionagi/utils.py +921 -123
  47. lionagi/version.py +1 -1
  48. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/METADATA +33 -16
  49. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/RECORD +53 -63
  50. lionagi/libs/file/create_path.py +0 -80
  51. lionagi/libs/file/file_util.py +0 -358
  52. lionagi/libs/parse/__init__.py +0 -3
  53. lionagi/libs/parse/fuzzy_parse_json.py +0 -117
  54. lionagi/libs/parse/to_dict.py +0 -336
  55. lionagi/libs/parse/to_json.py +0 -61
  56. lionagi/libs/parse/to_num.py +0 -378
  57. lionagi/libs/parse/to_xml.py +0 -57
  58. lionagi/libs/parse/xml_parser.py +0 -148
  59. lionagi/libs/schema/breakdown_pydantic_annotation.py +0 -48
  60. lionagi/service/endpoints/__init__.py +0 -3
  61. lionagi/service/endpoints/base.py +0 -706
  62. lionagi/service/endpoints/chat_completion.py +0 -116
  63. lionagi/service/endpoints/match_endpoint.py +0 -72
  64. lionagi/service/providers/__init__.py +0 -3
  65. lionagi/service/providers/anthropic_/__init__.py +0 -3
  66. lionagi/service/providers/anthropic_/messages.py +0 -99
  67. lionagi/service/providers/exa_/models.py +0 -3
  68. lionagi/service/providers/exa_/search.py +0 -80
  69. lionagi/service/providers/exa_/types.py +0 -7
  70. lionagi/service/providers/groq_/__init__.py +0 -3
  71. lionagi/service/providers/groq_/chat_completions.py +0 -56
  72. lionagi/service/providers/ollama_/__init__.py +0 -3
  73. lionagi/service/providers/ollama_/chat_completions.py +0 -134
  74. lionagi/service/providers/openai_/__init__.py +0 -3
  75. lionagi/service/providers/openai_/chat_completions.py +0 -101
  76. lionagi/service/providers/openai_/spec.py +0 -14
  77. lionagi/service/providers/openrouter_/__init__.py +0 -3
  78. lionagi/service/providers/openrouter_/chat_completions.py +0 -62
  79. lionagi/service/providers/perplexity_/__init__.py +0 -3
  80. lionagi/service/providers/perplexity_/chat_completions.py +0 -44
  81. lionagi/service/providers/perplexity_/models.py +0 -5
  82. lionagi/service/providers/types.py +0 -17
  83. /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
  84. /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
  85. {lionagi-0.12.2.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
  86. {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
- }