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