lionagi 0.7.0__py3-none-any.whl → 0.7.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 (43) hide show
  1. lionagi/operations/ReAct/ReAct.py +2 -2
  2. lionagi/operations/_act/act.py +10 -3
  3. lionagi/operations/communicate/communicate.py +0 -59
  4. lionagi/operations/interpret/interpret.py +1 -2
  5. lionagi/operations/operate/operate.py +10 -5
  6. lionagi/operations/parse/parse.py +0 -36
  7. lionagi/operations/plan/plan.py +3 -3
  8. lionagi/operatives/action/manager.py +105 -82
  9. lionagi/operatives/action/request_response_model.py +31 -0
  10. lionagi/operatives/action/tool.py +50 -20
  11. lionagi/protocols/_concepts.py +1 -1
  12. lionagi/protocols/adapters/adapter.py +25 -0
  13. lionagi/protocols/adapters/json_adapter.py +107 -27
  14. lionagi/protocols/adapters/pandas_/csv_adapter.py +55 -11
  15. lionagi/protocols/adapters/pandas_/excel_adapter.py +52 -10
  16. lionagi/protocols/adapters/pandas_/pd_dataframe_adapter.py +54 -4
  17. lionagi/protocols/adapters/pandas_/pd_series_adapter.py +40 -0
  18. lionagi/protocols/generic/element.py +1 -1
  19. lionagi/protocols/generic/pile.py +5 -8
  20. lionagi/protocols/graph/edge.py +1 -1
  21. lionagi/protocols/graph/graph.py +16 -8
  22. lionagi/protocols/graph/node.py +1 -1
  23. lionagi/protocols/mail/exchange.py +126 -15
  24. lionagi/protocols/mail/mail.py +33 -0
  25. lionagi/protocols/mail/mailbox.py +62 -0
  26. lionagi/protocols/mail/manager.py +97 -41
  27. lionagi/protocols/mail/package.py +57 -3
  28. lionagi/protocols/messages/action_request.py +77 -26
  29. lionagi/protocols/messages/action_response.py +55 -26
  30. lionagi/protocols/messages/assistant_response.py +50 -15
  31. lionagi/protocols/messages/base.py +36 -0
  32. lionagi/protocols/messages/instruction.py +175 -145
  33. lionagi/protocols/messages/manager.py +152 -56
  34. lionagi/protocols/messages/message.py +61 -25
  35. lionagi/protocols/messages/system.py +54 -19
  36. lionagi/service/imodel.py +24 -0
  37. lionagi/session/branch.py +40 -32
  38. lionagi/utils.py +1 -0
  39. lionagi/version.py +1 -1
  40. {lionagi-0.7.0.dist-info → lionagi-0.7.2.dist-info}/METADATA +1 -1
  41. {lionagi-0.7.0.dist-info → lionagi-0.7.2.dist-info}/RECORD +43 -43
  42. {lionagi-0.7.0.dist-info → lionagi-0.7.2.dist-info}/WHEEL +0 -0
  43. {lionagi-0.7.0.dist-info → lionagi-0.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -2,13 +2,18 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ """
6
+ Implements the `MessageManager` class, a manager for collecting or
7
+ manipulating sequences of `RoledMessage` objects, including system,
8
+ instructions, or action requests/responses.
9
+ """
10
+
5
11
  from typing import Any, Literal
6
12
 
7
13
  from jinja2 import Template
8
14
  from pydantic import BaseModel, JsonValue
9
15
 
10
16
  from .._concepts import Manager
11
- from ..generic.log import Log
12
17
  from ..generic.pile import Pile
13
18
  from ..generic.progression import Progression
14
19
  from .action_request import ActionRequest
@@ -22,6 +27,11 @@ DEFAULT_SYSTEM = "You are a helpful AI assistant. Let's think step by step."
22
27
 
23
28
 
24
29
  class MessageManager(Manager):
30
+ """
31
+ A manager maintaining an ordered list of `RoledMessage` items.
32
+ Capable of setting or replacing a system message, adding instructions,
33
+ assistant responses, or actions, and retrieving them conveniently.
34
+ """
25
35
 
26
36
  def __init__(
27
37
  self,
@@ -31,6 +41,7 @@ class MessageManager(Manager):
31
41
  ):
32
42
  super().__init__()
33
43
  m_ = []
44
+ # Attempt to parse 'messages' as a list or from a dictionary
34
45
  if isinstance(messages, list):
35
46
  for i in messages:
36
47
  if isinstance(i, dict):
@@ -58,10 +69,7 @@ class MessageManager(Manager):
58
69
 
59
70
  def set_system(self, system: System) -> None:
60
71
  """
61
- Set or update the system message.
62
-
63
- Args:
64
- system: The new system message to set
72
+ Replace or set the system message. If one existed, remove it.
65
73
  """
66
74
  if not self.system:
67
75
  self.system = system
@@ -73,17 +81,12 @@ class MessageManager(Manager):
73
81
  self.messages.exclude(old_system)
74
82
 
75
83
  async def aclear_messages(self):
76
- """Asynchronously clear all messages except system message."""
84
+ """Async clear all messages except system."""
77
85
  async with self.messages:
78
86
  self.clear_messages()
79
87
 
80
88
  async def a_add_message(self, **kwargs):
81
- """
82
- Asynchronously add a message.
83
-
84
- Args:
85
- **kwargs: Message creation parameters
86
- """
89
+ """Add a message asynchronously with a manager-level lock."""
87
90
  async with self.messages:
88
91
  return self.add_message(**kwargs)
89
92
 
@@ -103,15 +106,23 @@ class MessageManager(Manager):
103
106
  sender: SenderRecipient = None,
104
107
  recipient: SenderRecipient = None,
105
108
  ) -> Instruction:
109
+ """
110
+ Construct or update an Instruction message with advanced parameters.
106
111
 
107
- params = {k: v for k, v in locals().items() if v is not None}
112
+ If `instruction` is an existing Instruction, it is updated in place.
113
+ Otherwise, a new instance is created.
114
+ """
115
+ params = {
116
+ k: v
117
+ for k, v in locals().items()
118
+ if k != "instruction" and v is not None
119
+ }
108
120
 
109
121
  if isinstance(instruction, Instruction):
110
- params.pop("instruction")
111
122
  instruction.update(**params)
112
123
  return instruction
113
124
  else:
114
- return Instruction.create(**params)
125
+ return Instruction.create(instruction=instruction, **params)
115
126
 
116
127
  @staticmethod
117
128
  def create_assistant_response(
@@ -122,17 +133,25 @@ class MessageManager(Manager):
122
133
  template: Template | str = None,
123
134
  template_context: dict[str, Any] = None,
124
135
  ) -> AssistantResponse:
125
-
126
- params = {k: v for k, v in locals().items() if v is not None}
127
- template_context = params.pop("template_context", {})
128
- params.update(template_context)
136
+ """
137
+ Build or update an `AssistantResponse`. If `assistant_response` is an
138
+ existing instance, it's updated. Otherwise, a new one is created.
139
+ """
140
+ params = {
141
+ k: v
142
+ for k, v in locals().items()
143
+ if k not in ["assistant_response", "template_context"]
144
+ }
145
+ t_ctx = template_context or {}
146
+ params.update(t_ctx)
129
147
 
130
148
  if isinstance(assistant_response, AssistantResponse):
131
- params.pop("assistant_response")
132
149
  assistant_response.update(**params)
133
150
  return assistant_response
134
151
 
135
- return AssistantResponse.create(**params)
152
+ return AssistantResponse.create(
153
+ assistant_response=assistant_response, **params
154
+ )
136
155
 
137
156
  @staticmethod
138
157
  def create_action_request(
@@ -145,20 +164,33 @@ class MessageManager(Manager):
145
164
  template: Template | str = None,
146
165
  template_context: dict[str, Any] = None,
147
166
  ) -> ActionRequest:
167
+ """
168
+ Build or update an ActionRequest.
148
169
 
170
+ Args:
171
+ sender: Sender role or ID.
172
+ recipient: Recipient role or ID.
173
+ function: Function name for the request.
174
+ arguments: Arguments for the function.
175
+ action_request: Possibly existing ActionRequest to update.
176
+ template: Optional jinja template.
177
+ template_context: Extra context for the template.
178
+
179
+ Returns:
180
+ ActionRequest: The new or updated request object.
181
+ """
149
182
  params = {
150
183
  "sender": sender,
151
184
  "recipient": recipient,
152
185
  "function": function,
153
186
  "arguments": arguments,
154
187
  "template": template,
155
- **(template_context or {}),
156
188
  }
189
+ params.update(template_context or {})
157
190
 
158
191
  if isinstance(action_request, ActionRequest):
159
192
  action_request.update(**params)
160
193
  return action_request
161
-
162
194
  return ActionRequest.create(**params)
163
195
 
164
196
  @staticmethod
@@ -170,20 +202,35 @@ class MessageManager(Manager):
170
202
  sender: SenderRecipient = None,
171
203
  recipient: SenderRecipient = None,
172
204
  ) -> ActionResponse:
205
+ """
206
+ Create or update an ActionResponse, referencing a prior ActionRequest.
173
207
 
208
+ Args:
209
+ action_request (ActionRequest):
210
+ The request being answered.
211
+ action_output (Any):
212
+ The result of the invoked function.
213
+ action_response (ActionResponse|Any):
214
+ Possibly existing ActionResponse to update.
215
+ sender:
216
+ Sender ID or role.
217
+ recipient:
218
+ Recipient ID or role.
219
+
220
+ Returns:
221
+ ActionResponse: The newly created or updated response object.
222
+ """
174
223
  if not isinstance(action_request, ActionRequest):
175
224
  raise ValueError(
176
225
  "Error: please provide a corresponding action request for an "
177
226
  "action response."
178
227
  )
179
-
180
228
  params = {
181
229
  "action_request": action_request,
182
230
  "output": action_output,
183
231
  "sender": sender,
184
232
  "recipient": recipient,
185
233
  }
186
-
187
234
  if isinstance(action_response, ActionResponse):
188
235
  action_response.update(**params)
189
236
  return action_response
@@ -200,6 +247,10 @@ class MessageManager(Manager):
200
247
  template: Template | str = None,
201
248
  template_context: dict[str, Any] = None,
202
249
  ) -> System:
250
+ """
251
+ Create or update a `System` message. If `system` is an instance, update.
252
+ Otherwise, create a new System message.
253
+ """
203
254
  params = {
204
255
  "system_datetime": system_datetime,
205
256
  "sender": sender,
@@ -247,8 +298,14 @@ class MessageManager(Manager):
247
298
  action_output: Any = None,
248
299
  action_request: ActionRequest | None = None,
249
300
  action_response: ActionResponse | Any = None,
250
- ) -> tuple[RoledMessage, Log]:
251
-
301
+ ) -> RoledMessage:
302
+ """
303
+ The central method to add a new message of various types:
304
+ - System
305
+ - Instruction
306
+ - AssistantResponse
307
+ - ActionRequest / ActionResponse
308
+ """
252
309
  _msg = None
253
310
  if (
254
311
  sum(
@@ -334,75 +391,102 @@ class MessageManager(Manager):
334
391
  return _msg
335
392
 
336
393
  def clear_messages(self):
394
+ """Remove all messages except the system message if it exists."""
337
395
  self.messages.clear()
338
396
  if self.system:
339
397
  self.messages.insert(0, self.system)
340
398
 
341
399
  @property
342
400
  def last_response(self) -> AssistantResponse | None:
343
- """Get the last assistant response message."""
344
- for i in reversed(self.messages.progression):
345
- if isinstance(self.messages[i], AssistantResponse):
346
- return self.messages[i]
401
+ """
402
+ Retrieve the most recent `AssistantResponse`.
403
+ """
404
+ for mid in reversed(self.messages.progression):
405
+ if isinstance(self.messages[mid], AssistantResponse):
406
+ return self.messages[mid]
407
+ return None
347
408
 
348
409
  @property
349
410
  def last_instruction(self) -> Instruction | None:
350
- """Get the last instruction message."""
351
- for i in reversed(self.messages.progression):
352
- if isinstance(self.messages[i], Instruction):
353
- return self.messages[i]
411
+ """
412
+ Retrieve the most recent `Instruction`.
413
+ """
414
+ for mid in reversed(self.messages.progression):
415
+ if isinstance(self.messages[mid], Instruction):
416
+ return self.messages[mid]
417
+ return None
354
418
 
355
419
  @property
356
420
  def assistant_responses(self) -> Pile[AssistantResponse]:
357
- """Get all assistant response messages."""
421
+ """All `AssistantResponse` messages in the manager."""
422
+ return Pile(
423
+ collections=[
424
+ self.messages[mid]
425
+ for mid in self.messages.progression
426
+ if isinstance(self.messages[mid], AssistantResponse)
427
+ ]
428
+ )
429
+
430
+ @property
431
+ def actions(self) -> Pile[ActionRequest | ActionResponse]:
432
+ """All action messages in the manager."""
358
433
  return Pile(
359
434
  collections=[
360
- self.messages[i]
361
- for i in self.messages.progression
362
- if isinstance(self.messages[i], AssistantResponse)
435
+ self.messages[mid]
436
+ for mid in self.messages.progression
437
+ if isinstance(
438
+ self.messages[mid], (ActionRequest, ActionResponse)
439
+ )
363
440
  ]
364
441
  )
365
442
 
366
443
  @property
367
444
  def action_requests(self) -> Pile[ActionRequest]:
368
- """Get all action request messages."""
445
+ """All `ActionRequest` messages in the manager."""
369
446
  return Pile(
370
447
  collections=[
371
- self.messages[i]
372
- for i in self.messages.progression
373
- if isinstance(self.messages[i], ActionRequest)
448
+ self.messages[mid]
449
+ for mid in self.messages.progression
450
+ if isinstance(self.messages[mid], ActionRequest)
374
451
  ]
375
452
  )
376
453
 
377
454
  @property
378
455
  def action_responses(self) -> Pile[ActionResponse]:
379
- """Get all action response messages."""
456
+ """All `ActionResponse` messages in the manager."""
380
457
  return Pile(
381
458
  collections=[
382
- self.messages[i]
383
- for i in self.messages.progression
384
- if isinstance(self.messages[i], ActionResponse)
459
+ self.messages[mid]
460
+ for mid in self.messages.progression
461
+ if isinstance(self.messages[mid], ActionResponse)
385
462
  ]
386
463
  )
387
464
 
388
465
  @property
389
466
  def instructions(self) -> Pile[Instruction]:
390
- """Get all instruction messages."""
467
+ """All `Instruction` messages in the manager."""
391
468
  return Pile(
392
469
  collections=[
393
- self.messages[i]
394
- for i in self.messages.progression
395
- if isinstance(self.messages[i], Instruction)
470
+ self.messages[mid]
471
+ for mid in self.messages.progression
472
+ if isinstance(self.messages[mid], Instruction)
396
473
  ]
397
474
  )
398
475
 
399
476
  def remove_last_instruction_tool_schemas(self) -> None:
400
- id_ = self.last_instruction.id
401
- self.messages[id_].tool_schemas = None
477
+ """
478
+ Convenience method to strip 'tool_schemas' from the most recent Instruction.
479
+ """
480
+ if self.last_instruction:
481
+ self.messages[self.last_instruction.id].tool_schemas = None
402
482
 
403
483
  def concat_recent_action_responses_to_instruction(
404
484
  self, instruction: Instruction
405
485
  ) -> None:
486
+ """
487
+ Example method to merge the content of recent ActionResponses
488
+ into an instruction's context.
489
+ """
406
490
  for i in reversed(self.messages.progression):
407
491
  if isinstance(self.messages[i], ActionResponse):
408
492
  instruction.context.append(self.messages[i].content)
@@ -410,16 +494,25 @@ class MessageManager(Manager):
410
494
  break
411
495
 
412
496
  def to_chat_msgs(self, progression=None) -> list[dict]:
497
+ """
498
+ Convert a subset (or all) of messages into a chat representation array.
499
+
500
+ Args:
501
+ progression (Optional[Sequence]): A subset of message IDs or the full progression.
502
+
503
+ Returns:
504
+ list[dict]: Each item is a dict with 'role' and 'content'.
505
+ """
413
506
  if progression == []:
414
507
  return []
415
508
  try:
416
509
  return [
417
- self.messages[i].chat_msg
418
- for i in (progression or self.progression)
510
+ self.messages[mid].chat_msg
511
+ for mid in (progression or self.progression)
419
512
  ]
420
513
  except Exception as e:
421
514
  raise ValueError(
422
- "Invalid progress, not all requested messages are in the message pile"
515
+ "One or more messages in the requested progression are invalid."
423
516
  ) from e
424
517
 
425
518
  def __bool__(self):
@@ -427,3 +520,6 @@ class MessageManager(Manager):
427
520
 
428
521
  def __contains__(self, message: RoledMessage) -> bool:
429
522
  return message in self.messages
523
+
524
+
525
+ # File: lionagi/protocols/messages/manager.py
@@ -2,6 +2,11 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ """
6
+ Implements the `RoledMessage` base for system, user, assistant,
7
+ and action messages, plus Jinja2 environment and template loading.
8
+ """
9
+
5
10
  import json
6
11
  from pathlib import Path
7
12
  from typing import Any
@@ -27,6 +32,10 @@ __all__ = ("RoledMessage",)
27
32
 
28
33
 
29
34
  class RoledMessage(Node, Sendable):
35
+ """
36
+ A base class for all messages that have a `role` and carry structured
37
+ `content`. Subclasses might be `Instruction`, `ActionRequest`, etc.
38
+ """
30
39
 
31
40
  content: dict = Field(
32
41
  default_factory=dict,
@@ -69,11 +78,11 @@ class RoledMessage(Node, Sendable):
69
78
  @property
70
79
  def image_content(self) -> list[dict[str, Any]] | None:
71
80
  """
72
- Return image content if present in the message.
81
+ Extract structured image data from the message content if it is
82
+ represented as a chat message array.
73
83
 
74
84
  Returns:
75
- Optional[List[Dict[str, Any]]]: A list of image content
76
- dictionaries, or None if no images are present.
85
+ list[dict[str,Any]] | None: If no images found, None.
77
86
  """
78
87
  msg_ = self.chat_msg
79
88
  if isinstance(msg_, dict) and isinstance(msg_["content"], list):
@@ -83,11 +92,10 @@ class RoledMessage(Node, Sendable):
83
92
  @property
84
93
  def chat_msg(self) -> dict[str, Any] | None:
85
94
  """
86
- Return message in chat representation format.
95
+ A dictionary representation typically used in chat-based contexts.
87
96
 
88
97
  Returns:
89
- Optional[Dict[str, Any]]: The message formatted for chat use,
90
- or None if formatting fails.
98
+ dict: `{"role": <role>, "content": <rendered content>}`
91
99
  """
92
100
  try:
93
101
  return {"role": str(self.role), "content": self.rendered}
@@ -96,6 +104,13 @@ class RoledMessage(Node, Sendable):
96
104
 
97
105
  @property
98
106
  def rendered(self) -> str:
107
+ """
108
+ Attempt to format the message with a Jinja template (if provided).
109
+ If no template, fallback to JSON.
110
+
111
+ Returns:
112
+ str: The final formatted string.
113
+ """
99
114
  try:
100
115
  if isinstance(self.template, str):
101
116
  return self.template.format(**self.content)
@@ -106,12 +121,19 @@ class RoledMessage(Node, Sendable):
106
121
 
107
122
  @classmethod
108
123
  def create(cls, **kwargs):
109
- raise NotImplementedError(
110
- "create method must be implemented in subclass"
111
- )
124
+ raise NotImplementedError("create() must be implemented in subclass.")
112
125
 
113
126
  @classmethod
114
127
  def from_dict(cls, dict_: dict):
128
+ """
129
+ Deserialize a dictionary into a RoledMessage or subclass.
130
+
131
+ Args:
132
+ dict_ (dict): The raw data.
133
+
134
+ Returns:
135
+ RoledMessage: A newly constructed instance.
136
+ """
115
137
  try:
116
138
  self: RoledMessage = super().from_dict(
117
139
  {k: v for k, v in dict_.items() if v}
@@ -123,14 +145,23 @@ class RoledMessage(Node, Sendable):
123
145
 
124
146
  def is_clone(self) -> bool:
125
147
  """
126
- Check if the message is a clone of another message.
148
+ Check if this message is flagged as a clone.
127
149
 
128
150
  Returns:
129
- bool: True if the message is a clone, False otherwise.
151
+ bool: True if flagged `MESSAGE_CLONE`.
130
152
  """
131
153
  return self._flag == MessageFlag.MESSAGE_CLONE
132
154
 
133
155
  def clone(self, keep_role: bool = True) -> "RoledMessage":
156
+ """
157
+ Create a shallow copy of this message, possibly resetting the role.
158
+
159
+ Args:
160
+ keep_role (bool): If False, set the new message's role to `UNSET`.
161
+
162
+ Returns:
163
+ RoledMessage: The new cloned message.
164
+ """
134
165
  instance = self.__class__(
135
166
  content=self.content,
136
167
  role=self.role if keep_role else MessageRole.UNSET,
@@ -141,27 +172,15 @@ class RoledMessage(Node, Sendable):
141
172
 
142
173
  def to_log(self) -> Log:
143
174
  """
144
- Convert the message to a Log object.
145
-
146
- Creates a Log instance containing the message content and additional
147
- information as loginfo.
175
+ Convert this message into a `Log`, preserving all current fields.
148
176
 
149
177
  Returns:
150
- Log: A Log object representing the message.
178
+ Log: An immutable log entry derived from this message.
151
179
  """
152
180
  return Log.create(self)
153
181
 
154
182
  @field_serializer("role")
155
183
  def _serialize_role(self, value: MessageRole):
156
- """
157
- Serialize the role for storage or transmission.
158
-
159
- Args:
160
- value: The MessageRole to serialize
161
-
162
- Returns:
163
- str: The string value of the role
164
- """
165
184
  if isinstance(value, MessageRole):
166
185
  return value.value
167
186
  return str(value)
@@ -187,11 +206,25 @@ class RoledMessage(Node, Sendable):
187
206
 
188
207
  @field_serializer("template")
189
208
  def _serialize_template(self, value: Template | str):
209
+ # We do not store or transmit the raw Template object.
190
210
  if isinstance(value, Template):
191
211
  return None
192
212
  return value
193
213
 
194
214
  def update(self, sender, recipient, template, **kwargs):
215
+ """
216
+ Generic update mechanism for customizing the message in place.
217
+
218
+ Args:
219
+ sender (SenderRecipient):
220
+ New sender or role.
221
+ recipient (SenderRecipient):
222
+ New recipient or role.
223
+ template (Template | str):
224
+ New jinja Template or format string.
225
+ **kwargs:
226
+ Additional content to merge into self.content.
227
+ """
195
228
  if sender:
196
229
  self.sender = validate_sender_recipient(sender)
197
230
  if recipient:
@@ -214,3 +247,6 @@ class RoledMessage(Node, Sendable):
214
247
  f"Message(role={self.role}, sender={self.sender}, "
215
248
  f"content='{content_preview}')"
216
249
  )
250
+
251
+
252
+ # File: lionagi/protocols/messages/message.py