lionagi 0.7.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. lionagi/operations/ReAct/ReAct.py +2 -2
  2. lionagi/operations/communicate/communicate.py +0 -59
  3. lionagi/operations/interpret/interpret.py +1 -2
  4. lionagi/operations/operate/operate.py +10 -5
  5. lionagi/operations/parse/parse.py +0 -36
  6. lionagi/operations/plan/plan.py +3 -3
  7. lionagi/operatives/action/manager.py +105 -82
  8. lionagi/operatives/action/request_response_model.py +31 -0
  9. lionagi/operatives/action/tool.py +50 -20
  10. lionagi/protocols/_concepts.py +1 -1
  11. lionagi/protocols/adapters/adapter.py +25 -0
  12. lionagi/protocols/adapters/json_adapter.py +107 -27
  13. lionagi/protocols/adapters/pandas_/csv_adapter.py +55 -11
  14. lionagi/protocols/adapters/pandas_/excel_adapter.py +52 -10
  15. lionagi/protocols/adapters/pandas_/pd_dataframe_adapter.py +54 -4
  16. lionagi/protocols/adapters/pandas_/pd_series_adapter.py +40 -0
  17. lionagi/protocols/generic/element.py +1 -1
  18. lionagi/protocols/generic/pile.py +5 -8
  19. lionagi/protocols/graph/edge.py +1 -1
  20. lionagi/protocols/graph/graph.py +16 -8
  21. lionagi/protocols/graph/node.py +1 -1
  22. lionagi/protocols/mail/exchange.py +126 -15
  23. lionagi/protocols/mail/mail.py +33 -0
  24. lionagi/protocols/mail/mailbox.py +62 -0
  25. lionagi/protocols/mail/manager.py +97 -41
  26. lionagi/protocols/mail/package.py +57 -3
  27. lionagi/protocols/messages/action_request.py +77 -26
  28. lionagi/protocols/messages/action_response.py +55 -26
  29. lionagi/protocols/messages/assistant_response.py +50 -15
  30. lionagi/protocols/messages/base.py +36 -0
  31. lionagi/protocols/messages/instruction.py +175 -145
  32. lionagi/protocols/messages/manager.py +152 -56
  33. lionagi/protocols/messages/message.py +61 -25
  34. lionagi/protocols/messages/system.py +54 -19
  35. lionagi/service/imodel.py +24 -0
  36. lionagi/session/branch.py +40 -32
  37. lionagi/utils.py +1 -0
  38. lionagi/version.py +1 -1
  39. {lionagi-0.7.0.dist-info → lionagi-0.7.1.dist-info}/METADATA +1 -1
  40. {lionagi-0.7.0.dist-info → lionagi-0.7.1.dist-info}/RECORD +42 -42
  41. {lionagi-0.7.0.dist-info → lionagi-0.7.1.dist-info}/WHEEL +0 -0
  42. {lionagi-0.7.0.dist-info → lionagi-0.7.1.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