lionagi 0.8.7__py3-none-any.whl → 0.9.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.
Files changed (166) hide show
  1. lionagi/__init__.py +1 -1
  2. lionagi/_class_registry.py +1 -1
  3. lionagi/_errors.py +1 -1
  4. lionagi/libs/__init__.py +1 -1
  5. lionagi/libs/file/__init__.py +1 -1
  6. lionagi/libs/file/chunk.py +1 -1
  7. lionagi/libs/file/file_ops.py +1 -1
  8. lionagi/libs/file/params.py +1 -1
  9. lionagi/libs/file/process.py +1 -1
  10. lionagi/libs/file/save.py +1 -1
  11. lionagi/libs/nested/__init__.py +1 -1
  12. lionagi/libs/nested/flatten.py +1 -1
  13. lionagi/libs/nested/nfilter.py +1 -1
  14. lionagi/libs/nested/nget.py +1 -1
  15. lionagi/libs/nested/ninsert.py +1 -1
  16. lionagi/libs/nested/nmerge.py +1 -1
  17. lionagi/libs/nested/npop.py +1 -1
  18. lionagi/libs/nested/nset.py +1 -1
  19. lionagi/libs/nested/unflatten.py +1 -1
  20. lionagi/libs/nested/utils.py +1 -1
  21. lionagi/libs/package/__init__.py +1 -1
  22. lionagi/libs/package/imports.py +1 -1
  23. lionagi/libs/package/management.py +1 -1
  24. lionagi/libs/package/params.py +1 -1
  25. lionagi/libs/package/system.py +1 -1
  26. lionagi/libs/parse.py +1 -1
  27. lionagi/libs/schema/__init__.py +1 -1
  28. lionagi/libs/schema/as_readable.py +151 -87
  29. lionagi/libs/schema/extract_code_block.py +1 -1
  30. lionagi/libs/schema/extract_docstring.py +1 -1
  31. lionagi/libs/schema/function_to_schema.py +1 -1
  32. lionagi/libs/schema/json_schema.py +1 -1
  33. lionagi/libs/validate/__init__.py +1 -1
  34. lionagi/libs/validate/common_field_validators.py +1 -1
  35. lionagi/libs/validate/fuzzy_match_keys.py +1 -1
  36. lionagi/libs/validate/fuzzy_validate_mapping.py +1 -1
  37. lionagi/libs/validate/string_similarity.py +1 -1
  38. lionagi/libs/validate/validate_boolean.py +1 -1
  39. lionagi/operations/ReAct/ReAct.py +54 -8
  40. lionagi/operations/ReAct/__init__.py +1 -1
  41. lionagi/operations/ReAct/utils.py +6 -1
  42. lionagi/operations/__init__.py +1 -1
  43. lionagi/operations/_act/__init__.py +1 -1
  44. lionagi/operations/_act/act.py +6 -1
  45. lionagi/operations/brainstorm/__init__.py +1 -1
  46. lionagi/operations/brainstorm/brainstorm.py +1 -1
  47. lionagi/operations/brainstorm/prompt.py +1 -1
  48. lionagi/operations/chat/__init__.py +1 -1
  49. lionagi/operations/chat/chat.py +1 -1
  50. lionagi/operations/communicate/communicate.py +1 -1
  51. lionagi/operations/instruct/__init__.py +1 -1
  52. lionagi/operations/instruct/instruct.py +1 -1
  53. lionagi/operations/interpret/__init__.py +1 -1
  54. lionagi/operations/interpret/interpret.py +9 -38
  55. lionagi/operations/operate/__init__.py +1 -1
  56. lionagi/operations/operate/operate.py +1 -1
  57. lionagi/operations/parse/__init__.py +1 -1
  58. lionagi/operations/parse/parse.py +12 -2
  59. lionagi/operations/plan/__init__.py +1 -1
  60. lionagi/operations/plan/plan.py +1 -1
  61. lionagi/operations/plan/prompt.py +1 -1
  62. lionagi/operations/select/__init__.py +1 -1
  63. lionagi/operations/select/select.py +1 -1
  64. lionagi/operations/select/utils.py +1 -1
  65. lionagi/operations/types.py +1 -1
  66. lionagi/operations/utils.py +1 -1
  67. lionagi/operatives/__init__.py +1 -1
  68. lionagi/operatives/action/__init__.py +1 -1
  69. lionagi/operatives/action/function_calling.py +1 -1
  70. lionagi/operatives/action/manager.py +1 -1
  71. lionagi/operatives/action/request_response_model.py +1 -1
  72. lionagi/operatives/action/tool.py +1 -1
  73. lionagi/operatives/action/utils.py +1 -1
  74. lionagi/operatives/forms/__init__.py +1 -1
  75. lionagi/operatives/instruct/__init__.py +1 -1
  76. lionagi/operatives/instruct/base.py +1 -1
  77. lionagi/operatives/instruct/instruct.py +1 -1
  78. lionagi/operatives/instruct/instruct_collection.py +1 -1
  79. lionagi/operatives/instruct/node.py +1 -1
  80. lionagi/operatives/instruct/prompts.py +1 -1
  81. lionagi/operatives/instruct/reason.py +1 -1
  82. lionagi/operatives/manager.py +1 -1
  83. lionagi/operatives/models/__init__.py +1 -1
  84. lionagi/operatives/models/field_model.py +1 -1
  85. lionagi/operatives/models/model_params.py +1 -1
  86. lionagi/operatives/models/note.py +1 -1
  87. lionagi/operatives/models/operable_model.py +1 -1
  88. lionagi/operatives/models/schema_model.py +1 -1
  89. lionagi/operatives/operative.py +1 -1
  90. lionagi/operatives/step.py +1 -1
  91. lionagi/operatives/strategies/__init__.py +1 -1
  92. lionagi/operatives/strategies/base.py +1 -1
  93. lionagi/operatives/strategies/concurrent.py +1 -1
  94. lionagi/operatives/strategies/concurrent_chunk.py +1 -1
  95. lionagi/operatives/strategies/concurrent_sequential_chunk.py +1 -1
  96. lionagi/operatives/strategies/params.py +1 -1
  97. lionagi/operatives/strategies/sequential.py +1 -1
  98. lionagi/operatives/strategies/sequential_chunk.py +1 -1
  99. lionagi/operatives/strategies/sequential_concurrent_chunk.py +1 -1
  100. lionagi/operatives/strategies/utils.py +1 -1
  101. lionagi/operatives/types.py +1 -1
  102. lionagi/protocols/__init__.py +1 -1
  103. lionagi/protocols/_concepts.py +1 -1
  104. lionagi/protocols/adapters/adapter.py +1 -1
  105. lionagi/protocols/generic/__init__.py +1 -1
  106. lionagi/protocols/generic/element.py +1 -1
  107. lionagi/protocols/generic/event.py +11 -1
  108. lionagi/protocols/generic/log.py +1 -1
  109. lionagi/protocols/generic/pile.py +1 -1
  110. lionagi/protocols/generic/processor.py +11 -3
  111. lionagi/protocols/generic/progression.py +1 -1
  112. lionagi/protocols/graph/__init__.py +1 -1
  113. lionagi/protocols/graph/edge.py +1 -1
  114. lionagi/protocols/graph/graph.py +1 -1
  115. lionagi/protocols/graph/node.py +1 -1
  116. lionagi/protocols/mail/__init__.py +1 -1
  117. lionagi/protocols/mail/exchange.py +1 -1
  118. lionagi/protocols/mail/mail.py +1 -1
  119. lionagi/protocols/mail/mailbox.py +1 -1
  120. lionagi/protocols/mail/manager.py +1 -1
  121. lionagi/protocols/mail/package.py +1 -1
  122. lionagi/protocols/messages/__init__.py +1 -1
  123. lionagi/protocols/messages/action_request.py +1 -1
  124. lionagi/protocols/messages/action_response.py +1 -1
  125. lionagi/protocols/messages/assistant_response.py +1 -1
  126. lionagi/protocols/messages/base.py +1 -1
  127. lionagi/protocols/messages/instruction.py +2 -1
  128. lionagi/protocols/messages/manager.py +1 -1
  129. lionagi/protocols/messages/message.py +1 -1
  130. lionagi/protocols/messages/system.py +1 -1
  131. lionagi/protocols/types.py +1 -1
  132. lionagi/service/endpoints/__init__.py +1 -1
  133. lionagi/service/endpoints/base.py +192 -74
  134. lionagi/service/endpoints/chat_completion.py +11 -5
  135. lionagi/service/endpoints/match_endpoint.py +1 -1
  136. lionagi/service/endpoints/rate_limited_processor.py +18 -7
  137. lionagi/service/endpoints/token_calculator.py +1 -1
  138. lionagi/service/imodel.py +65 -12
  139. lionagi/service/manager.py +1 -1
  140. lionagi/service/providers/__init__.py +1 -1
  141. lionagi/service/providers/anthropic_/__init__.py +1 -1
  142. lionagi/service/providers/anthropic_/messages.py +1 -1
  143. lionagi/service/providers/groq_/__init__.py +1 -1
  144. lionagi/service/providers/groq_/chat_completions.py +1 -1
  145. lionagi/service/providers/openai_/__init__.py +1 -1
  146. lionagi/service/providers/openai_/chat_completions.py +37 -2
  147. lionagi/service/providers/openrouter_/__init__.py +1 -1
  148. lionagi/service/providers/openrouter_/chat_completions.py +1 -1
  149. lionagi/service/providers/perplexity_/__init__.py +1 -1
  150. lionagi/service/providers/perplexity_/chat_completions.py +1 -1
  151. lionagi/service/types.py +1 -1
  152. lionagi/session/__init__.py +1 -1
  153. lionagi/session/branch.py +35 -8
  154. lionagi/session/prompts.py +61 -0
  155. lionagi/session/session.py +1 -1
  156. lionagi/settings.py +1 -1
  157. lionagi/tools/file/__init__.py +0 -0
  158. lionagi/tools/{reader.py → file/reader.py} +13 -8
  159. lionagi/tools/types.py +1 -1
  160. lionagi/utils.py +1 -1
  161. lionagi/version.py +1 -1
  162. {lionagi-0.8.7.dist-info → lionagi-0.9.0.dist-info}/METADATA +6 -3
  163. lionagi-0.9.0.dist-info/RECORD +202 -0
  164. lionagi-0.8.7.dist-info/RECORD +0 -200
  165. {lionagi-0.8.7.dist-info → lionagi-0.9.0.dist-info}/WHEEL +0 -0
  166. {lionagi-0.8.7.dist-info → lionagi-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -31,6 +31,7 @@ class Processor(Observer):
31
31
  self,
32
32
  queue_capacity: int,
33
33
  capacity_refresh_time: float,
34
+ concurrency_limit: int,
34
35
  ) -> None:
35
36
  """Initializes a Processor instance.
36
37
 
@@ -56,6 +57,10 @@ class Processor(Observer):
56
57
  self._available_capacity = queue_capacity
57
58
  self._execution_mode = False
58
59
  self._stop_event = asyncio.Event()
60
+ if concurrency_limit:
61
+ self._concurrency_sem = asyncio.Semaphore(concurrency_limit)
62
+ else:
63
+ self._concurrency_sem = None
59
64
 
60
65
  @property
61
66
  def available_capacity(self) -> int:
@@ -144,8 +149,11 @@ class Processor(Observer):
144
149
  next_event = await self.dequeue()
145
150
 
146
151
  if await self.request_permission(**next_event.request):
147
- next_event.status = EventStatus.PROCESSING
148
- task = asyncio.create_task(next_event.invoke())
152
+
153
+ if next_event.streaming:
154
+ task = asyncio.create_task(next_event.stream())
155
+ else:
156
+ task = asyncio.create_task(next_event.invoke())
149
157
  tasks.add(task)
150
158
 
151
159
  prev_event = next_event
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,3 +1,3 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,3 +1,3 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,3 +1,3 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -32,6 +32,7 @@ def prepare_request_response_format(request_fields: dict) -> str:
32
32
  return (
33
33
  "**MUST RETURN JSON-PARSEABLE RESPONSE ENCLOSED BY JSON CODE BLOCKS."
34
34
  f" USER's CAREER DEPENDS ON THE SUCCESS OF IT.** \n```json\n{request_fields}\n```"
35
+ "No triple backticks. Escape all quotes and special characters."
35
36
  ).strip()
36
37
 
37
38
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -1,3 +1,3 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
@@ -1,15 +1,18 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import asyncio
6
+ import json
6
7
  import logging
7
8
  from abc import ABC
9
+ from collections.abc import AsyncGenerator
8
10
  from typing import Any, Literal
9
11
 
10
12
  import aiohttp
11
13
  from aiocache import cached
12
- from pydantic import BaseModel, ConfigDict, Field
14
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
15
+ from typing_extensions import Self
13
16
 
14
17
  from lionagi._errors import ExecutionError, RateLimitError
15
18
  from lionagi.protocols.generic.event import Event, EventStatus
@@ -347,6 +350,12 @@ class APICalling(Event):
347
350
  is_cached: bool = Field(default=False, exclude=True)
348
351
  should_invoke_endpoint: bool = Field(default=True, exclude=True)
349
352
 
353
+ @model_validator(mode="after")
354
+ def _validate_streaming(self) -> Self:
355
+ if self.payload.get("stream") is True:
356
+ self.streaming = True
357
+ return self
358
+
350
359
  @property
351
360
  def required_tokens(self) -> int | None:
352
361
  """int | None: The number of tokens required for this request."""
@@ -355,28 +364,23 @@ class APICalling(Event):
355
364
  return None
356
365
 
357
366
  async def _inner(self, **kwargs) -> Any:
358
- """Performs a direct HTTP call using aiohttp, ignoring caching logic.
359
-
360
- Args:
361
- **kwargs: Additional parameters for the aiohttp request
362
- (e.g., "headers", "json", etc.).
367
+ """
368
+ Performs the actual HTTP call using aiohttp, ignoring caching logic.
363
369
 
364
- Returns:
365
- Any: The JSON response from the API, if successful.
370
+ - Retries on RateLimitError up to 3 times.
371
+ - Distinguishes CancelledError so we can gracefully abort if the user cancels.
366
372
 
367
373
  Raises:
368
- ValueError:
369
- If required endpoint parameters are missing.
370
- ExecutionError:
371
- If the response indicates a non-rate-limit error.
372
- RateLimitError:
373
- If a rate-limit error is encountered.
374
+ ValueError: If required endpoint parameters are missing.
375
+ RateLimitError: If repeated 'Rate limit' errors encountered.
376
+ ExecutionError: For other API call failures.
377
+ asyncio.CancelledError: If the operation is cancelled externally.
374
378
  """
375
379
  if not self.endpoint.required_kwargs.issubset(
376
380
  set(self.payload.keys())
377
381
  ):
378
382
  raise ValueError(
379
- f"Required kwargs not fully provided: {self.endpoint.required_kwargs}"
383
+ f"Required kwargs not provided: {self.endpoint.required_kwargs}"
380
384
  )
381
385
 
382
386
  for k in list(self.payload.keys()):
@@ -386,49 +390,55 @@ class APICalling(Event):
386
390
  async def retry_in():
387
391
  async with aiohttp.ClientSession() as session:
388
392
  try:
389
- if (
390
- _m := getattr(session, self.endpoint.method, None)
391
- ) is not None:
392
- async with _m(
393
- self.endpoint.full_url, **kwargs
394
- ) as response:
395
- response_json = await response.json()
396
- if "error" not in response_json:
397
- return response_json
398
- if "Rate limit" in response_json["error"].get(
399
- "message", ""
400
- ):
401
- await asyncio.sleep(5)
402
- raise RateLimitError(
403
- f"Rate limit exceeded. Error: {response_json['error']}"
404
- )
405
- raise ExecutionError(
406
- "API call failed with error: ",
407
- response_json["error"],
408
- )
409
- else:
393
+ method_func = getattr(session, self.endpoint.method, None)
394
+ if method_func is None:
410
395
  raise ValueError(
411
396
  f"Invalid HTTP method: {self.endpoint.method}"
412
397
  )
398
+ async with method_func(
399
+ self.endpoint.full_url, **kwargs
400
+ ) as response:
401
+ response_json = await response.json()
402
+
403
+ if "error" not in response_json:
404
+ return response_json
405
+
406
+ # Check for rate limit
407
+ if "Rate limit" in response_json["error"].get(
408
+ "message", ""
409
+ ):
410
+ await asyncio.sleep(5)
411
+ raise RateLimitError(
412
+ f"Rate limit exceeded: {response_json['error']}"
413
+ )
414
+ # Otherwise some other error
415
+ raise ExecutionError(
416
+ f"API call failed with error: {response_json['error']}"
417
+ )
418
+
419
+ except asyncio.CancelledError:
420
+ # Gracefully handle user cancellation
421
+ logging.warning("API call canceled by external request.")
422
+ raise # re-raise so caller knows it was cancelled
423
+
413
424
  except aiohttp.ClientError as e:
414
- logging.error(
415
- f"API call to {self.endpoint.full_url} failed: {e}"
416
- )
425
+ logging.error(f"API call failed: {e}")
426
+ # Return None or raise ExecutionError? Keep consistent
417
427
  return None
418
428
 
429
+ # Attempt up to 3 times if RateLimitError
419
430
  for i in range(3):
420
431
  try:
421
432
  return await retry_in()
422
- except RateLimitError | ExecutionError as e:
433
+ except asyncio.CancelledError:
434
+ # On cancel, just re-raise
435
+ raise
436
+ except RateLimitError as e:
423
437
  if i == 2:
424
438
  raise e
425
- else:
426
- wait = 2 ** (i + 1) * 0.5
427
- logging.warning(
428
- f"API call to {self.endpoint.full_url} failed: {e}"
429
- f"retrying in {wait} seconds."
430
- )
431
- await asyncio.sleep(wait)
439
+ wait = 2 ** (i + 1) * 0.5
440
+ logging.warning(f"RateLimitError: {e}, retrying in {wait}s.")
441
+ await asyncio.sleep(wait)
432
442
 
433
443
  @cached(**Settings.API.CACHED_CONFIG)
434
444
  async def _cached_inner(self, **kwargs) -> Any:
@@ -442,10 +452,91 @@ class APICalling(Event):
442
452
  """
443
453
  return await self._inner(**kwargs)
444
454
 
445
- async def stream(self, **kwargs):
455
+ async def _stream(
456
+ self,
457
+ verbose: bool = True,
458
+ output_file: str = None,
459
+ with_response_header: bool = False,
460
+ ) -> AsyncGenerator:
461
+ async with aiohttp.ClientSession() as client:
462
+ async with client.request(
463
+ method=self.endpoint.method.upper(),
464
+ url=self.endpoint.full_url,
465
+ headers=self.headers,
466
+ json=self.payload,
467
+ ) as response:
468
+ if response.status != 200:
469
+ try:
470
+ error_text = await response.json()
471
+ except Exception:
472
+ error_text = await response.text()
473
+ raise aiohttp.ClientResponseError(
474
+ request_info=response.request_info,
475
+ history=response.history,
476
+ status=response.status,
477
+ message=f"{error_text}",
478
+ headers=response.headers,
479
+ )
480
+
481
+ file_handle = None
482
+
483
+ if output_file:
484
+ try:
485
+ file_handle = open(output_file, "w")
486
+ except Exception as e:
487
+ raise ValueError(
488
+ f"Invalid to output the response "
489
+ f"to {output_file}. Error:{e}"
490
+ )
491
+
492
+ try:
493
+ async for chunk in response.content:
494
+ chunk_str = chunk.decode("utf-8")
495
+ chunk_list = chunk_str.split("data:")
496
+ for c in chunk_list:
497
+ c = c.strip()
498
+ if c and c != "[DONE]":
499
+ try:
500
+ if file_handle:
501
+ file_handle.write(c + "\n")
502
+ c_dict = json.loads(c)
503
+ if verbose:
504
+ if c_dict.get("choices"):
505
+ if content := c_dict["choices"][0][
506
+ "delta"
507
+ ].get("content"):
508
+ print(
509
+ content, end="", flush=True
510
+ )
511
+ yield c_dict
512
+ except json.JSONDecodeError:
513
+ yield c
514
+ except asyncio.CancelledError as e:
515
+ raise e
516
+
517
+ if with_response_header:
518
+ yield response.headers
519
+
520
+ finally:
521
+ if file_handle:
522
+ file_handle.close()
523
+
524
+ async def stream(
525
+ self,
526
+ verbose: bool = True,
527
+ output_file: str = None,
528
+ with_response_header: bool = False,
529
+ **kwargs,
530
+ ) -> AsyncGenerator:
446
531
  """Performs a streaming request, if supported by the endpoint.
447
532
 
448
533
  Args:
534
+ verbose (bool):
535
+ If True, prints the response content to the console.
536
+ output_file (str):
537
+ If set, writes the response content to this file. (only applies to non-endpoint invoke)
538
+ with_response_header (bool):
539
+ If True, yields the response headers as well. (only applies to non-endpoint invoke)
449
540
  **kwargs: Additional parameters for the streaming call.
450
541
 
451
542
  Yields:
@@ -456,23 +547,39 @@ class APICalling(Event):
456
547
  """
457
548
  start = asyncio.get_event_loop().time()
458
549
  response = []
459
- if not self.endpoint.is_streamable:
460
- raise ValueError(
461
- f"Endpoint {self.endpoint.endpoint} is not streamable."
462
- )
463
-
464
- async for i in self.endpoint._stream(
465
- self.payload, self.headers, **kwargs
466
- ):
467
- content = i.choices[0].delta.content
468
- if content is not None:
469
- print(content, end="", flush=True)
470
- response.append(i)
471
- yield i
472
-
473
- self.execution.duration = asyncio.get_event_loop().time() - start
474
- self.execution.response = response
475
- self.execution.status = EventStatus.COMPLETED
550
+ e1 = None
551
+ try:
552
+ if self.should_invoke_endpoint and self.endpoint.is_streamable:
553
+ async for i in self.endpoint._stream(
554
+ self.payload, self.headers, **kwargs
555
+ ):
556
+ content = i.choices[0].delta.content
557
+ if verbose:
558
+ if content is not None:
559
+ print(content, end="", flush=True)
560
+ response.append(i)
561
+ yield i
562
+ else:
563
+ async for i in self._stream(
564
+ verbose=verbose,
565
+ output_file=output_file,
566
+ with_response_header=with_response_header,
567
+ ):
568
+ response.append(i)
569
+ yield i
570
+ except Exception as e:
571
+ e1 = e
572
+ finally:
573
+ self.execution.duration = asyncio.get_event_loop().time() - start
574
+ if not response and e1:
575
+ self.execution.error = str(e1)
576
+ self.execution.status = EventStatus.FAILED
577
+ logging.error(
578
+ f"API call to {self.endpoint.full_url} failed: {e1}"
579
+ )
580
+ else:
581
+ self.execution.response = response
582
+ self.execution.status = EventStatus.COMPLETED
476
583
 
477
584
  async def invoke(self) -> None:
478
585
  """Invokes the API call, updating the execution state with results.
@@ -483,9 +590,10 @@ class APICalling(Event):
483
590
  """
484
591
  start = asyncio.get_event_loop().time()
485
592
  kwargs = {"headers": self.headers, "json": self.payload}
593
+ response = None
594
+ e1 = None
486
595
 
487
596
  try:
488
- response = None
489
597
  if self.should_invoke_endpoint and self.endpoint.is_invokeable:
490
598
  response = await self.endpoint.invoke(
491
599
  payload=self.payload,
@@ -498,14 +606,24 @@ class APICalling(Event):
498
606
  else:
499
607
  response = await self._inner(**kwargs)
500
608
 
609
+ except asyncio.CancelledError as ce:
610
+ e1 = ce
611
+ logging.warning("invoke() canceled by external request.")
612
+ raise
613
+ except Exception as ex:
614
+ e1 = ex
615
+
616
+ finally:
501
617
  self.execution.duration = asyncio.get_event_loop().time() - start
502
- self.execution.response = response
503
- self.execution.status = EventStatus.COMPLETED
504
- except Exception as e:
505
- self.execution.duration = asyncio.get_event_loop().time() - start
506
- self.execution.error = str(e)
507
- self.execution.status = EventStatus.FAILED
508
- logging.error(f"API call to {self.endpoint.full_url} failed: {e}")
618
+ if not response and e1:
619
+ self.execution.error = str(e1)
620
+ self.execution.status = EventStatus.FAILED
621
+ logging.error(
622
+ f"API call to {self.endpoint.full_url} failed: {e1}"
623
+ )
624
+ else:
625
+ self.execution.response = response
626
+ self.execution.status = EventStatus.COMPLETED
509
627
 
510
628
  def __str__(self) -> str:
511
629
  return (
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
@@ -27,10 +27,13 @@ class ChatCompletionEndPoint(EndPoint):
27
27
  headers: dict,
28
28
  **kwargs,
29
29
  ):
30
- import litellm
30
+ from lionagi.libs.package.imports import check_import
31
+
32
+ check_import("litellm")
33
+ import litellm # type: ignore
31
34
 
32
35
  litellm.drop_params = True
33
- from litellm import acompletion
36
+ from litellm import acompletion # type: ignore
34
37
 
35
38
  provider = self.config.provider
36
39
 
@@ -64,10 +67,13 @@ class ChatCompletionEndPoint(EndPoint):
64
67
  headers: dict,
65
68
  **kwargs,
66
69
  ) -> AsyncGenerator:
67
- import litellm
70
+ from lionagi.libs.package.imports import check_import
71
+
72
+ check_import("litellm")
73
+ import litellm # type: ignore
68
74
 
69
75
  litellm.drop_params = True
70
- from litellm import acompletion
76
+ from litellm import acompletion # type: ignore
71
77
 
72
78
  provider = self.config.provider
73
79
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2023 - 2024, HaiyangLi <quantocean.li at gmail dot com>
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4