lionagi 0.18.0__py3-none-any.whl → 0.18.2__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 (93) hide show
  1. lionagi/__init__.py +102 -59
  2. lionagi/_errors.py +0 -5
  3. lionagi/adapters/spec_adapters/__init__.py +9 -0
  4. lionagi/adapters/spec_adapters/_protocol.py +236 -0
  5. lionagi/adapters/spec_adapters/pydantic_field.py +158 -0
  6. lionagi/fields.py +83 -0
  7. lionagi/ln/__init__.py +3 -1
  8. lionagi/ln/_async_call.py +2 -2
  9. lionagi/ln/concurrency/primitives.py +4 -4
  10. lionagi/ln/concurrency/task.py +1 -0
  11. lionagi/ln/fuzzy/_fuzzy_match.py +2 -2
  12. lionagi/ln/types/__init__.py +51 -0
  13. lionagi/ln/types/_sentinel.py +154 -0
  14. lionagi/ln/{types.py → types/base.py} +108 -168
  15. lionagi/ln/types/operable.py +221 -0
  16. lionagi/ln/types/spec.py +441 -0
  17. lionagi/models/field_model.py +69 -7
  18. lionagi/models/hashable_model.py +2 -3
  19. lionagi/models/model_params.py +4 -3
  20. lionagi/operations/ReAct/ReAct.py +1 -1
  21. lionagi/operations/act/act.py +3 -3
  22. lionagi/operations/builder.py +5 -7
  23. lionagi/operations/fields.py +380 -0
  24. lionagi/operations/flow.py +4 -6
  25. lionagi/operations/node.py +4 -4
  26. lionagi/operations/operate/operate.py +123 -89
  27. lionagi/operations/operate/operative.py +198 -0
  28. lionagi/operations/operate/step.py +203 -0
  29. lionagi/operations/select/select.py +1 -1
  30. lionagi/operations/select/utils.py +7 -1
  31. lionagi/operations/types.py +7 -7
  32. lionagi/protocols/action/manager.py +5 -6
  33. lionagi/protocols/contracts.py +2 -2
  34. lionagi/protocols/generic/__init__.py +22 -0
  35. lionagi/protocols/generic/element.py +36 -127
  36. lionagi/protocols/generic/pile.py +9 -10
  37. lionagi/protocols/generic/progression.py +23 -22
  38. lionagi/protocols/graph/edge.py +6 -5
  39. lionagi/protocols/ids.py +6 -49
  40. lionagi/protocols/messages/__init__.py +3 -1
  41. lionagi/protocols/messages/base.py +7 -6
  42. lionagi/protocols/messages/instruction.py +0 -1
  43. lionagi/protocols/messages/message.py +2 -2
  44. lionagi/protocols/types.py +1 -11
  45. lionagi/service/connections/__init__.py +3 -0
  46. lionagi/service/connections/providers/claude_code_cli.py +3 -2
  47. lionagi/service/hooks/_types.py +1 -1
  48. lionagi/service/hooks/_utils.py +1 -1
  49. lionagi/service/hooks/hook_event.py +3 -8
  50. lionagi/service/hooks/hook_registry.py +5 -5
  51. lionagi/service/hooks/hooked_event.py +61 -1
  52. lionagi/service/imodel.py +24 -20
  53. lionagi/service/third_party/claude_code.py +1 -2
  54. lionagi/service/third_party/openai_models.py +24 -22
  55. lionagi/service/token_calculator.py +1 -94
  56. lionagi/session/branch.py +26 -228
  57. lionagi/session/session.py +5 -90
  58. lionagi/version.py +1 -1
  59. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/METADATA +6 -5
  60. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/RECORD +62 -82
  61. lionagi/fields/__init__.py +0 -47
  62. lionagi/fields/action.py +0 -188
  63. lionagi/fields/base.py +0 -153
  64. lionagi/fields/code.py +0 -239
  65. lionagi/fields/file.py +0 -234
  66. lionagi/fields/instruct.py +0 -135
  67. lionagi/fields/reason.py +0 -55
  68. lionagi/fields/research.py +0 -52
  69. lionagi/operations/brainstorm/__init__.py +0 -2
  70. lionagi/operations/brainstorm/brainstorm.py +0 -498
  71. lionagi/operations/brainstorm/prompt.py +0 -11
  72. lionagi/operations/instruct/__init__.py +0 -2
  73. lionagi/operations/instruct/instruct.py +0 -28
  74. lionagi/operations/plan/__init__.py +0 -6
  75. lionagi/operations/plan/plan.py +0 -386
  76. lionagi/operations/plan/prompt.py +0 -25
  77. lionagi/operations/utils.py +0 -45
  78. lionagi/protocols/forms/__init__.py +0 -2
  79. lionagi/protocols/forms/base.py +0 -85
  80. lionagi/protocols/forms/flow.py +0 -79
  81. lionagi/protocols/forms/form.py +0 -86
  82. lionagi/protocols/forms/report.py +0 -48
  83. lionagi/protocols/mail/__init__.py +0 -2
  84. lionagi/protocols/mail/exchange.py +0 -220
  85. lionagi/protocols/mail/mail.py +0 -51
  86. lionagi/protocols/mail/mailbox.py +0 -103
  87. lionagi/protocols/mail/manager.py +0 -218
  88. lionagi/protocols/mail/package.py +0 -101
  89. lionagi/protocols/operatives/__init__.py +0 -2
  90. lionagi/protocols/operatives/operative.py +0 -362
  91. lionagi/protocols/operatives/step.py +0 -227
  92. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/WHEEL +0 -0
  93. {lionagi-0.18.0.dist-info → lionagi-0.18.2.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ import anyio
5
5
  from pydantic import PrivateAttr
6
6
 
7
7
  from lionagi.ln import get_cancelled_exc_class
8
- from lionagi.protocols.types import DataLogger, Event, EventStatus, Log
8
+ from lionagi.protocols.types import DataLogger, Event, EventStatus
9
9
  from lionagi.service.hooks import HookEvent, HookEventTypes
10
10
 
11
11
  global_hook_logger = DataLogger(
@@ -43,6 +43,16 @@ class HookedEvent(Event):
43
43
  self.execution.status = EventStatus.PROCESSING
44
44
  if h_ev := self._pre_invoke_hook_event:
45
45
  await h_ev.invoke()
46
+
47
+ # Check if hook failed or was cancelled - propagate to main event
48
+ if h_ev.execution.status in (
49
+ EventStatus.FAILED,
50
+ EventStatus.CANCELLED,
51
+ ):
52
+ self.execution.status = h_ev.execution.status
53
+ self.execution.error = f"Pre-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
54
+ return
55
+
46
56
  if h_ev._should_exit:
47
57
  raise h_ev._exit_cause or RuntimeError(
48
58
  "Pre-invocation hook requested exit without a cause"
@@ -53,6 +63,19 @@ class HookedEvent(Event):
53
63
 
54
64
  if h_ev := self._post_invoke_hook_event:
55
65
  await h_ev.invoke()
66
+
67
+ # Check if hook failed or was cancelled - propagate to main event
68
+ if h_ev.execution.status in (
69
+ EventStatus.FAILED,
70
+ EventStatus.CANCELLED,
71
+ ):
72
+ self.execution.status = h_ev.execution.status
73
+ self.execution.error = f"Post-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
74
+ self.execution.response = (
75
+ response # Keep response even if hook failed
76
+ )
77
+ return
78
+
56
79
  if h_ev._should_exit:
57
80
  raise h_ev._exit_cause or RuntimeError(
58
81
  "Post-invocation hook requested exit without a cause"
@@ -87,10 +110,47 @@ class HookedEvent(Event):
87
110
  try:
88
111
  self.execution.status = EventStatus.PROCESSING
89
112
 
113
+ # Execute pre-invoke hook if present
114
+ if h_ev := self._pre_invoke_hook_event:
115
+ await h_ev.invoke()
116
+
117
+ # Check if hook failed or was cancelled - propagate to main event
118
+ if h_ev.execution.status in (
119
+ EventStatus.FAILED,
120
+ EventStatus.CANCELLED,
121
+ ):
122
+ self.execution.status = h_ev.execution.status
123
+ self.execution.error = f"Pre-invoke hook {h_ev.execution.status.value}: {h_ev.execution.error}"
124
+ return
125
+
126
+ if h_ev._should_exit:
127
+ raise h_ev._exit_cause or RuntimeError(
128
+ "Pre-invocation hook requested exit without a cause"
129
+ )
130
+ await global_hook_logger.alog(h_ev)
131
+
90
132
  async for chunk in self._stream():
91
133
  response.append(chunk)
92
134
  yield chunk
93
135
 
136
+ # Execute post-invoke hook if present
137
+ if h_ev := self._post_invoke_hook_event:
138
+ await h_ev.invoke()
139
+
140
+ # Check if hook failed or was cancelled - don't fail the stream since data was already sent
141
+ if h_ev.execution.status in (
142
+ EventStatus.FAILED,
143
+ EventStatus.CANCELLED,
144
+ ):
145
+ # Log but don't fail the stream
146
+ await global_hook_logger.alog(h_ev)
147
+ elif h_ev._should_exit:
148
+ raise h_ev._exit_cause or RuntimeError(
149
+ "Post-invocation hook requested exit without a cause"
150
+ )
151
+ else:
152
+ await global_hook_logger.alog(h_ev)
153
+
94
154
  self.execution.response = response
95
155
  self.execution.status = EventStatus.COMPLETED
96
156
 
lionagi/service/imodel.py CHANGED
@@ -3,19 +3,22 @@
3
3
 
4
4
  import asyncio
5
5
  from collections.abc import AsyncGenerator, Callable
6
+ from typing import Any
7
+ from uuid import UUID, uuid4
6
8
 
7
9
  from pydantic import BaseModel
8
10
 
9
11
  from lionagi.ln import is_coro_func, now_utc
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.service.hooks.hooked_event import HookedEvent
14
-
15
- from .connections.api_calling import APICalling
16
- from .connections.endpoint import Endpoint
17
- from .connections.match_endpoint import match_endpoint
18
- from .hooks import HookEvent, HookRegistry, global_hook_logger
12
+ from lionagi.protocols.generic import ID, Event, EventStatus, Log
13
+
14
+ from .connections import APICalling, Endpoint, match_endpoint
15
+ from .hooks import (
16
+ HookedEvent,
17
+ HookEvent,
18
+ HookEventTypes,
19
+ HookRegistry,
20
+ global_hook_logger,
21
+ )
19
22
  from .rate_limited_processor import RateLimitedAPIExecutor
20
23
 
21
24
 
@@ -52,7 +55,7 @@ class iModel:
52
55
  provider_metadata: dict | None = None,
53
56
  hook_registry: HookRegistry | dict | None = None,
54
57
  exit_hook: bool = False,
55
- id: IDType | str = None,
58
+ id: UUID | str = None,
56
59
  created_at: float | None = None,
57
60
  **kwargs,
58
61
  ) -> None:
@@ -100,7 +103,7 @@ class iModel:
100
103
  if id is not None:
101
104
  self.id = ID.get_id(id)
102
105
  else:
103
- self.id = IDType.create()
106
+ self.id = uuid4()
104
107
  if created_at is not None:
105
108
  if not isinstance(created_at, float):
106
109
  raise ValueError("created_at must be a float timestamp.")
@@ -270,7 +273,7 @@ class iModel:
270
273
  include_token_usage_to_model=include_token_usage_to_model,
271
274
  )
272
275
 
273
- async def process_chunk(self, chunk) -> None:
276
+ async def process_chunk(self, chunk) -> Any:
274
277
  """Processes a chunk of streaming data.
275
278
 
276
279
  Override this method in subclasses if you need custom handling
@@ -284,6 +287,7 @@ class iModel:
284
287
  if is_coro_func(self.streaming_process_func):
285
288
  return await self.streaming_process_func(chunk)
286
289
  return self.streaming_process_func(chunk)
290
+ return None
287
291
 
288
292
  async def stream(self, api_call=None, **kw) -> AsyncGenerator:
289
293
  """Performs a streaming API call with the given arguments.
@@ -313,8 +317,8 @@ class iModel:
313
317
  try:
314
318
  async for i in api_call.stream():
315
319
  result = await self.process_chunk(i)
316
- if result:
317
- yield result
320
+ # Yield processed result if available, otherwise yield raw chunk
321
+ yield result if result is not None else i
318
322
  except Exception as e:
319
323
  raise ValueError(f"Failed to stream API call: {e}")
320
324
  finally:
@@ -323,8 +327,8 @@ class iModel:
323
327
  try:
324
328
  async for i in api_call.stream():
325
329
  result = await self.process_chunk(i)
326
- if result:
327
- yield result
330
+ # Yield processed result if available, otherwise yield raw chunk
331
+ yield result if result is not None else i
328
332
  except Exception as e:
329
333
  raise ValueError(f"Failed to stream API call: {e}")
330
334
  finally:
@@ -360,10 +364,10 @@ class iModel:
360
364
  await self.executor.append(api_call)
361
365
  await self.executor.forward()
362
366
  ctr = 0
363
- while api_call.status not in (
364
- EventStatus.COMPLETED,
365
- EventStatus.FAILED,
366
- ):
367
+ while api_call.status in [
368
+ EventStatus.PROCESSING,
369
+ EventStatus.PENDING,
370
+ ]:
367
371
  if ctr > 100:
368
372
  break
369
373
  await self.executor.forward()
@@ -22,7 +22,6 @@ from pydantic import BaseModel, Field, field_validator, model_validator
22
22
 
23
23
  from lionagi import ln
24
24
  from lionagi.libs.schema.as_readable import as_readable
25
- from lionagi.utils import is_coro_func
26
25
 
27
26
  HAS_CLAUDE_CODE_CLI = False
28
27
  CLAUDE_CLI = None
@@ -556,7 +555,7 @@ def _pp_final(sess: ClaudeSession, theme) -> None:
556
555
  async def _maybe_await(func, *args, **kw):
557
556
  """Call func which may be sync or async."""
558
557
  res = func(*args, **kw) if func else None
559
- if is_coro_func(res):
558
+ if ln.is_coro_func(res):
560
559
  await res
561
560
 
562
561
 
@@ -145,7 +145,7 @@ class ImageURLObject(BaseModel):
145
145
  """Image URL object; 'detail' is optional and model-dependent."""
146
146
 
147
147
  url: str
148
- detail: Optional[Literal["auto", "low", "high"]] = Field(
148
+ detail: Literal["auto", "low", "high"] | None = Field(
149
149
  default=None,
150
150
  description="Optional detail control for vision models (auto/low/high).",
151
151
  )
@@ -168,8 +168,8 @@ class FunctionDef(BaseModel):
168
168
  """JSON Schema function definition for tool-calling."""
169
169
 
170
170
  name: str
171
- description: Optional[str] = None
172
- parameters: Dict[str, Any] = Field(
171
+ description: str | None = None
172
+ parameters: dict[str, Any] = Field(
173
173
  default_factory=dict,
174
174
  description="JSON Schema describing function parameters.",
175
175
  )
@@ -204,7 +204,7 @@ class ToolChoiceFunction(BaseModel):
204
204
  """Explicit tool selection."""
205
205
 
206
206
  type: Literal["function"] = "function"
207
- function: Dict[str, str] # {"name": "<function_name>"}
207
+ function: dict[str, str] # {"name": "<function_name>"}
208
208
 
209
209
 
210
210
  ToolChoice = Union[Literal["auto", "none"], ToolChoiceFunction]
@@ -223,12 +223,16 @@ class ResponseFormatJSONObject(BaseModel):
223
223
 
224
224
  class JSONSchemaFormat(BaseModel):
225
225
  name: str
226
- schema: Dict[str, Any]
227
- strict: Optional[bool] = Field(
226
+ schema_: dict[str, Any] = Field(
227
+ alias="schema", description="JSON Schema definition"
228
+ )
229
+ strict: bool | None = Field(
228
230
  default=None,
229
231
  description="If true, disallow unspecified properties (strict schema).",
230
232
  )
231
233
 
234
+ model_config = {"populate_by_name": True}
235
+
232
236
 
233
237
  class ResponseFormatJSONSchema(BaseModel):
234
238
  type: Literal["json_schema"] = "json_schema"
@@ -247,31 +251,29 @@ ResponseFormat = Union[
247
251
 
248
252
  class SystemMessage(BaseModel):
249
253
  role: Literal[ChatRole.system] = ChatRole.system
250
- content: Union[str, List[ContentPart]]
251
- name: Optional[str] = None # optional per API
254
+ content: str | list[ContentPart]
255
+ name: str | None = None # optional per API
252
256
 
253
257
 
254
258
  class DeveloperMessage(BaseModel):
255
259
  role: Literal[ChatRole.developer] = ChatRole.developer
256
- content: Union[str, List[ContentPart]]
257
- name: Optional[str] = None
260
+ content: str | list[ContentPart]
261
+ name: str | None = None
258
262
 
259
263
 
260
264
  class UserMessage(BaseModel):
261
265
  role: Literal[ChatRole.user] = ChatRole.user
262
- content: Union[str, List[ContentPart]]
263
- name: Optional[str] = None
266
+ content: str | list[ContentPart]
267
+ name: str | None = None
264
268
 
265
269
 
266
270
  class AssistantMessage(BaseModel):
267
271
  role: Literal[ChatRole.assistant] = ChatRole.assistant
268
272
  # Either textual content, or only tool_calls (when asking you to call tools)
269
- content: Optional[Union[str, List[ContentPart]]] = None
270
- name: Optional[str] = None
271
- tool_calls: Optional[List[ToolCall]] = None # modern tool-calling result
272
- function_call: Optional[FunctionCall] = (
273
- None # legacy function-calling result
274
- )
273
+ content: str | list[ContentPart] | None = None
274
+ name: str | None = None
275
+ tool_calls: list[ToolCall] | None = None # modern tool-calling result
276
+ function_call: FunctionCall | None = None # legacy function-calling result
275
277
 
276
278
 
277
279
  class ToolMessage(BaseModel):
@@ -292,7 +294,7 @@ ChatMessage = (
292
294
 
293
295
 
294
296
  class StreamOptions(BaseModel):
295
- include_usage: Optional[bool] = Field(
297
+ include_usage: bool | None = Field(
296
298
  default=None,
297
299
  description="If true, a final streamed chunk includes token usage.",
298
300
  )
@@ -309,7 +311,7 @@ class OpenAIChatCompletionsRequest(BaseModel):
309
311
 
310
312
  # Required
311
313
  model: str = Field(..., description="Model name, e.g., 'gpt-4o', 'gpt-4o-mini'.") # type: ignore
312
- messages: List[ChatMessage] = Field(
314
+ messages: list[ChatMessage] = Field(
313
315
  ...,
314
316
  description="Conversation so far, including system/developer context.",
315
317
  )
@@ -348,7 +350,7 @@ class OpenAIChatCompletionsRequest(BaseModel):
348
350
  n: int | None = Field(
349
351
  default=None, ge=1, description="# of choices to generate."
350
352
  )
351
- stop: str | List[str] | None = Field(
353
+ stop: str | list[str] | None = Field(
352
354
  default=None, description="Stop sequence(s)."
353
355
  )
354
356
  logit_bias: dict[str, float] | None = Field(
@@ -406,7 +408,7 @@ class OpenAIChatCompletionsRequest(BaseModel):
406
408
  description="Whether to store the response server-side (model-dependent).",
407
409
  )
408
410
  metadata: dict[str, Any] | None = None
409
- reasoning_effort: Optional[Literal["low", "medium", "high"]] = Field(
411
+ reasoning_effort: Literal["low", "medium", "high"] | None = Field(
410
412
  default=None,
411
413
  description="For reasoning models: trade-off between speed and accuracy.",
412
414
  )
@@ -1,83 +1,10 @@
1
1
  # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import base64
5
4
  from collections.abc import Callable
6
- from io import BytesIO
7
5
 
8
6
  import tiktoken
9
7
 
10
- GPT4O_IMAGE_PRICING = {
11
- "base_cost": 85,
12
- "low_detail": 0,
13
- "max_dimension": 2048,
14
- "min_side": 768,
15
- "tile_size": 512,
16
- "tile_cost": 170,
17
- }
18
-
19
- GPT4O_MINI_IMAGE_PRICING = {
20
- "base_cost": 2833,
21
- "low_detail": 0,
22
- "max_dimension": 2048,
23
- "min_side": 768,
24
- "tile_size": 512,
25
- "tile_cost": 5667,
26
- }
27
-
28
- O1_IMAGE_PRICING = {
29
- "base_cost": 75,
30
- "low_detail": 0,
31
- "max_dimension": 2048,
32
- "min_side": 768,
33
- "tile_size": 512,
34
- "tile_cost": 150,
35
- }
36
-
37
-
38
- def calculate_image_token_usage_from_base64(
39
- image_base64: str, detail, image_pricing
40
- ):
41
- from PIL import Image
42
-
43
- # Decode the base64 string to get image data
44
- if "data:image/jpeg;base64," in image_base64:
45
- image_base64 = image_base64.split("data:image/jpeg;base64,")[1]
46
- image_base64.strip("{}")
47
-
48
- image_data = base64.b64decode(image_base64)
49
- image = Image.open(BytesIO(image_data))
50
-
51
- # Get image dimensions
52
- width, height = image.size
53
-
54
- if detail == "low":
55
- return image_pricing["base_cost"] + image_pricing["low_detail"]
56
-
57
- # Scale to fit within a 2048 x 2048 tile
58
- max_dimension = image_pricing["max_dimension"]
59
- if width > max_dimension or height > max_dimension:
60
- scale_factor = max_dimension / max(width, height)
61
- width = int(width * scale_factor)
62
- height = int(height * scale_factor)
63
-
64
- # Scale such that the shortest side is 768px
65
- min_side = image_pricing["min_side"]
66
- if min(width, height) > min_side:
67
- scale_factor = min_side / min(width, height)
68
- width = int(width * scale_factor)
69
- height = int(height * scale_factor)
70
-
71
- # Calculate the number of 512px tiles
72
- num_tiles = (width // image_pricing["tile_size"]) * (
73
- height // image_pricing["tile_size"]
74
- )
75
- token_cost = (
76
- image_pricing["base_cost"] + image_pricing["tile_cost"] * num_tiles
77
- )
78
-
79
- return token_cost
80
-
81
8
 
82
9
  def get_encoding_name(value: str) -> str:
83
10
  try:
@@ -91,17 +18,6 @@ def get_encoding_name(value: str) -> str:
91
18
  return "o200k_base"
92
19
 
93
20
 
94
- def get_image_pricing(model: str) -> dict:
95
- if "gpt-4o-mini" in model:
96
- return GPT4O_MINI_IMAGE_PRICING
97
- elif "gpt-4o" in model:
98
- return GPT4O_IMAGE_PRICING
99
- elif "o1" in model and "mini" not in model:
100
- return O1_IMAGE_PRICING
101
- else:
102
- raise ValueError("Invalid model name")
103
-
104
-
105
21
  class TokenCalculator:
106
22
  @staticmethod
107
23
  def calculate_message_tokens(messages: list[dict], /, **kwargs) -> int:
@@ -177,16 +93,7 @@ class TokenCalculator:
177
93
  if "text" in i_:
178
94
  return TokenCalculator._calculate_chatitem(str(i_["text"]))
179
95
  elif "image_url" in i_:
180
- a: str = i_["image_url"].get("url", "")
181
- if "data:image/jpeg;base64," in a:
182
- a = a.split("data:image/jpeg;base64,")[1].strip()
183
- pricing = get_image_pricing(model_name)
184
- return (
185
- calculate_image_token_usage_from_base64(
186
- a, i_.get("detail", "low"), pricing
187
- )
188
- + 15 # buffer for image
189
- )
96
+ return 500 # fixed cost for image URL
190
97
 
191
98
  if isinstance(i_, list):
192
99
  return sum(