lionagi 0.17.4__py3-none-any.whl → 0.17.6__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/__init__.py +45 -7
- lionagi/config.py +26 -0
- lionagi/fields/action.py +5 -3
- lionagi/libs/file/chunk.py +3 -14
- lionagi/libs/file/process.py +10 -92
- lionagi/libs/schema/breakdown_pydantic_annotation.py +45 -0
- lionagi/ln/_async_call.py +19 -8
- lionagi/ln/_hash.py +12 -2
- lionagi/ln/_to_list.py +23 -12
- lionagi/ln/fuzzy/_fuzzy_match.py +3 -6
- lionagi/ln/fuzzy/_fuzzy_validate.py +9 -8
- lionagi/ln/fuzzy/_string_similarity.py +11 -5
- lionagi/ln/fuzzy/_to_dict.py +19 -19
- lionagi/ln/types.py +15 -0
- lionagi/operations/operate/operate.py +7 -11
- lionagi/operations/parse/parse.py +5 -3
- lionagi/protocols/generic/element.py +3 -6
- lionagi/protocols/generic/event.py +1 -1
- lionagi/protocols/mail/package.py +2 -2
- lionagi/protocols/messages/instruction.py +9 -1
- lionagi/protocols/operatives/operative.py +4 -3
- lionagi/service/broadcaster.py +61 -0
- lionagi/service/connections/api_calling.py +22 -145
- lionagi/service/connections/mcp/wrapper.py +8 -15
- lionagi/service/hooks/__init__.py +2 -10
- lionagi/service/hooks/_types.py +1 -0
- lionagi/service/hooks/hooked_event.py +142 -0
- lionagi/service/imodel.py +2 -2
- lionagi/session/branch.py +46 -169
- lionagi/session/session.py +1 -44
- lionagi/tools/file/reader.py +6 -4
- lionagi/utils.py +3 -342
- lionagi/version.py +1 -1
- {lionagi-0.17.4.dist-info → lionagi-0.17.6.dist-info}/METADATA +4 -4
- {lionagi-0.17.4.dist-info → lionagi-0.17.6.dist-info}/RECORD +37 -41
- lionagi/libs/file/_utils.py +0 -10
- lionagi/libs/file/concat.py +0 -121
- lionagi/libs/file/concat_files.py +0 -85
- lionagi/libs/file/file_ops.py +0 -118
- lionagi/libs/file/save.py +0 -103
- lionagi/ln/concurrency/throttle.py +0 -83
- lionagi/settings.py +0 -71
- {lionagi-0.17.4.dist-info → lionagi-0.17.6.dist-info}/WHEEL +0 -0
- {lionagi-0.17.4.dist-info → lionagi-0.17.6.dist-info}/licenses/LICENSE +0 -0
lionagi/ln/fuzzy/_to_dict.py
CHANGED
@@ -9,10 +9,6 @@ from typing import Any, Literal
|
|
9
9
|
|
10
10
|
from ._fuzzy_json import fuzzy_json
|
11
11
|
|
12
|
-
# ----------------------------
|
13
|
-
# Helpers (small, tight, local)
|
14
|
-
# ----------------------------
|
15
|
-
|
16
12
|
|
17
13
|
def _is_na(obj: Any) -> bool:
|
18
14
|
"""None / Pydantic undefined sentinels -> treat as NA."""
|
@@ -67,7 +63,7 @@ def _parse_str(
|
|
67
63
|
def _object_to_mapping_like(
|
68
64
|
obj: Any,
|
69
65
|
*,
|
70
|
-
|
66
|
+
prioritize_model_dump: bool = True,
|
71
67
|
**kwargs: Any,
|
72
68
|
) -> Mapping | dict | Any:
|
73
69
|
"""
|
@@ -80,11 +76,11 @@ def _object_to_mapping_like(
|
|
80
76
|
5) dict(obj)
|
81
77
|
"""
|
82
78
|
# 1) Pydantic v2
|
83
|
-
if
|
79
|
+
if prioritize_model_dump and hasattr(obj, "model_dump"):
|
84
80
|
return obj.model_dump(**kwargs)
|
85
81
|
|
86
82
|
# 2) Common methods
|
87
|
-
for name in ("to_dict", "dict", "to_json", "json"):
|
83
|
+
for name in ("to_dict", "dict", "to_json", "json", "model_dump"):
|
88
84
|
if hasattr(obj, name):
|
89
85
|
res = getattr(obj, name)(**kwargs)
|
90
86
|
return json.loads(res) if isinstance(res, str) else res
|
@@ -118,7 +114,7 @@ def _preprocess_recursive(
|
|
118
114
|
max_depth: int,
|
119
115
|
recursive_custom_types: bool,
|
120
116
|
str_parse_opts: dict[str, Any],
|
121
|
-
|
117
|
+
prioritize_model_dump: bool,
|
122
118
|
) -> Any:
|
123
119
|
"""
|
124
120
|
Recursively process nested structures:
|
@@ -145,7 +141,7 @@ def _preprocess_recursive(
|
|
145
141
|
max_depth=max_depth,
|
146
142
|
recursive_custom_types=recursive_custom_types,
|
147
143
|
str_parse_opts=str_parse_opts,
|
148
|
-
|
144
|
+
prioritize_model_dump=prioritize_model_dump,
|
149
145
|
)
|
150
146
|
|
151
147
|
# Dict-like
|
@@ -158,7 +154,7 @@ def _preprocess_recursive(
|
|
158
154
|
max_depth=max_depth,
|
159
155
|
recursive_custom_types=recursive_custom_types,
|
160
156
|
str_parse_opts=str_parse_opts,
|
161
|
-
|
157
|
+
prioritize_model_dump=prioritize_model_dump,
|
162
158
|
)
|
163
159
|
for k, v in obj.items()
|
164
160
|
}
|
@@ -172,7 +168,7 @@ def _preprocess_recursive(
|
|
172
168
|
max_depth=max_depth,
|
173
169
|
recursive_custom_types=recursive_custom_types,
|
174
170
|
str_parse_opts=str_parse_opts,
|
175
|
-
|
171
|
+
prioritize_model_dump=prioritize_model_dump,
|
176
172
|
)
|
177
173
|
for v in obj
|
178
174
|
]
|
@@ -198,7 +194,7 @@ def _preprocess_recursive(
|
|
198
194
|
max_depth=max_depth,
|
199
195
|
recursive_custom_types=recursive_custom_types,
|
200
196
|
str_parse_opts=str_parse_opts,
|
201
|
-
|
197
|
+
prioritize_model_dump=prioritize_model_dump,
|
202
198
|
)
|
203
199
|
except Exception:
|
204
200
|
return obj
|
@@ -207,7 +203,7 @@ def _preprocess_recursive(
|
|
207
203
|
if recursive_custom_types:
|
208
204
|
with contextlib.suppress(Exception):
|
209
205
|
mapped = _object_to_mapping_like(
|
210
|
-
obj,
|
206
|
+
obj, prioritize_model_dump=prioritize_model_dump
|
211
207
|
)
|
212
208
|
return _preprocess_recursive(
|
213
209
|
mapped,
|
@@ -215,7 +211,7 @@ def _preprocess_recursive(
|
|
215
211
|
max_depth=max_depth,
|
216
212
|
recursive_custom_types=recursive_custom_types,
|
217
213
|
str_parse_opts=str_parse_opts,
|
218
|
-
|
214
|
+
prioritize_model_dump=prioritize_model_dump,
|
219
215
|
)
|
220
216
|
|
221
217
|
return obj
|
@@ -232,7 +228,7 @@ def _convert_top_level_to_dict(
|
|
232
228
|
fuzzy_parse: bool,
|
233
229
|
str_type: Literal["json", "xml"] | None,
|
234
230
|
parser: Callable[[str], Any] | None,
|
235
|
-
|
231
|
+
prioritize_model_dump: bool,
|
236
232
|
use_enum_values: bool,
|
237
233
|
**kwargs: Any,
|
238
234
|
) -> dict[str, Any]:
|
@@ -273,7 +269,7 @@ def _convert_top_level_to_dict(
|
|
273
269
|
# faithfully following your previous "non-Sequence -> model path" behavior.
|
274
270
|
if not isinstance(obj, Sequence):
|
275
271
|
converted = _object_to_mapping_like(
|
276
|
-
obj,
|
272
|
+
obj, prioritize_model_dump=prioritize_model_dump, **kwargs
|
277
273
|
)
|
278
274
|
# If conversion returned a string, try to parse JSON to mapping; else pass-through
|
279
275
|
if isinstance(converted, str):
|
@@ -321,7 +317,7 @@ def to_dict(
|
|
321
317
|
input_: Any,
|
322
318
|
/,
|
323
319
|
*,
|
324
|
-
|
320
|
+
prioritize_model_dump: bool = True,
|
325
321
|
fuzzy_parse: bool = False,
|
326
322
|
suppress: bool = False,
|
327
323
|
str_type: Literal["json", "xml"] | None = "json",
|
@@ -330,12 +326,16 @@ def to_dict(
|
|
330
326
|
max_recursive_depth: int | None = None,
|
331
327
|
recursive_python_only: bool = True,
|
332
328
|
use_enum_values: bool = False,
|
329
|
+
use_model_dump: bool | None = None, # deprecated
|
333
330
|
**kwargs: Any,
|
334
331
|
) -> dict[str, Any]:
|
335
332
|
"""
|
336
333
|
Convert various input types to a dictionary, with optional recursive processing.
|
337
334
|
Semantics preserved from original implementation.
|
338
335
|
"""
|
336
|
+
if use_model_dump is not None:
|
337
|
+
prioritize_model_dump = use_model_dump
|
338
|
+
|
339
339
|
try:
|
340
340
|
# Clamp recursion depth (match your constraints)
|
341
341
|
if not isinstance(max_recursive_depth, int):
|
@@ -368,7 +368,7 @@ def to_dict(
|
|
368
368
|
max_depth=max_depth,
|
369
369
|
recursive_custom_types=not recursive_python_only,
|
370
370
|
str_parse_opts=str_parse_opts,
|
371
|
-
|
371
|
+
prioritize_model_dump=prioritize_model_dump,
|
372
372
|
)
|
373
373
|
|
374
374
|
# Final top-level conversion
|
@@ -377,7 +377,7 @@ def to_dict(
|
|
377
377
|
fuzzy_parse=fuzzy_parse,
|
378
378
|
str_type=str_type,
|
379
379
|
parser=parser,
|
380
|
-
|
380
|
+
prioritize_model_dump=prioritize_model_dump,
|
381
381
|
use_enum_values=use_enum_values,
|
382
382
|
**kwargs,
|
383
383
|
)
|
lionagi/ln/types.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from collections.abc import Sequence
|
3
4
|
from dataclasses import dataclass, field
|
4
5
|
from enum import Enum as _Enum
|
5
6
|
from typing import Any, ClassVar, Final, Literal, TypeVar, Union
|
@@ -22,6 +23,7 @@ __all__ = (
|
|
22
23
|
"not_sentinel",
|
23
24
|
"Params",
|
24
25
|
"DataClass",
|
26
|
+
"KeysLike",
|
25
27
|
)
|
26
28
|
|
27
29
|
T = TypeVar("T")
|
@@ -236,6 +238,16 @@ class Params:
|
|
236
238
|
data[k] = v
|
237
239
|
return data
|
238
240
|
|
241
|
+
def __hash__(self) -> int:
|
242
|
+
from ._hash import hash_dict
|
243
|
+
|
244
|
+
return hash_dict(self.to_dict())
|
245
|
+
|
246
|
+
def __eq__(self, other: Any) -> bool:
|
247
|
+
if not isinstance(other, Params):
|
248
|
+
return False
|
249
|
+
return hash(self) == hash(other)
|
250
|
+
|
239
251
|
|
240
252
|
@dataclass(slots=True)
|
241
253
|
class DataClass:
|
@@ -297,3 +309,6 @@ class DataClass:
|
|
297
309
|
if value is None and cls._none_as_sentinel:
|
298
310
|
return True
|
299
311
|
return is_sentinel(value)
|
312
|
+
|
313
|
+
|
314
|
+
KeysLike = Sequence[str] | KeysDict
|
@@ -12,6 +12,7 @@ from lionagi.models import FieldModel, ModelParams
|
|
12
12
|
from lionagi.protocols.operatives.step import Operative, Step
|
13
13
|
from lionagi.protocols.types import Instruction, Progression, SenderRecipient
|
14
14
|
from lionagi.service.imodel import iModel
|
15
|
+
from lionagi.session.branch import AlcallParams
|
15
16
|
|
16
17
|
if TYPE_CHECKING:
|
17
18
|
from lionagi.session.branch import Branch, ToolRef
|
@@ -64,10 +65,8 @@ async def operate(
|
|
64
65
|
return_operative: bool = False,
|
65
66
|
actions: bool = False,
|
66
67
|
reason: bool = False,
|
67
|
-
|
68
|
-
action_strategy: Literal[
|
69
|
-
"sequential", "concurrent", "batch"
|
70
|
-
] = "concurrent",
|
68
|
+
call_params: AlcallParams = None,
|
69
|
+
action_strategy: Literal["sequential", "concurrent"] = "concurrent",
|
71
70
|
verbose_action: bool = False,
|
72
71
|
field_models: list[FieldModel] = None,
|
73
72
|
exclude_fields: list | dict | None = None,
|
@@ -191,17 +190,14 @@ async def operate(
|
|
191
190
|
getattr(response_model, "action_required", None) is True
|
192
191
|
and getattr(response_model, "action_requests", None) is not None
|
193
192
|
):
|
194
|
-
|
195
|
-
|
196
|
-
instruct.action_strategy
|
197
|
-
if instruct.action_strategy
|
198
|
-
else action_kwargs.get("strategy", "concurrent")
|
193
|
+
action_strategy = (
|
194
|
+
action_strategy or instruct.action_strategy or "concurrent"
|
199
195
|
)
|
200
|
-
|
201
196
|
action_response_models = await branch.act(
|
202
197
|
response_model.action_requests,
|
198
|
+
strategy=action_strategy,
|
203
199
|
verbose_action=verbose_action,
|
204
|
-
|
200
|
+
call_params=call_params,
|
205
201
|
)
|
206
202
|
# Possibly refine the operative with the tool outputs
|
207
203
|
operative = Step.respond_operative(
|
@@ -6,9 +6,6 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
6
6
|
|
7
7
|
from pydantic import BaseModel
|
8
8
|
|
9
|
-
from lionagi.ln.fuzzy._fuzzy_validate import fuzzy_validate_mapping
|
10
|
-
from lionagi.utils import breakdown_pydantic_annotation
|
11
|
-
|
12
9
|
if TYPE_CHECKING:
|
13
10
|
from lionagi.session.branch import Branch
|
14
11
|
|
@@ -34,6 +31,11 @@ async def parse(
|
|
34
31
|
suppress_conversion_errors: bool = False,
|
35
32
|
response_format=None,
|
36
33
|
):
|
34
|
+
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
35
|
+
breakdown_pydantic_annotation,
|
36
|
+
)
|
37
|
+
from lionagi.ln.fuzzy._fuzzy_validate import fuzzy_validate_mapping
|
38
|
+
|
37
39
|
if operative is not None:
|
38
40
|
max_retries = operative.max_retries
|
39
41
|
response_format = operative.request_type or response_format
|
@@ -21,8 +21,7 @@ from pydantic import (
|
|
21
21
|
from lionagi import ln
|
22
22
|
from lionagi._class_registry import get_class
|
23
23
|
from lionagi._errors import IDError
|
24
|
-
from lionagi.
|
25
|
-
from lionagi.utils import import_module, time, to_dict
|
24
|
+
from lionagi.utils import import_module, to_dict
|
26
25
|
|
27
26
|
from .._concepts import Collective, Observable, Ordering
|
28
27
|
|
@@ -156,9 +155,7 @@ class Element(BaseModel, Observable):
|
|
156
155
|
frozen=True,
|
157
156
|
)
|
158
157
|
created_at: float = Field(
|
159
|
-
default_factory=lambda:
|
160
|
-
tz=Settings.Config.TIMEZONE, type_="timestamp"
|
161
|
-
),
|
158
|
+
default_factory=lambda: ln.now_utc().timestamp(),
|
162
159
|
title="Creation Timestamp",
|
163
160
|
description="Timestamp of element creation.",
|
164
161
|
frozen=True,
|
@@ -205,7 +202,7 @@ class Element(BaseModel, Observable):
|
|
205
202
|
ValueError: If `val` cannot be converted to a float timestamp.
|
206
203
|
"""
|
207
204
|
if val is None:
|
208
|
-
return
|
205
|
+
return ln.now_utc().timestamp()
|
209
206
|
if isinstance(val, float):
|
210
207
|
return val
|
211
208
|
if isinstance(val, dt.datetime):
|
@@ -138,7 +138,7 @@ class Event(Element):
|
|
138
138
|
"""
|
139
139
|
|
140
140
|
execution: Execution = Field(default_factory=Execution)
|
141
|
-
streaming: bool = False
|
141
|
+
streaming: bool = Field(False, exclude=True)
|
142
142
|
|
143
143
|
@field_serializer("execution")
|
144
144
|
def _serialize_execution(self, val: Execution) -> dict:
|
@@ -5,8 +5,8 @@
|
|
5
5
|
from enum import Enum
|
6
6
|
from typing import Any
|
7
7
|
|
8
|
+
from lionagi.ln import now_utc
|
8
9
|
from lionagi.protocols.generic.element import ID, IDType
|
9
|
-
from lionagi.utils import time
|
10
10
|
|
11
11
|
from .._concepts import Communicatable, Observable
|
12
12
|
|
@@ -93,7 +93,7 @@ class Package(Observable):
|
|
93
93
|
):
|
94
94
|
super().__init__()
|
95
95
|
self.id = IDType.create()
|
96
|
-
self.created_at =
|
96
|
+
self.created_at = now_utc().timestamp()
|
97
97
|
self.category = validate_category(category)
|
98
98
|
self.item = item
|
99
99
|
self.request_source = request_source
|
@@ -7,7 +7,7 @@ from typing import Any, Literal
|
|
7
7
|
from pydantic import BaseModel, JsonValue, field_serializer
|
8
8
|
from typing_extensions import override
|
9
9
|
|
10
|
-
from lionagi.utils import UNDEFINED,
|
10
|
+
from lionagi.utils import UNDEFINED, copy
|
11
11
|
|
12
12
|
from .base import MessageRole
|
13
13
|
from .message import RoledMessage, SenderRecipient
|
@@ -256,6 +256,10 @@ def prepare_instruction_content(
|
|
256
256
|
Raises:
|
257
257
|
ValueError: If request_fields and request_model are both given.
|
258
258
|
"""
|
259
|
+
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
260
|
+
breakdown_pydantic_annotation,
|
261
|
+
)
|
262
|
+
|
259
263
|
if request_fields and request_model:
|
260
264
|
raise ValueError(
|
261
265
|
"only one of request_fields or request_model can be provided"
|
@@ -476,6 +480,10 @@ class Instruction(RoledMessage):
|
|
476
480
|
|
477
481
|
@response_format.setter
|
478
482
|
def response_format(self, model: type[BaseModel]) -> None:
|
483
|
+
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
484
|
+
breakdown_pydantic_annotation,
|
485
|
+
)
|
486
|
+
|
479
487
|
if isinstance(model, BaseModel):
|
480
488
|
self.content["request_model"] = type(model)
|
481
489
|
else:
|
@@ -7,9 +7,10 @@ from typing import Any
|
|
7
7
|
from pydantic import BaseModel
|
8
8
|
from pydantic.fields import FieldInfo
|
9
9
|
|
10
|
+
from lionagi.ln import extract_json
|
10
11
|
from lionagi.ln.fuzzy._fuzzy_match import fuzzy_match_keys
|
11
12
|
from lionagi.models import FieldModel, ModelParams, OperableModel
|
12
|
-
from lionagi.utils import UNDEFINED
|
13
|
+
from lionagi.utils import UNDEFINED
|
13
14
|
|
14
15
|
|
15
16
|
class Operative:
|
@@ -145,7 +146,7 @@ class Operative:
|
|
145
146
|
Raises:
|
146
147
|
Exception: If the validation fails.
|
147
148
|
"""
|
148
|
-
d_ =
|
149
|
+
d_ = extract_json(text, fuzzy_parse=True)
|
149
150
|
if isinstance(d_, list | tuple) and len(d_) == 1:
|
150
151
|
d_ = d_[0]
|
151
152
|
try:
|
@@ -167,7 +168,7 @@ class Operative:
|
|
167
168
|
"""
|
168
169
|
d_ = text
|
169
170
|
try:
|
170
|
-
d_ =
|
171
|
+
d_ = extract_json(text, fuzzy_parse=True)
|
171
172
|
if isinstance(d_, list | tuple) and len(d_) == 1:
|
172
173
|
d_ = d_[0]
|
173
174
|
d_ = fuzzy_match_keys(
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from collections.abc import Callable
|
5
|
+
from typing import Any, ClassVar
|
6
|
+
|
7
|
+
from lionagi.ln.concurrency.utils import is_coro_func
|
8
|
+
from lionagi.protocols.generic.event import Event
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
__all__ = ("Broadcaster",)
|
13
|
+
|
14
|
+
|
15
|
+
class Broadcaster:
|
16
|
+
"""Real-time event broadcasting system for hook events. Should subclass to implement specific event types."""
|
17
|
+
|
18
|
+
_instance: ClassVar[Broadcaster | None] = None
|
19
|
+
_subscribers: ClassVar[list[Callable[[Any], None]]] = []
|
20
|
+
_event_type: ClassVar[type[Event]]
|
21
|
+
|
22
|
+
def __new__(cls):
|
23
|
+
if cls._instance is None:
|
24
|
+
cls._instance = super().__new__(cls)
|
25
|
+
return cls._instance
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def subscribe(cls, callback: Callable[[Any], None]) -> None:
|
29
|
+
"""Subscribe to hook events with sync callback."""
|
30
|
+
if callback not in cls._subscribers:
|
31
|
+
cls._subscribers.append(callback)
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def unsubscribe(cls, callback: Callable[[Any], None]) -> None:
|
35
|
+
"""Unsubscribe from hook events."""
|
36
|
+
if callback in cls._subscribers:
|
37
|
+
cls._subscribers.remove(callback)
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
async def broadcast(cls, event) -> None:
|
41
|
+
"""Broadcast event to all subscribers."""
|
42
|
+
if not isinstance(event, cls._event_type):
|
43
|
+
raise ValueError(
|
44
|
+
f"Event must be of type {cls._event_type.__name__}"
|
45
|
+
)
|
46
|
+
|
47
|
+
for callback in cls._subscribers:
|
48
|
+
try:
|
49
|
+
if is_coro_func(callback):
|
50
|
+
await callback(event)
|
51
|
+
else:
|
52
|
+
callback(event)
|
53
|
+
except Exception as e:
|
54
|
+
logger.error(
|
55
|
+
f"Error in subscriber callback: {e}", exc_info=True
|
56
|
+
)
|
57
|
+
|
58
|
+
@classmethod
|
59
|
+
def get_subscriber_count(cls) -> int:
|
60
|
+
"""Get total number of subscribers."""
|
61
|
+
return len(cls._subscribers)
|
@@ -2,31 +2,18 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
|
5
|
-
import asyncio
|
6
5
|
import logging
|
7
6
|
|
8
|
-
from
|
9
|
-
from pydantic import Field, PrivateAttr, model_validator
|
7
|
+
from pydantic import Field, model_validator
|
10
8
|
from typing_extensions import Self
|
11
9
|
|
12
|
-
from
|
13
|
-
from lionagi.protocols.types import Log
|
14
|
-
from lionagi.service.hooks import HookEvent, HookEventTypes, global_hook_logger
|
15
|
-
|
10
|
+
from ..hooks.hooked_event import HookedEvent
|
16
11
|
from .endpoint import Endpoint
|
17
12
|
|
18
|
-
|
19
|
-
# Lazy import for TokenCalculator
|
20
|
-
def _get_token_calculator():
|
21
|
-
from lionagi.service.token_calculator import TokenCalculator
|
22
|
-
|
23
|
-
return TokenCalculator
|
24
|
-
|
25
|
-
|
26
13
|
logger = logging.getLogger(__name__)
|
27
14
|
|
28
15
|
|
29
|
-
class APICalling(
|
16
|
+
class APICalling(HookedEvent):
|
30
17
|
"""Handles asynchronous API calls with automatic token usage tracking.
|
31
18
|
|
32
19
|
This class manages API calls through endpoints, handling both regular
|
@@ -61,9 +48,6 @@ class APICalling(Event):
|
|
61
48
|
exclude=True,
|
62
49
|
)
|
63
50
|
|
64
|
-
_pre_invoke_hook_event: HookEvent = PrivateAttr(None)
|
65
|
-
_post_invoke_hook_event: HookEvent = PrivateAttr(None)
|
66
|
-
|
67
51
|
@model_validator(mode="after")
|
68
52
|
def _validate_streaming(self) -> Self:
|
69
53
|
"""Validate streaming configuration and add token usage if requested."""
|
@@ -85,20 +69,16 @@ class APICalling(Event):
|
|
85
69
|
TOKEN_LIMITS = {
|
86
70
|
# OpenAI models
|
87
71
|
"gpt-4": 128_000,
|
88
|
-
"gpt-4-turbo": 128_000,
|
89
|
-
"o1-mini": 128_000,
|
90
|
-
"o1-preview": 128_000,
|
91
72
|
"o1": 200_000,
|
92
73
|
"o3": 200_000,
|
93
74
|
"gpt-4.1": 1_000_000,
|
75
|
+
"gpt-5": 1_000_000,
|
94
76
|
# Anthropic models
|
95
77
|
"sonnet": 200_000,
|
96
78
|
"haiku": 200_000,
|
97
79
|
"opus": 200_000,
|
98
80
|
# Google models
|
99
81
|
"gemini": 1_000_000,
|
100
|
-
# Alibaba models
|
101
|
-
"qwen-turbo": 1_000_000,
|
102
82
|
}
|
103
83
|
|
104
84
|
token_msg = (
|
@@ -131,12 +111,14 @@ class APICalling(Event):
|
|
131
111
|
@property
|
132
112
|
def required_tokens(self) -> int | None:
|
133
113
|
"""Calculate the number of tokens required for this request."""
|
114
|
+
from lionagi.service.token_calculator import TokenCalculator
|
115
|
+
|
134
116
|
if not self.endpoint.config.requires_tokens:
|
135
117
|
return None
|
136
118
|
|
137
119
|
# Handle chat completions format
|
138
120
|
if "messages" in self.payload:
|
139
|
-
return
|
121
|
+
return TokenCalculator.calculate_message_tokens(
|
140
122
|
self.payload["messages"], **self.payload
|
141
123
|
)
|
142
124
|
# Handle responses API format
|
@@ -157,95 +139,29 @@ class APICalling(Event):
|
|
157
139
|
messages.append(item)
|
158
140
|
else:
|
159
141
|
return None
|
160
|
-
return
|
142
|
+
return TokenCalculator.calculate_message_tokens(
|
161
143
|
messages, **self.payload
|
162
144
|
)
|
163
145
|
# Handle embeddings endpoint
|
164
146
|
elif "embed" in self.endpoint.config.endpoint:
|
165
|
-
return
|
166
|
-
**self.payload
|
167
|
-
)
|
147
|
+
return TokenCalculator.calculate_embed_token(**self.payload)
|
168
148
|
|
169
149
|
return None
|
170
150
|
|
171
|
-
async def
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
try:
|
179
|
-
self.execution.status = EventStatus.PROCESSING
|
180
|
-
if h_ev := self._pre_invoke_hook_event:
|
181
|
-
await h_ev.invoke()
|
182
|
-
if h_ev._should_exit:
|
183
|
-
raise h_ev._exit_cause or RuntimeError(
|
184
|
-
"Pre-invocation hook requested exit without a cause"
|
185
|
-
)
|
186
|
-
await global_hook_logger.alog(Log.create(h_ev))
|
187
|
-
|
188
|
-
# Make the API call with skip_payload_creation=True since payload is already prepared
|
189
|
-
response = await self.endpoint.call(
|
190
|
-
request=self.payload,
|
191
|
-
cache_control=self.cache_control,
|
192
|
-
skip_payload_creation=True,
|
193
|
-
extra_headers=self.headers if self.headers else None,
|
194
|
-
)
|
195
|
-
|
196
|
-
if h_ev := self._post_invoke_hook_event:
|
197
|
-
await h_ev.invoke()
|
198
|
-
if h_ev._should_exit:
|
199
|
-
raise h_ev._exit_cause or RuntimeError(
|
200
|
-
"Post-invocation hook requested exit without a cause"
|
201
|
-
)
|
202
|
-
await global_hook_logger.alog(Log.create(h_ev))
|
203
|
-
|
204
|
-
self.execution.response = response
|
205
|
-
self.execution.status = EventStatus.COMPLETED
|
206
|
-
|
207
|
-
except get_cancelled_exc_class():
|
208
|
-
self.execution.error = "API call cancelled"
|
209
|
-
self.execution.status = EventStatus.CANCELLED
|
210
|
-
raise
|
211
|
-
|
212
|
-
except Exception as e:
|
213
|
-
self.execution.error = str(e)
|
214
|
-
self.execution.status = EventStatus.FAILED
|
215
|
-
logger.error(f"API call failed: {e}")
|
216
|
-
|
217
|
-
finally:
|
218
|
-
self.execution.duration = asyncio.get_event_loop().time() - start
|
219
|
-
|
220
|
-
async def stream(self):
|
221
|
-
"""Stream the API response through the endpoint.
|
222
|
-
|
223
|
-
Yields:
|
224
|
-
Streaming chunks from the API.
|
225
|
-
"""
|
226
|
-
start = asyncio.get_event_loop().time()
|
227
|
-
response = []
|
228
|
-
|
229
|
-
try:
|
230
|
-
self.execution.status = EventStatus.PROCESSING
|
231
|
-
|
232
|
-
async for chunk in self.endpoint.stream(
|
233
|
-
request=self.payload,
|
234
|
-
extra_headers=self.headers if self.headers else None,
|
235
|
-
):
|
236
|
-
response.append(chunk)
|
237
|
-
yield chunk
|
238
|
-
|
239
|
-
self.execution.response = response
|
240
|
-
self.execution.status = EventStatus.COMPLETED
|
241
|
-
|
242
|
-
except Exception as e:
|
243
|
-
self.execution.error = str(e)
|
244
|
-
self.execution.status = EventStatus.FAILED
|
245
|
-
logger.error(f"Streaming failed: {e}")
|
151
|
+
async def _invoke(self):
|
152
|
+
return await self.endpoint.call(
|
153
|
+
request=self.payload,
|
154
|
+
cache_control=self.cache_control,
|
155
|
+
skip_payload_creation=True,
|
156
|
+
extra_headers=self.headers if self.headers else None,
|
157
|
+
)
|
246
158
|
|
247
|
-
|
248
|
-
|
159
|
+
async def _stream(self):
|
160
|
+
async for i in self.endpoint.stream(
|
161
|
+
request=self.payload,
|
162
|
+
extra_headers=self.headers if self.headers else None,
|
163
|
+
):
|
164
|
+
yield i
|
249
165
|
|
250
166
|
@property
|
251
167
|
def request(self) -> dict:
|
@@ -253,42 +169,3 @@ class APICalling(Event):
|
|
253
169
|
return {
|
254
170
|
"required_tokens": self.required_tokens,
|
255
171
|
}
|
256
|
-
|
257
|
-
@property
|
258
|
-
def response(self):
|
259
|
-
"""Get the response from the execution."""
|
260
|
-
return self.execution.response if self.execution else None
|
261
|
-
|
262
|
-
def create_pre_invoke_hook(
|
263
|
-
self,
|
264
|
-
hook_registry,
|
265
|
-
exit_hook: bool = None,
|
266
|
-
hook_timeout: float = 30.0,
|
267
|
-
hook_params: dict = None,
|
268
|
-
):
|
269
|
-
h_ev = HookEvent(
|
270
|
-
hook_type=HookEventTypes.PreInvokation,
|
271
|
-
event_like=self,
|
272
|
-
registry=hook_registry,
|
273
|
-
exit=exit_hook,
|
274
|
-
timeout=hook_timeout,
|
275
|
-
params=hook_params or {},
|
276
|
-
)
|
277
|
-
self._pre_invoke_hook_event = h_ev
|
278
|
-
|
279
|
-
def create_post_invoke_hook(
|
280
|
-
self,
|
281
|
-
hook_registry,
|
282
|
-
exit_hook: bool = None,
|
283
|
-
hook_timeout: float = 30.0,
|
284
|
-
hook_params: dict = None,
|
285
|
-
):
|
286
|
-
h_ev = HookEvent(
|
287
|
-
hook_type=HookEventTypes.PostInvokation,
|
288
|
-
event_like=self,
|
289
|
-
registry=hook_registry,
|
290
|
-
exit=exit_hook,
|
291
|
-
timeout=hook_timeout,
|
292
|
-
params=hook_params or {},
|
293
|
-
)
|
294
|
-
self._post_invoke_hook_event = h_ev
|