lionagi 0.17.11__py3-none-any.whl → 0.18.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 (52) hide show
  1. lionagi/libs/schema/minimal_yaml.py +98 -0
  2. lionagi/ln/types.py +32 -5
  3. lionagi/models/field_model.py +9 -0
  4. lionagi/operations/ReAct/ReAct.py +474 -237
  5. lionagi/operations/ReAct/utils.py +3 -0
  6. lionagi/operations/act/act.py +206 -0
  7. lionagi/operations/chat/chat.py +130 -114
  8. lionagi/operations/communicate/communicate.py +101 -42
  9. lionagi/operations/flow.py +4 -4
  10. lionagi/operations/interpret/interpret.py +65 -20
  11. lionagi/operations/operate/operate.py +212 -106
  12. lionagi/operations/parse/parse.py +170 -142
  13. lionagi/operations/select/select.py +78 -17
  14. lionagi/operations/select/utils.py +1 -1
  15. lionagi/operations/types.py +119 -23
  16. lionagi/protocols/generic/log.py +3 -2
  17. lionagi/protocols/messages/__init__.py +27 -0
  18. lionagi/protocols/messages/action_request.py +86 -184
  19. lionagi/protocols/messages/action_response.py +73 -131
  20. lionagi/protocols/messages/assistant_response.py +130 -159
  21. lionagi/protocols/messages/base.py +26 -18
  22. lionagi/protocols/messages/instruction.py +281 -625
  23. lionagi/protocols/messages/manager.py +112 -62
  24. lionagi/protocols/messages/message.py +87 -197
  25. lionagi/protocols/messages/system.py +52 -123
  26. lionagi/protocols/types.py +0 -2
  27. lionagi/service/connections/endpoint.py +0 -8
  28. lionagi/service/connections/providers/oai_.py +29 -94
  29. lionagi/service/connections/providers/ollama_.py +3 -2
  30. lionagi/service/hooks/hooked_event.py +2 -2
  31. lionagi/service/third_party/claude_code.py +3 -2
  32. lionagi/service/third_party/openai_models.py +433 -0
  33. lionagi/session/branch.py +170 -178
  34. lionagi/session/session.py +3 -9
  35. lionagi/tools/file/reader.py +2 -2
  36. lionagi/version.py +1 -1
  37. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/METADATA +1 -2
  38. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/RECORD +41 -49
  39. lionagi/operations/_act/act.py +0 -86
  40. lionagi/protocols/messages/templates/README.md +0 -28
  41. lionagi/protocols/messages/templates/action_request.jinja2 +0 -5
  42. lionagi/protocols/messages/templates/action_response.jinja2 +0 -9
  43. lionagi/protocols/messages/templates/assistant_response.jinja2 +0 -6
  44. lionagi/protocols/messages/templates/instruction_message.jinja2 +0 -61
  45. lionagi/protocols/messages/templates/system_message.jinja2 +0 -11
  46. lionagi/protocols/messages/templates/tool_schemas.jinja2 +0 -7
  47. lionagi/service/connections/providers/types.py +0 -28
  48. lionagi/service/third_party/openai_model_names.py +0 -198
  49. lionagi/service/types.py +0 -58
  50. /lionagi/operations/{_act → act}/__init__.py +0 -0
  51. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/WHEEL +0 -0
  52. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,6 @@
3
3
 
4
4
  from typing import Any, Literal
5
5
 
6
- from jinja2 import Template
7
6
  from pydantic import BaseModel, JsonValue
8
7
 
9
8
  from .._concepts import Manager
@@ -88,6 +87,7 @@ class MessageManager(Manager):
88
87
  *,
89
88
  instruction: JsonValue = None,
90
89
  context: JsonValue = None,
90
+ handle_context: Literal["extend", "replace"] = "extend",
91
91
  guidance: JsonValue = None,
92
92
  images: list = None,
93
93
  request_fields: JsonValue = None,
@@ -105,17 +105,49 @@ class MessageManager(Manager):
105
105
  If `instruction` is an existing Instruction, it is updated in place.
106
106
  Otherwise, a new instance is created.
107
107
  """
108
- params = {
108
+ raw_params = {
109
109
  k: v
110
110
  for k, v in locals().items()
111
111
  if k != "instruction" and v is not None
112
112
  }
113
113
 
114
+ handle_ctx = raw_params.get("handle_context", "extend")
115
+
114
116
  if isinstance(instruction, Instruction):
117
+ params = {
118
+ k: v for k, v in raw_params.items() if k != "handle_context"
119
+ }
120
+ ctx_value = params.pop("context", None)
121
+ if ctx_value is not None:
122
+ if isinstance(ctx_value, list):
123
+ ctx_list = list(ctx_value)
124
+ else:
125
+ ctx_list = [ctx_value]
126
+ if handle_ctx == "extend":
127
+ merged = list(instruction.content.prompt_context)
128
+ merged.extend(ctx_list)
129
+ params["context"] = merged
130
+ else:
131
+ params["context"] = list(ctx_list)
132
+ # Always replace in from_dict since we've already done the merge logic
133
+ params["handle_context"] = "replace"
115
134
  instruction.update(**params)
116
135
  return instruction
117
136
  else:
118
- return Instruction.create(instruction=instruction, **params)
137
+ # Build content dict for Instruction
138
+ content_dict = {
139
+ k: v
140
+ for k, v in raw_params.items()
141
+ if k not in ["sender", "recipient"]
142
+ }
143
+ content_dict["handle_context"] = handle_ctx
144
+ if instruction is not None:
145
+ content_dict["instruction"] = instruction
146
+ return Instruction(
147
+ content=content_dict,
148
+ sender=raw_params.get("sender"),
149
+ recipient=raw_params.get("recipient"),
150
+ )
119
151
 
120
152
  @staticmethod
121
153
  def create_assistant_response(
@@ -123,8 +155,6 @@ class MessageManager(Manager):
123
155
  sender: Any = None,
124
156
  recipient: Any = None,
125
157
  assistant_response: AssistantResponse | Any = None,
126
- template: Template | str = None,
127
- template_context: dict[str, Any] = None,
128
158
  ) -> AssistantResponse:
129
159
  """
130
160
  Build or update an `AssistantResponse`. If `assistant_response` is an
@@ -133,17 +163,23 @@ class MessageManager(Manager):
133
163
  params = {
134
164
  k: v
135
165
  for k, v in locals().items()
136
- if k not in ["assistant_response", "template_context"]
166
+ if k != "assistant_response" and v is not None
137
167
  }
138
- t_ctx = template_context or {}
139
- params.update(t_ctx)
140
168
 
141
169
  if isinstance(assistant_response, AssistantResponse):
142
170
  assistant_response.update(**params)
143
171
  return assistant_response
144
172
 
145
- return AssistantResponse.create(
146
- assistant_response=assistant_response, **params
173
+ # Create new AssistantResponse
174
+ content_dict = (
175
+ {"assistant_response": assistant_response}
176
+ if assistant_response
177
+ else {}
178
+ )
179
+ return AssistantResponse(
180
+ content=content_dict,
181
+ sender=params.get("sender"),
182
+ recipient=params.get("recipient"),
147
183
  )
148
184
 
149
185
  @staticmethod
@@ -154,8 +190,6 @@ class MessageManager(Manager):
154
190
  function: str = None,
155
191
  arguments: dict[str, Any] = None,
156
192
  action_request: ActionRequest | None = None,
157
- template: Template | str = None,
158
- template_context: dict[str, Any] = None,
159
193
  ) -> ActionRequest:
160
194
  """
161
195
  Build or update an ActionRequest.
@@ -166,25 +200,31 @@ class MessageManager(Manager):
166
200
  function: Function name for the request.
167
201
  arguments: Arguments for the function.
168
202
  action_request: Possibly existing ActionRequest to update.
169
- template: Optional jinja template.
170
- template_context: Extra context for the template.
171
203
 
172
204
  Returns:
173
205
  ActionRequest: The new or updated request object.
174
206
  """
175
207
  params = {
176
- "sender": sender,
177
- "recipient": recipient,
178
- "function": function,
179
- "arguments": arguments,
180
- "template": template,
208
+ k: v
209
+ for k, v in locals().items()
210
+ if k != "action_request" and v is not None
181
211
  }
182
- params.update(template_context or {})
183
212
 
184
213
  if isinstance(action_request, ActionRequest):
185
214
  action_request.update(**params)
186
215
  return action_request
187
- return ActionRequest.create(**params)
216
+
217
+ # Create new ActionRequest
218
+ content_dict = {}
219
+ if function:
220
+ content_dict["function"] = function
221
+ if arguments:
222
+ content_dict["arguments"] = arguments
223
+ return ActionRequest(
224
+ content=content_dict,
225
+ sender=params.get("sender"),
226
+ recipient=params.get("recipient"),
227
+ )
188
228
 
189
229
  @staticmethod
190
230
  def create_action_response(
@@ -217,17 +257,27 @@ class MessageManager(Manager):
217
257
  raise ValueError(
218
258
  "Error: please provide a corresponding action request for an action response."
219
259
  )
220
- params = {
221
- "action_request": action_request,
222
- "output": action_output,
223
- "sender": sender,
224
- "recipient": recipient,
225
- }
226
260
  if isinstance(action_response, ActionResponse):
227
- action_response.update(**params)
261
+ action_response.update(
262
+ output=action_output, sender=sender, recipient=recipient
263
+ )
228
264
  return action_response
229
265
 
230
- return ActionResponse.create(**params)
266
+ # Create new ActionResponse
267
+ content_dict = {
268
+ "function": action_request.content.function,
269
+ "arguments": action_request.content.arguments,
270
+ "output": action_output,
271
+ "action_request_id": str(action_request.id),
272
+ }
273
+ response = ActionResponse(
274
+ content=content_dict, sender=sender, recipient=recipient
275
+ )
276
+
277
+ # Update the request to reference this response
278
+ action_request.content.action_response_id = str(response.id)
279
+
280
+ return response
231
281
 
232
282
  @staticmethod
233
283
  def create_system(
@@ -236,28 +286,33 @@ class MessageManager(Manager):
236
286
  system_datetime: bool | str = None,
237
287
  sender: Any = None,
238
288
  recipient: Any = None,
239
- template: Template | str = None,
240
- template_context: dict[str, Any] = None,
241
289
  ) -> System:
242
290
  """
243
291
  Create or update a `System` message. If `system` is an instance, update.
244
292
  Otherwise, create a new System message.
245
293
  """
246
294
  params = {
247
- "system_datetime": system_datetime,
248
- "sender": sender,
249
- "recipient": recipient,
250
- "template": template,
251
- **(template_context or {}),
295
+ k: v
296
+ for k, v in locals().items()
297
+ if k != "system" and v is not None
252
298
  }
299
+
253
300
  if isinstance(system, System):
254
301
  system.update(**params)
255
302
  return system
256
303
 
304
+ # Create new System message
305
+ content_dict = {}
257
306
  if system:
258
- params["system_message"] = system
259
-
260
- return System.create(**params)
307
+ content_dict["system_message"] = system
308
+ if system_datetime is not None:
309
+ content_dict["system_datetime"] = system_datetime
310
+
311
+ return System(
312
+ content=content_dict if content_dict else None,
313
+ sender=params.get("sender"),
314
+ recipient=params.get("recipient"),
315
+ )
261
316
 
262
317
  def add_message(
263
318
  self,
@@ -265,12 +320,11 @@ class MessageManager(Manager):
265
320
  # common
266
321
  sender: SenderRecipient = None,
267
322
  recipient: SenderRecipient = None,
268
- template: Template | str = None,
269
- template_context: dict[str, Any] = None,
270
323
  metadata: dict[str, Any] = None,
271
324
  # instruction
272
325
  instruction: JsonValue = None,
273
326
  context: JsonValue = None,
327
+ handle_context: Literal["extend", "replace"] = "extend",
274
328
  guidance: JsonValue = None,
275
329
  request_fields: JsonValue = None,
276
330
  plain_content: JsonValue = None,
@@ -299,18 +353,13 @@ class MessageManager(Manager):
299
353
  - ActionRequest / ActionResponse
300
354
  """
301
355
  _msg = None
302
- if (
303
- sum(
304
- bool(x)
305
- for x in (
306
- instruction,
307
- assistant_response,
308
- system,
309
- action_request,
310
- )
311
- )
312
- > 1
313
- ):
356
+ # When creating ActionResponse, both action_request and action_output are needed
357
+ # So don't count action_request as a message type when action_output is present
358
+ message_types = [instruction, assistant_response, system]
359
+ if action_request and not action_output:
360
+ message_types.append(action_request)
361
+
362
+ if sum(bool(x) for x in message_types) > 1:
314
363
  raise ValueError("Only one message type can be added at a time.")
315
364
 
316
365
  if system:
@@ -319,8 +368,6 @@ class MessageManager(Manager):
319
368
  system_datetime=system_datetime,
320
369
  sender=sender,
321
370
  recipient=recipient,
322
- template=template,
323
- template_context=template_context,
324
371
  )
325
372
  self.set_system(_msg)
326
373
 
@@ -333,15 +380,15 @@ class MessageManager(Manager):
333
380
  recipient=recipient,
334
381
  )
335
382
 
336
- elif action_request or (action_function and action_arguments):
383
+ elif action_request or (
384
+ action_function and action_arguments is not None
385
+ ):
337
386
  _msg = self.create_action_request(
338
387
  sender=sender,
339
388
  recipient=recipient,
340
389
  function=action_function,
341
390
  arguments=action_arguments,
342
391
  action_request=action_request,
343
- template=template,
344
- template_context=template_context,
345
392
  )
346
393
 
347
394
  elif assistant_response:
@@ -349,14 +396,13 @@ class MessageManager(Manager):
349
396
  sender=sender,
350
397
  recipient=recipient,
351
398
  assistant_response=assistant_response,
352
- template=template,
353
- template_context=template_context,
354
399
  )
355
400
 
356
401
  else:
357
402
  _msg = self.create_instruction(
358
403
  instruction=instruction,
359
404
  context=context,
405
+ handle_context=handle_context,
360
406
  guidance=guidance,
361
407
  images=images,
362
408
  request_fields=request_fields,
@@ -466,7 +512,9 @@ class MessageManager(Manager):
466
512
  Convenience method to strip 'tool_schemas' from the most recent Instruction.
467
513
  """
468
514
  if self.last_instruction:
469
- self.messages[self.last_instruction.id].tool_schemas = None
515
+ self.messages[
516
+ self.last_instruction.id
517
+ ].content.tool_schemas.clear()
470
518
 
471
519
  def concat_recent_action_responses_to_instruction(
472
520
  self, instruction: Instruction
@@ -477,7 +525,9 @@ class MessageManager(Manager):
477
525
  """
478
526
  for i in reversed(list(self.messages.progression)):
479
527
  if isinstance(self.messages[i], ActionResponse):
480
- instruction.context.append(self.messages[i].content)
528
+ instruction.content.prompt_context.append(
529
+ self.messages[i].content
530
+ )
481
531
  else:
482
532
  break
483
533
 
@@ -1,252 +1,142 @@
1
1
  # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- from __future__ import annotations
4
+ from dataclasses import dataclass
5
+ from typing import Any, ClassVar
5
6
 
6
- from pathlib import Path
7
- from typing import Any
7
+ from pydantic import field_serializer, field_validator
8
8
 
9
- from jinja2 import Environment, FileSystemLoader, Template
10
- from pydantic import Field, PrivateAttr, field_serializer
11
-
12
- from lionagi import ln
9
+ from lionagi.ln.types import DataClass
13
10
 
14
11
  from .._concepts import Sendable
15
- from ..generic.element import Element, IDType
16
- from ..generic.log import Log
17
12
  from ..graph.node import Node
18
13
  from .base import (
19
- MessageFlag,
20
14
  MessageRole,
21
15
  SenderRecipient,
16
+ serialize_sender_recipient,
22
17
  validate_sender_recipient,
23
18
  )
24
19
 
25
- template_path = Path(__file__).parent / "templates"
26
- jinja_env = Environment(loader=FileSystemLoader(template_path))
27
-
28
- __all__ = ("RoledMessage",)
29
20
 
21
+ @dataclass(slots=True)
22
+ class MessageContent(DataClass):
23
+ """A base class for message content structures."""
30
24
 
31
- class RoledMessage(Node, Sendable):
32
- """
33
- A base class for all messages that have a `role` and carry structured
34
- `content`. Subclasses might be `Instruction`, `ActionRequest`, etc.
35
- """
25
+ _none_as_sentinel: ClassVar[bool] = True
36
26
 
37
- content: dict = Field(
38
- default_factory=dict,
39
- description="The content of the message.",
40
- )
27
+ @property
28
+ def rendered(self) -> str:
29
+ """Render the content as a string."""
30
+ raise NotImplementedError(
31
+ "Subclasses must implement rendered property."
32
+ )
41
33
 
42
- role: MessageRole | None = Field(
43
- None,
44
- description="The role of the message in the conversation.",
45
- )
34
+ @classmethod
35
+ def from_dict(cls, data: dict[str, Any]) -> "MessageContent":
36
+ """Create an instance from a dictionary."""
37
+ raise NotImplementedError(
38
+ "Subclasses must implement from_dict method."
39
+ )
46
40
 
47
- _flag: MessageFlag | None = PrivateAttr(None)
48
41
 
49
- template: str | Template | None = None
42
+ class RoledMessage(Node, Sendable):
43
+ """Base class for all messages with a role and structured content.
50
44
 
51
- sender: SenderRecipient | None = Field(
52
- default=MessageRole.UNSET,
53
- title="Sender",
54
- description="The ID of the sender node or a role.",
55
- )
45
+ Subclasses must provide a concrete MessageContent type.
46
+ """
56
47
 
57
- recipient: SenderRecipient | None = Field(
58
- default=MessageRole.UNSET,
59
- title="Recipient",
60
- description="The ID of the recipient node or a role.",
61
- )
48
+ role: MessageRole = MessageRole.UNSET
49
+ content: MessageContent
50
+ sender: SenderRecipient | None = MessageRole.UNSET
51
+ recipient: SenderRecipient | None = MessageRole.UNSET
62
52
 
63
53
  @field_serializer("sender", "recipient")
64
54
  def _serialize_sender_recipient(self, value: SenderRecipient) -> str:
65
- if isinstance(value, MessageRole | MessageFlag):
66
- return value.value
67
- if isinstance(value, str):
68
- return value
69
- if isinstance(value, Element):
70
- return str(value.id)
71
- if isinstance(value, IDType):
72
- return str(value)
73
- return str(value)
55
+ return serialize_sender_recipient(value)
74
56
 
75
- @property
76
- def image_content(self) -> list[dict[str, Any]] | None:
77
- """
78
- Extract structured image data from the message content if it is
79
- represented as a chat message array.
80
-
81
- Returns:
82
- list[dict[str,Any]] | None: If no images found, None.
83
- """
84
- msg_ = self.chat_msg
85
- if isinstance(msg_, dict) and isinstance(msg_["content"], list):
86
- return [i for i in msg_["content"] if i["type"] == "image_url"]
87
- return None
57
+ @field_validator("sender", "recipient")
58
+ def _validate_sender_recipient(cls, v):
59
+ if v is None:
60
+ return None
61
+ return validate_sender_recipient(v)
88
62
 
89
63
  @property
90
64
  def chat_msg(self) -> dict[str, Any] | None:
91
- """
92
- A dictionary representation typically used in chat-based contexts.
93
-
94
- Returns:
95
- dict: `{"role": <role>, "content": <rendered content>}`
96
- """
65
+ """A dictionary representation typically used in chat-based contexts."""
97
66
  try:
98
- return {"role": str(self.role), "content": self.rendered}
67
+ role_str = (
68
+ self.role.value
69
+ if isinstance(self.role, MessageRole)
70
+ else str(self.role)
71
+ )
72
+ return {"role": role_str, "content": self.rendered}
99
73
  except Exception:
100
74
  return None
101
75
 
102
76
  @property
103
77
  def rendered(self) -> str:
104
- """
105
- Attempt to format the message with a Jinja template (if provided).
106
- If no template, fallback to JSON.
78
+ """Render the message content as a string.
107
79
 
108
- Returns:
109
- str: The final formatted string.
80
+ Delegates to the content's rendered property.
110
81
  """
111
- try:
112
- if isinstance(self.template, str):
113
- return self.template.format(**self.content)
114
- if isinstance(self.template, Template):
115
- return self.template.render(**self.content)
116
- except Exception:
117
- return ln.json_dumps(
118
- self.content,
119
- pretty=True,
120
- sort_keys=True,
121
- append_newline=True,
122
- deterministic_sets=True,
123
- decimal_as_float=True,
124
- )
82
+ return self.content.rendered
125
83
 
126
- @classmethod
127
- def create(cls, **kwargs):
128
- raise NotImplementedError("create() must be implemented in subclass.")
84
+ @field_validator("role", mode="before")
85
+ def _validate_role(cls, v):
86
+ if isinstance(v, str):
87
+ return MessageRole(v)
88
+ if isinstance(v, MessageRole):
89
+ return v
90
+ return MessageRole.UNSET
129
91
 
130
- @classmethod
131
- def from_dict(cls, dict_: dict):
132
- """
133
- Deserialize a dictionary into a RoledMessage or subclass.
92
+ def update(self, sender=None, recipient=None, **kw):
93
+ """Update message fields.
134
94
 
135
95
  Args:
136
- dict_ (dict): The raw data.
137
-
138
- Returns:
139
- RoledMessage: A newly constructed instance.
96
+ sender: New sender role or ID.
97
+ recipient: New recipient role or ID.
98
+ **kw: Content updates to apply via from_dict() reconstruction.
140
99
  """
141
- try:
142
- self: RoledMessage = super().from_dict(
143
- {k: v for k, v in dict_.items() if v}
144
- )
145
- self._flag = MessageFlag.MESSAGE_LOAD
146
- return self
147
- except Exception as e:
148
- raise ValueError(f"Invalid RoledMessage data: {e}")
100
+ if sender:
101
+ self.sender = validate_sender_recipient(sender)
102
+ if recipient:
103
+ self.recipient = validate_sender_recipient(recipient)
104
+ if kw:
105
+ _dict = self.content.to_dict()
106
+ _dict.update(kw)
107
+ self.content = type(self.content).from_dict(_dict)
149
108
 
150
- def is_clone(self) -> bool:
151
- """
152
- Check if this message is flagged as a clone.
109
+ def clone(self) -> "RoledMessage":
110
+ """Create a clone with a new ID but reference to original.
153
111
 
154
112
  Returns:
155
- bool: True if flagged `MESSAGE_CLONE`.
113
+ A new message instance with a new ID and deep-copied content,
114
+ with a reference to the original message in metadata.
156
115
  """
157
- return self._flag == MessageFlag.MESSAGE_CLONE
116
+ # Create a new instance from dict, excluding frozen fields (id, created_at)
117
+ # This allows new id and created_at to be generated
118
+ data = self.to_dict()
119
+ original_id = data.pop("id")
120
+ data.pop("created_at") # Let new created_at be generated
158
121
 
159
- def clone(self, keep_role: bool = True) -> RoledMessage:
160
- """
161
- Create a shallow copy of this message, possibly resetting the role.
122
+ # Create new instance
123
+ cloned = type(self).from_dict(data)
162
124
 
163
- Args:
164
- keep_role (bool): If False, set the new message's role to `UNSET`.
165
-
166
- Returns:
167
- RoledMessage: The new cloned message.
168
- """
169
- instance = self.__class__(
170
- content=self.content,
171
- role=self.role if keep_role else MessageRole.UNSET,
172
- metadata={"clone_from": self},
173
- )
174
- instance._flag = MessageFlag.MESSAGE_CLONE
175
- return instance
176
-
177
- def to_log(self) -> Log:
178
- """
179
- Convert this message into a `Log`, preserving all current fields.
125
+ # Store reference to original in metadata
126
+ cloned.metadata["clone_from"] = str(original_id)
180
127
 
181
- Returns:
182
- Log: An immutable log entry derived from this message.
183
- """
184
- return Log.create(self)
185
-
186
- @field_serializer("role")
187
- def _serialize_role(self, value: MessageRole):
188
- if isinstance(value, MessageRole):
189
- return value.value
190
- return str(value)
191
-
192
- @field_serializer("metadata")
193
- def _serialize_metadata(self, value: dict):
194
- if "clone_from" in value:
195
- origin_obj: RoledMessage = value.pop("clone_from")
196
- origin_info = origin_obj.to_dict()
197
- value["clone_from_info"] = {
198
- "clone_from_info": {
199
- "original_id": origin_info["id"],
200
- "original_created_at": origin_info["created_at"],
201
- "original_sender": origin_info["sender"],
202
- "original_recipient": origin_info["recipient"],
203
- "original_lion_class": origin_info["metadata"][
204
- "lion_class"
205
- ],
206
- "original_role": origin_info["role"],
207
- }
208
- }
209
- return value
210
-
211
- @field_serializer("template")
212
- def _serialize_template(self, value: Template | str):
213
- # We do not store or transmit the raw Template object.
214
- if isinstance(value, Template):
215
- return None
216
- return value
128
+ return cloned
217
129
 
218
- def update(self, sender, recipient, template, **kwargs):
130
+ @property
131
+ def image_content(self) -> list[dict[str, Any]] | None:
219
132
  """
220
- Generic update mechanism for customizing the message in place.
221
-
222
- Args:
223
- sender (SenderRecipient):
224
- New sender or role.
225
- recipient (SenderRecipient):
226
- New recipient or role.
227
- template (Template | str):
228
- New jinja Template or format string.
229
- **kwargs:
230
- Additional content to merge into self.content.
133
+ Extract structured image data from the message content if it is
134
+ represented as a chat message array.
231
135
  """
232
- if sender:
233
- self.sender = validate_sender_recipient(sender)
234
- if recipient:
235
- self.recipient = validate_sender_recipient(recipient)
236
- if kwargs:
237
- self.content.update(kwargs)
238
- if template:
239
- if not isinstance(template, Template | str):
240
- raise ValueError("Template must be a Jinja2 Template or str")
241
- self.template = template
242
-
243
- def __str__(self) -> str:
244
- content_preview = (
245
- f"{str(self.content)[:75]}..."
246
- if len(str(self.content)) > 75
247
- else str(self.content)
248
- )
249
- return f"Message(role={self.role}, sender={self.sender}, content='{content_preview}')"
136
+ msg_ = self.chat_msg
137
+ if isinstance(msg_, dict) and isinstance(msg_["content"], list):
138
+ return [i for i in msg_["content"] if i["type"] == "image_url"]
139
+ return None
250
140
 
251
141
 
252
142
  # File: lionagi/protocols/messages/message.py