lionagi 0.14.8__py3-none-any.whl → 0.14.10__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 (54) hide show
  1. lionagi/_errors.py +120 -11
  2. lionagi/_types.py +0 -6
  3. lionagi/config.py +3 -1
  4. lionagi/fields/reason.py +1 -1
  5. lionagi/libs/concurrency/throttle.py +79 -0
  6. lionagi/libs/parse.py +2 -1
  7. lionagi/libs/unstructured/__init__.py +0 -0
  8. lionagi/libs/unstructured/pdf_to_image.py +45 -0
  9. lionagi/libs/unstructured/read_image_to_base64.py +33 -0
  10. lionagi/libs/validate/to_num.py +378 -0
  11. lionagi/libs/validate/xml_parser.py +203 -0
  12. lionagi/models/operable_model.py +8 -3
  13. lionagi/operations/flow.py +0 -1
  14. lionagi/protocols/generic/event.py +2 -0
  15. lionagi/protocols/generic/log.py +26 -10
  16. lionagi/protocols/operatives/step.py +1 -1
  17. lionagi/protocols/types.py +9 -1
  18. lionagi/service/__init__.py +22 -1
  19. lionagi/service/connections/api_calling.py +57 -2
  20. lionagi/service/connections/endpoint_config.py +1 -1
  21. lionagi/service/connections/header_factory.py +4 -2
  22. lionagi/service/connections/match_endpoint.py +10 -10
  23. lionagi/service/connections/providers/anthropic_.py +5 -2
  24. lionagi/service/connections/providers/claude_code_.py +13 -17
  25. lionagi/service/connections/providers/claude_code_cli.py +51 -16
  26. lionagi/service/connections/providers/exa_.py +5 -3
  27. lionagi/service/connections/providers/oai_.py +116 -81
  28. lionagi/service/connections/providers/ollama_.py +38 -18
  29. lionagi/service/connections/providers/perplexity_.py +36 -14
  30. lionagi/service/connections/providers/types.py +30 -0
  31. lionagi/service/hooks/__init__.py +25 -0
  32. lionagi/service/hooks/_types.py +52 -0
  33. lionagi/service/hooks/_utils.py +85 -0
  34. lionagi/service/hooks/hook_event.py +67 -0
  35. lionagi/service/hooks/hook_registry.py +221 -0
  36. lionagi/service/imodel.py +120 -34
  37. lionagi/service/third_party/claude_code.py +715 -0
  38. lionagi/service/third_party/openai_model_names.py +198 -0
  39. lionagi/service/third_party/pplx_models.py +16 -8
  40. lionagi/service/types.py +21 -0
  41. lionagi/session/branch.py +1 -4
  42. lionagi/tools/base.py +1 -3
  43. lionagi/tools/file/reader.py +1 -1
  44. lionagi/tools/memory/tools.py +2 -2
  45. lionagi/utils.py +12 -775
  46. lionagi/version.py +1 -1
  47. {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/METADATA +6 -2
  48. {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/RECORD +50 -40
  49. lionagi/service/connections/providers/_claude_code/__init__.py +0 -3
  50. lionagi/service/connections/providers/_claude_code/models.py +0 -244
  51. lionagi/service/connections/providers/_claude_code/stream_cli.py +0 -359
  52. lionagi/service/third_party/openai_models.py +0 -18241
  53. {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/WHEEL +0 -0
  54. {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,221 @@
1
+ # Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, TypeVar
7
+
8
+ from lionagi.libs.concurrency import get_cancelled_exc_class
9
+ from lionagi.protocols.types import Event, EventStatus
10
+ from lionagi.utils import UNDEFINED
11
+
12
+ from ._types import HookDict, HookEventTypes, StreamHandlers
13
+ from ._utils import validate_hooks, validate_stream_handlers
14
+
15
+ E = TypeVar("E", bound=Event)
16
+
17
+
18
+ class HookRegistry:
19
+ def __init__(
20
+ self,
21
+ hooks: HookDict = None,
22
+ stream_handlers: StreamHandlers = None,
23
+ ):
24
+ _hooks = {}
25
+ _stream_handlers = {}
26
+
27
+ if hooks is not None:
28
+ validate_hooks(hooks)
29
+ _hooks.update(hooks)
30
+
31
+ if stream_handlers is not None:
32
+ validate_stream_handlers(stream_handlers)
33
+ _stream_handlers.update(stream_handlers)
34
+
35
+ self._hooks = _hooks
36
+ self._stream_handlers = _stream_handlers
37
+
38
+ async def _call(
39
+ self,
40
+ ht_: HookEventTypes,
41
+ ct_: str | type,
42
+ ch_: Any,
43
+ ev_: E | type[E],
44
+ /,
45
+ **kw,
46
+ ) -> tuple[Any | Exception, bool]:
47
+ if ht_ is None and ct_ is None:
48
+ raise RuntimeError(
49
+ "Either hook_type or chunk_type must be provided"
50
+ )
51
+ if ht_ and (h := self._hooks.get(ht_)):
52
+ validate_hooks({ht_: h})
53
+ return await h(ev_, **kw)
54
+ elif not ct_:
55
+ raise RuntimeError(
56
+ "Hook type is required when chunk_type is not provided"
57
+ )
58
+ else:
59
+ validate_stream_handlers(
60
+ {ct_: (h := self._stream_handlers.get(ct_))}
61
+ )
62
+ return await h(ev_, ct_, ch_, **kw)
63
+
64
+ async def _call_stream_handler(
65
+ self,
66
+ ct_: str | type,
67
+ ch_: Any,
68
+ ev_,
69
+ /,
70
+ **kw,
71
+ ):
72
+ handler = self._stream_handlers.get(ct_)
73
+ validate_stream_handlers({ct_: handler})
74
+ return await handler(ev_, ct_, ch_, **kw)
75
+
76
+ async def pre_event_create(
77
+ self, event_type: type[E], /, exit: bool = False, **kw
78
+ ) -> tuple[E | Exception | None, bool, EventStatus]:
79
+ """Hook to be called before an event is created.
80
+
81
+ Typically used to modify or validate the event creation parameters.
82
+
83
+ The hook function takes an event type and any additional keyword arguments.
84
+ It can:
85
+ - return an instance of the event type
86
+ - return None if no event should be created during handling, event will be
87
+ created in corresponding default manner
88
+ - raise an exception if this event should be cancelled
89
+ (status: cancelled, reason: f"pre-event-create hook aborted this event: {e}")
90
+ """
91
+ try:
92
+ res = await self._call(
93
+ HookEventTypes.PreEventCreate,
94
+ None,
95
+ None,
96
+ event_type,
97
+ **kw,
98
+ )
99
+ return (res, False, EventStatus.COMPLETED)
100
+ except get_cancelled_exc_class() as e:
101
+ return ((UNDEFINED, e), True, EventStatus.CANCELLED)
102
+ except Exception as e:
103
+ return (e, exit, EventStatus.CANCELLED)
104
+
105
+ async def pre_invokation(
106
+ self, event: E, /, exit: bool = False, **kw
107
+ ) -> tuple[Any, bool, EventStatus]:
108
+ """Hook to be called when an event is dequeued and right before it is invoked.
109
+
110
+ Typically used to check permissions.
111
+
112
+ The hook function takes the content of the event as a dictionary.
113
+ It can either raise an exception to abort the event invokation or pass to continue (status: cancelled).
114
+ It cannot modify the event itself, and won't be able to access the event instance.
115
+ """
116
+ try:
117
+ res = await self._call(
118
+ HookEventTypes.PreInvokation,
119
+ None,
120
+ None,
121
+ event,
122
+ **kw,
123
+ )
124
+ return (res, False, EventStatus.COMPLETED)
125
+ except get_cancelled_exc_class() as e:
126
+ return ((UNDEFINED, e), True, EventStatus.CANCELLED)
127
+ except Exception as e:
128
+ return (e, exit, EventStatus.CANCELLED)
129
+
130
+ async def post_invokation(
131
+ self, event: E, /, exit: bool = False, **kw
132
+ ) -> tuple[None | Exception, bool, EventStatus, EventStatus]:
133
+ """Hook to be called right after event finished its execution.
134
+ It can either raise an exception to abort the event invokation or pass to continue (status: aborted).
135
+ It cannot modify the event itself, and won't be able to access the event instance.
136
+ """
137
+ try:
138
+ res = await self._call(
139
+ HookEventTypes.PostInvokation,
140
+ None,
141
+ None,
142
+ event,
143
+ **kw,
144
+ )
145
+ return (res, False, EventStatus.COMPLETED)
146
+ except get_cancelled_exc_class() as e:
147
+ return ((UNDEFINED, e), True, EventStatus.CANCELLED)
148
+ except Exception as e:
149
+ return (e, exit, EventStatus.ABORTED)
150
+
151
+ async def handle_streaming_chunk(
152
+ self, chunk_type: str | type, chunk: Any, /, exit: bool = False, **kw
153
+ ) -> tuple[Any, bool, EventStatus | None]:
154
+ """Hook to be called to consume streaming chunks.
155
+
156
+ Typically used for logging or stream event abortion.
157
+
158
+ The handler function signature should be: `async def handler(chunk: Any) -> None`
159
+ It can either raise an exception to mark the event invokation as "failed" or pass to continue (status: aborted).
160
+ """
161
+ try:
162
+ res = await self._call_stream_handler(
163
+ chunk_type,
164
+ chunk,
165
+ None,
166
+ **kw,
167
+ )
168
+ return (res, False, None)
169
+ except get_cancelled_exc_class() as e:
170
+ return ((UNDEFINED, e), True, EventStatus.CANCELLED)
171
+ except Exception as e:
172
+ return (e, exit, EventStatus.ABORTED)
173
+
174
+ async def call(
175
+ self,
176
+ event_like: Event | type[Event],
177
+ /,
178
+ *,
179
+ hook_type: HookEventTypes = None,
180
+ chunk_type=None,
181
+ chunk=None,
182
+ exit=False,
183
+ **kw,
184
+ ):
185
+ """Call a hook or stream handler.
186
+
187
+ If method is provided, it will call the corresponding hook.
188
+ If chunk_type is provided, it will call the corresponding stream handler.
189
+ If both are provided, method will be used.
190
+ """
191
+ if hook_type is None and chunk_type is None:
192
+ raise ValueError("Either method or chunk_type must be provided")
193
+ if hook_type:
194
+ meta = {}
195
+ meta["event_type"] = event_like.class_name(full=True)
196
+ match hook_type:
197
+ case HookEventTypes.PreEventCreate:
198
+ return await self.pre_event_create(event_like, **kw), meta
199
+ case HookEventTypes.PreInvokation:
200
+ meta["event_id"] = str(event_like.id)
201
+ meta["event_created_at"] = event_like.created_at
202
+ return await self.pre_invokation(event_like, **kw), meta
203
+ case HookEventTypes.PostInvokation:
204
+ meta["event_id"] = str(event_like.id)
205
+ meta["event_created_at"] = event_like.created_at
206
+ return await self.post_invokation(**kw), meta
207
+ return await self.handle_streaming_chunk(chunk_type, chunk, exit, **kw)
208
+
209
+ def _can_handle(
210
+ self,
211
+ /,
212
+ *,
213
+ ht_: HookEventTypes = None,
214
+ ct_=None,
215
+ ) -> bool:
216
+ """Check if the registry can handle the given event or chunk type."""
217
+ if ht_:
218
+ return ht_ in self._hooks
219
+ if ct_:
220
+ return ct_ in self._stream_handlers
221
+ return False
lionagi/service/imodel.py CHANGED
@@ -7,12 +7,15 @@ from collections.abc import AsyncGenerator, Callable
7
7
 
8
8
  from pydantic import BaseModel
9
9
 
10
- from lionagi.protocols.generic.event import EventStatus
11
- from lionagi.utils import is_coro_func
10
+ from lionagi.protocols.generic.log import Log
11
+ from lionagi.protocols.types import ID, Event, EventStatus, IDType
12
+ from lionagi.service.hooks.hook_event import HookEventTypes
13
+ from lionagi.utils import is_coro_func, time
12
14
 
13
15
  from .connections.api_calling import APICalling
14
16
  from .connections.endpoint import Endpoint
15
17
  from .connections.match_endpoint import match_endpoint
18
+ from .hooks import HookEvent, HookRegistry, global_hook_logger
16
19
  from .rate_limited_processor import RateLimitedAPIExecutor
17
20
 
18
21
 
@@ -47,6 +50,10 @@ class iModel:
47
50
  concurrency_limit: int | None = None,
48
51
  streaming_process_func: Callable = None,
49
52
  provider_metadata: dict | None = None,
53
+ hook_registry: HookRegistry | dict | None = None,
54
+ exit_hook: bool = False,
55
+ id: IDType | str = None,
56
+ created_at: float | None = None,
50
57
  **kwargs,
51
58
  ) -> None:
52
59
  """Initializes the iModel instance.
@@ -86,6 +93,22 @@ class iModel:
86
93
  Additional keyword arguments, such as `model`, or any other
87
94
  provider-specific fields.
88
95
  """
96
+
97
+ # 1. put in ID and timestamp -----------------------------------------
98
+ self.id = None
99
+ self.created_at = None
100
+ if id is not None:
101
+ self.id = ID.get_id(id)
102
+ else:
103
+ self.id = IDType.create()
104
+ if created_at is not None:
105
+ if not isinstance(created_at, float):
106
+ raise ValueError("created_at must be a float timestamp.")
107
+ self.created_at = created_at
108
+ else:
109
+ self.created_at = time()
110
+
111
+ # 2. Configure Endpoint ---------------------------------------------
89
112
  model = kwargs.get("model", None)
90
113
  if model:
91
114
  if not provider:
@@ -96,7 +119,6 @@ class iModel:
96
119
  else:
97
120
  raise ValueError("Provider must be provided")
98
121
 
99
- # Pass api_key to endpoint if provided
100
122
  if api_key is not None:
101
123
  kwargs["api_key"] = api_key
102
124
  if isinstance(endpoint, Endpoint):
@@ -112,6 +134,7 @@ class iModel:
112
134
  if base_url:
113
135
  self.endpoint.config.base_url = base_url
114
136
 
137
+ # 3. Configure executor ---------------------------------------------
115
138
  self.executor = RateLimitedAPIExecutor(
116
139
  queue_capacity=queue_capacity,
117
140
  capacity_refresh_time=capacity_refresh_time,
@@ -120,11 +143,91 @@ class iModel:
120
143
  limit_tokens=limit_tokens,
121
144
  concurrency_limit=concurrency_limit,
122
145
  )
123
- # Use provided streaming_process_func or default to None
124
- self.streaming_process_func = streaming_process_func
125
146
 
126
- # Provider-specific metadata storage (e.g., session_id for Claude Code)
147
+ # 4. other configurations --------------------------------------------
148
+ self.streaming_process_func = streaming_process_func
127
149
  self.provider_metadata = provider_metadata or {}
150
+ self.hook_registry = hook_registry or HookRegistry()
151
+ if isinstance(self.hook_registry, dict):
152
+ self.hook_registry = HookRegistry(**self.hook_registry)
153
+ self.exit_hook: bool = exit_hook
154
+
155
+ async def create_event(
156
+ self,
157
+ create_event_type: type[Event] = APICalling,
158
+ create_event_exit_hook: bool = None,
159
+ create_event_hook_timeout: float = 10.0,
160
+ create_event_hook_params: dict = None,
161
+ pre_invoke_event_exit_hook: bool = None,
162
+ pre_invoke_event_hook_timeout: float = 30.0,
163
+ pre_invoke_event_hook_params: dict = None,
164
+ post_invoke_event_exit_hook: bool = None,
165
+ post_invoke_event_hook_timeout: float = 30.0,
166
+ post_invoke_event_hook_params: dict = None,
167
+ **kwargs,
168
+ ) -> tuple[HookEvent | None, APICalling]:
169
+ h_ev = None
170
+ if self.hook_registry._can_handle(ht_=HookEventTypes.PreEventCreate):
171
+ h_ev = HookEvent(
172
+ hook_type=HookEventTypes.PreEventCreate,
173
+ registry=self.hook_registry,
174
+ event_like=create_event_type,
175
+ params=create_event_hook_params or {},
176
+ exit=(
177
+ self.exit_hook
178
+ if create_event_exit_hook is None
179
+ else create_event_exit_hook
180
+ ),
181
+ timeout=create_event_hook_timeout,
182
+ )
183
+ await h_ev.invoke()
184
+ if h_ev._should_exit:
185
+ raise h_ev._exit_cause or RuntimeError(
186
+ "PreEventCreate hook requested exit without a cause"
187
+ )
188
+
189
+ if create_event_type is APICalling:
190
+ api_call = self.create_api_calling(**kwargs)
191
+ if h_ev:
192
+ h_ev.assosiated_event_info["event_id"] = str(api_call.id)
193
+ h_ev.assosiated_event_info["event_created_at"] = (
194
+ api_call.created_at
195
+ )
196
+ await global_hook_logger.alog(Log(content=h_ev.to_dict()))
197
+
198
+ if self.hook_registry._can_handle(
199
+ ht_=HookEventTypes.PreInvokation
200
+ ):
201
+ api_call.create_pre_invoke_hook(
202
+ hook_registry=self.hook_registry,
203
+ exit_hook=(
204
+ self.exit_hook
205
+ if pre_invoke_event_exit_hook is None
206
+ else pre_invoke_event_exit_hook
207
+ ),
208
+ hook_timeout=pre_invoke_event_hook_timeout,
209
+ hook_params=pre_invoke_event_hook_params or {},
210
+ )
211
+
212
+ if self.hook_registry._can_handle(
213
+ ht_=HookEventTypes.PostInvokation
214
+ ):
215
+ api_call.create_post_invoke_hook(
216
+ hook_registry=self.hook_registry,
217
+ exit_hook=(
218
+ self.exit_hook
219
+ if post_invoke_event_exit_hook is None
220
+ else post_invoke_event_exit_hook
221
+ ),
222
+ hook_timeout=post_invoke_event_hook_timeout,
223
+ hook_params=post_invoke_event_hook_params or {},
224
+ )
225
+
226
+ return api_call
227
+
228
+ raise ValueError(
229
+ f"Unsupported event type: {create_event_type}. Only APICalling is supported."
230
+ )
128
231
 
129
232
  def create_api_calling(
130
233
  self, include_token_usage_to_model: bool = False, **kwargs
@@ -178,12 +281,7 @@ class iModel:
178
281
  return await self.streaming_process_func(chunk)
179
282
  return self.streaming_process_func(chunk)
180
283
 
181
- async def stream(
182
- self,
183
- api_call=None,
184
- include_token_usage_to_model: bool = False,
185
- **kwargs,
186
- ) -> AsyncGenerator:
284
+ async def stream(self, api_call=None, **kw) -> AsyncGenerator:
187
285
  """Performs a streaming API call with the given arguments.
188
286
 
189
287
  Args:
@@ -196,11 +294,8 @@ class iModel:
196
294
  goes wrong.
197
295
  """
198
296
  if api_call is None:
199
- kwargs["stream"] = True
200
- api_call = self.create_api_calling(
201
- include_token_usage_to_model=include_token_usage_to_model,
202
- **kwargs,
203
- )
297
+ kw["stream"] = True
298
+ api_call = await self.create_event(**kw)
204
299
  await self.executor.append(api_call)
205
300
 
206
301
  if (
@@ -231,9 +326,7 @@ class iModel:
231
326
  finally:
232
327
  yield self.executor.pile.pop(api_call.id)
233
328
 
234
- async def invoke(
235
- self, api_call: APICalling = None, **kwargs
236
- ) -> APICalling | None:
329
+ async def invoke(self, api_call: APICalling = None, **kw) -> APICalling:
237
330
  """Invokes a rate-limited API call with the given arguments.
238
331
 
239
332
  Args:
@@ -251,8 +344,9 @@ class iModel:
251
344
  """
252
345
  try:
253
346
  if api_call is None:
254
- kwargs.pop("stream", None)
255
- api_call = self.create_api_calling(**kwargs)
347
+ kw.pop("stream", None)
348
+ api_call = await self.create_event(**kw)
349
+
256
350
  if (
257
351
  self.executor.processor is None
258
352
  or self.executor.processor.is_stopped()
@@ -291,18 +385,6 @@ class iModel:
291
385
  except Exception as e:
292
386
  raise ValueError(f"Failed to invoke API call: {e}")
293
387
 
294
- @property
295
- def allowed_roles(self) -> set[str]:
296
- """list[str]: Roles that are permissible for this endpoint.
297
-
298
- Returns:
299
- If the endpoint has an `allowed_roles` attribute, returns that;
300
- otherwise, defaults to `{"system", "user", "assistant"}`.
301
- """
302
- if hasattr(self.endpoint, "allowed_roles"):
303
- return self.endpoint.allowed_roles
304
- return {"system", "user", "assistant"}
305
-
306
388
  @property
307
389
  def model_name(self) -> str:
308
390
  """str: The name of the model used by the endpoint.
@@ -323,6 +405,8 @@ class iModel:
323
405
 
324
406
  def to_dict(self):
325
407
  return {
408
+ "id": str(self.id) if self.id else None,
409
+ "created_at": self.created_at,
326
410
  "endpoint": self.endpoint.to_dict(),
327
411
  "processor_config": self.executor.config,
328
412
  "provider_metadata": self.provider_metadata,
@@ -343,5 +427,7 @@ class iModel:
343
427
  return cls(
344
428
  endpoint=e1,
345
429
  provider_metadata=data.get("provider_metadata"),
430
+ id=data.get("id"),
431
+ created_at=data.get("created_at"),
346
432
  **data.get("processor_config", {}),
347
433
  )