lionagi 0.0.201__py3-none-any.whl → 0.0.204__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 (53) hide show
  1. lionagi/_services/anthropic.py +79 -1
  2. lionagi/_services/base_service.py +1 -1
  3. lionagi/_services/services.py +61 -25
  4. lionagi/_services/transformers.py +46 -0
  5. lionagi/agents/__init__.py +0 -0
  6. lionagi/configs/oai_configs.py +1 -1
  7. lionagi/configs/openrouter_configs.py +1 -1
  8. lionagi/core/__init__.py +3 -7
  9. lionagi/core/branch/__init__.py +0 -0
  10. lionagi/core/branch/branch.py +589 -0
  11. lionagi/core/branch/branch_manager.py +139 -0
  12. lionagi/core/branch/cluster.py +1 -0
  13. lionagi/core/branch/conversation.py +484 -0
  14. lionagi/core/core_util.py +59 -0
  15. lionagi/core/flow/__init__.py +0 -0
  16. lionagi/core/flow/flow.py +19 -0
  17. lionagi/core/instruction_set/__init__.py +0 -0
  18. lionagi/core/instruction_set/instruction_set.py +343 -0
  19. lionagi/core/messages/__init__.py +0 -0
  20. lionagi/core/messages/messages.py +176 -0
  21. lionagi/core/sessions/__init__.py +0 -0
  22. lionagi/core/sessions/session.py +428 -0
  23. lionagi/models/__init__.py +0 -0
  24. lionagi/models/base_model.py +0 -0
  25. lionagi/models/imodel.py +53 -0
  26. lionagi/schema/data_logger.py +75 -155
  27. lionagi/tests/test_utils/test_call_util.py +658 -657
  28. lionagi/tools/tool_manager.py +121 -188
  29. lionagi/utils/__init__.py +5 -10
  30. lionagi/utils/call_util.py +667 -585
  31. lionagi/utils/io_util.py +3 -0
  32. lionagi/utils/nested_util.py +17 -211
  33. lionagi/utils/pd_util.py +57 -0
  34. lionagi/utils/sys_util.py +220 -184
  35. lionagi/utils/url_util.py +55 -0
  36. lionagi/version.py +1 -1
  37. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/METADATA +12 -8
  38. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/RECORD +47 -32
  39. lionagi/core/branch.py +0 -193
  40. lionagi/core/conversation.py +0 -341
  41. lionagi/core/flow.py +0 -8
  42. lionagi/core/instruction_set.py +0 -150
  43. lionagi/core/messages.py +0 -243
  44. lionagi/core/sessions.py +0 -474
  45. /lionagi/{tools → agents}/planner.py +0 -0
  46. /lionagi/{tools → agents}/prompter.py +0 -0
  47. /lionagi/{tools → agents}/scorer.py +0 -0
  48. /lionagi/{tools → agents}/summarizer.py +0 -0
  49. /lionagi/{tools → agents}/validator.py +0 -0
  50. /lionagi/core/{flow_util.py → flow/flow_util.py} +0 -0
  51. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/LICENSE +0 -0
  52. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/WHEEL +0 -0
  53. {lionagi-0.0.201.dist-info → lionagi-0.0.204.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,139 @@
1
+ from typing import Dict, Any
2
+ from collections import deque
3
+ from enum import Enum
4
+
5
+
6
+ class RequestTitle(str, Enum):
7
+ """
8
+ An enumeration of valid request titles for communication requests.
9
+
10
+ Attributes:
11
+ MESSAGES (str): Represents a request carrying messages.
12
+ TOOL (str): Represents a request for tool information or actions.
13
+ SERVICE (str): Represents a request related to services.
14
+ LLMCONFIG (str): Represents a request to configure or modify LLM settings.
15
+ """
16
+ MESSAGES = 'messages'
17
+ TOOL = 'tool'
18
+ SERVICE = 'service'
19
+ LLMCONFIG = 'llmconfig'
20
+
21
+
22
+ class Request:
23
+ """
24
+ Represents a request for communication between components in the system.
25
+
26
+ Args:
27
+ from_name (str): The name of the sender.
28
+ to_name (str): The name of the recipient.
29
+ title (Union[str, RequestTitle]): The title of the request, indicating its type or category.
30
+ request (Any): The actual content or data of the request.
31
+
32
+ Raises:
33
+ ValueError: If the request title is invalid or not recognized.
34
+ """
35
+
36
+ def __init__(self, from_name, to_name, title, request):
37
+ self.from_ = from_name
38
+ self.to_ = to_name
39
+ try:
40
+ if isinstance(title, str):
41
+ title = RequestTitle(title)
42
+ if isinstance(title, RequestTitle):
43
+ self.title = title
44
+ else:
45
+ raise ValueError(f'Invalid request title. Valid titles are {list(RequestTitle)}')
46
+ except:
47
+ raise ValueError(f'Invalid request title. Valid titles are {list(RequestTitle)}')
48
+ self.request = request
49
+
50
+
51
+ class BranchManager:
52
+ """
53
+ Manages branches and their communication requests within a system.
54
+
55
+ Args:
56
+ sources (Dict[str, Any]): Initial mapping of source names to their respective details.
57
+
58
+ Methods:
59
+ add_source: Adds a new source to the manager.
60
+ delete_source: Removes a source from the manager.
61
+ collect: Collects outgoing requests from a specified source and queues them for the recipient.
62
+ send: Sends the queued requests to their respective recipients in the system.
63
+ """
64
+
65
+ def __init__(self, sources: Dict[str, Any]):
66
+ self.sources = sources
67
+ self.requests = {}
68
+ for key in self.sources.keys():
69
+ self.requests[key] = {}
70
+
71
+ def add_source(self, sources: Dict[str, Any]):
72
+ """
73
+ Adds a new source or multiple sources to the manager.
74
+
75
+ Args:
76
+ sources (Dict[str, Any]): A dictionary mapping new source names to their details.
77
+
78
+ Raises:
79
+ ValueError: If any of the provided source names already exist.
80
+ """
81
+ for key in sources.keys():
82
+ if key in self.sources:
83
+ raise ValueError(f'{key} exists, please input a different name.')
84
+ self.sources[key] = {}
85
+
86
+ def delete_source(self, source_name):
87
+ """
88
+ Deletes a source from the manager by name.
89
+
90
+ Args:
91
+ source_name (str): The name of the source to delete.
92
+
93
+ Raises:
94
+ ValueError: If the specified source name does not exist.
95
+ """
96
+ if source_name not in self.sources:
97
+ raise ValueError(f'{source_name} does not exist.')
98
+ self.sources.pop(source_name)
99
+
100
+ def collect(self, from_name):
101
+ """
102
+ Collects all outgoing requests from a specified source.
103
+
104
+ Args:
105
+ from_name (str): The name of the source from which to collect outgoing requests.
106
+
107
+ Raises:
108
+ ValueError: If the specified source name does not exist.
109
+ """
110
+ if from_name not in self.sources:
111
+ raise ValueError(f'{from_name} does not exist.')
112
+ while self.sources[from_name].pending_outs:
113
+ request = self.sources[from_name].pending_outs.popleft()
114
+ if request.from_ not in self.requests[request.to_]:
115
+ self.requests[request.to_] = {request.from_: deque()}
116
+ self.requests[request.to_][request.from_].append(request)
117
+
118
+ def send(self, to_name):
119
+ """
120
+ Sends all queued requests to a specified recipient.
121
+
122
+ Args:
123
+ to_name (str): The name of the recipient to whom the requests should be sent.
124
+
125
+ Raises:
126
+ ValueError: If the specified recipient name does not exist or there are no requests to send.
127
+ """
128
+ if to_name not in self.sources:
129
+ raise ValueError(f'{to_name} does not exist.')
130
+ if not self.requests[to_name]:
131
+ return
132
+ else:
133
+ for key in list(self.requests[to_name].keys()):
134
+ request = self.requests[to_name].pop(key)
135
+ if key not in self.sources[to_name].pending_ins:
136
+ self.sources[to_name].pending_ins[key] = request
137
+ else:
138
+ self.sources[to_name].pending_ins[key].append(request)
139
+
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1,484 @@
1
+ import json
2
+ import pandas as pd
3
+ from datetime import datetime
4
+ from typing import Any, Optional, Dict, Union
5
+ from lionagi.schema import DataLogger
6
+ from lionagi.utils import lcall, as_dict
7
+ from ..messages.messages import Message, System, Instruction, Response
8
+ from ..core_util import sign_message, validate_messages
9
+
10
+
11
+ class Conversation:
12
+ """
13
+ Represents a conversation with messages, tools, and instructions.
14
+
15
+ A `Conversation` is a container for messages exchanged in a conversation, as well as tools and instructions
16
+ for interacting with external services or tools.
17
+
18
+ Attributes:
19
+ messages (pd.DataFrame): A DataFrame containing conversation messages.
20
+ _logger (DataLogger): An instance of DataLogger for logging.
21
+ """
22
+
23
+ def __init__(self, dir: Optional[str] = None) -> None:
24
+ """
25
+ Initializes a Conversation object with an empty DataFrame for messages and a DataLogger.
26
+
27
+ Args:
28
+ dir (Optional[str]): The directory path for storing logs. Defaults to None.
29
+
30
+ Examples:
31
+ >>> conversation = Conversation(dir='logs/')
32
+ """
33
+ self.messages = pd.DataFrame(columns=["node_id", "role", "sender", "timestamp", "content"])
34
+ self.logger = DataLogger(dir=dir)
35
+
36
+ def _create_message(
37
+ self,
38
+ system: Optional[Union[dict, list, System]] = None,
39
+ instruction: Optional[Union[dict, list, Instruction]] = None,
40
+ context: Optional[Union[str, Dict[str, Any]]] = None,
41
+ response: Optional[Union[dict, list, Response]] = None,
42
+ sender: Optional[str] = None
43
+ ) -> Message:
44
+ """
45
+ Creates a Message object based on the given parameters.
46
+
47
+ Only one of `system`, `instruction`, or `response` can be provided to create a message.
48
+
49
+ Args:
50
+ system (Optional[Union[dict, list, System]]): The system message content.
51
+ instruction (Optional[Union[dict, list, Instruction]]): The instruction content.
52
+ context (Optional[Union[str, Dict[str, Any]]]): The context associated with the instruction.
53
+ response (Optional[Union[dict, list, Response]]): The response content.
54
+ sender (Optional[str]): The sender of the message.
55
+
56
+ Returns:
57
+ Message: A message object created from the provided parameters.
58
+
59
+ Raises:
60
+ ValueError: If more than one or none of the parameters (system, instruction, response) are provided.
61
+
62
+ Examples:
63
+ >>> conversation = Conversation()
64
+ >>> msg = conversation._create_message(system="System message", sender="system")
65
+ """
66
+
67
+ if sum(lcall([system, instruction, response], bool)) != 1:
68
+ raise ValueError("Error: Message must have one and only one role.")
69
+ else:
70
+ if isinstance(any([system, instruction, response]), Message):
71
+ if system:
72
+ return system
73
+ elif instruction:
74
+ return instruction
75
+ elif response:
76
+ return response
77
+
78
+ msg = 0
79
+ if response:
80
+ msg = Response(response=response, sender=sender)
81
+ elif instruction:
82
+ msg = Instruction(instruction=instruction,
83
+ context=context, sender=sender)
84
+ elif system:
85
+ msg = System(system=system, sender=sender)
86
+ return msg
87
+
88
+ def add_message(
89
+ self,
90
+ system: Optional[Union[dict, list, System]] = None,
91
+ instruction: Optional[Union[dict, list, Instruction]] = None,
92
+ context: Optional[Union[str, Dict[str, Any]]] = None,
93
+ response: Optional[Union[dict, list, Response]] = None,
94
+ sender: Optional[str] = None
95
+ ) -> None:
96
+ """
97
+ Creates and adds a new message to the conversation's messages DataFrame.
98
+ Args:
99
+ system (Optional[System]): Content for a system message.
100
+ instruction (Optional[Instruction]): Content for an instruction message.
101
+ context (Optional[Union[str, Dict[str, Any]]]): Context for the instruction message.
102
+ response (Optional[Response]): Content for a response message.
103
+ sender (Optional[str]): The sender of the message.
104
+
105
+ Examples:
106
+ >>> conversation = Conversation()
107
+ >>> conversation.add_message(instruction="What's the weather?", sender="user")
108
+ """
109
+ msg = self._create_message(
110
+ system=system, instruction=instruction,
111
+ context=context, response=response, sender=sender
112
+ )
113
+ message_dict = msg.to_dict()
114
+ if isinstance(as_dict(message_dict['content']), dict):
115
+ message_dict['content'] = json.dumps(message_dict['content'])
116
+ message_dict['timestamp'] = datetime.now()
117
+ self.messages.loc[len(self.messages)] = message_dict
118
+
119
+ @property
120
+ def last_row(self) -> pd.Series:
121
+ """
122
+ Retrieves the last row from the messages DataFrame.
123
+
124
+ Returns:
125
+ pd.Series: A Series object representing the last message.
126
+ """
127
+ return self.messages.iloc[-1]
128
+
129
+ @property
130
+ def first_system(self) -> pd.Series:
131
+ """
132
+ Retrieves the first system message from the messages DataFrame.
133
+
134
+ Returns:
135
+ pd.Series: A Series object representing the first system message.
136
+ """
137
+ return self.messages[self.messages.role == 'system'].iloc[0]
138
+
139
+ @property
140
+ def last_response(self) -> pd.Series:
141
+ """
142
+ Retrieves the last response message from the messages DataFrame.
143
+
144
+ Returns:
145
+ pd.Series: A Series object representing the last response message.
146
+ """
147
+ return self.get_last_rows(role='assistant')
148
+
149
+ @property
150
+ def last_instruction(self) -> pd.Series:
151
+ """
152
+ Retrieves the last instruction message from the messages DataFrame.
153
+
154
+ Returns:
155
+ pd.Series: A Series object representing the last instruction message.
156
+ """
157
+ return self.get_last_rows(role='user')
158
+
159
+ def get_last_rows(
160
+ self,
161
+ sender: Optional[str] = None,
162
+ role: Optional[str] = None,
163
+ n: int = 1,
164
+ sign_ = False
165
+ ) -> Union[pd.DataFrame, pd.Series]:
166
+ """
167
+ Retrieves the last n rows from the messages DataFrame filtered by sender or role.
168
+
169
+ Args:
170
+ sender (Optional[str]): The sender filter for the messages.
171
+ role (Optional[str]): The role filter for the messages.
172
+ n (int): The number of rows to retrieve.
173
+ sign_: If sign messages with a sender identifier.
174
+
175
+ Returns:
176
+ Union[pd.DataFrame, pd.Series]: The last n messages as a DataFrame or a single message as a Series.
177
+
178
+ Raises:
179
+ ValueError: If both sender and role are provided or if none is provided.
180
+ """
181
+
182
+ if sender is None and role is None:
183
+ outs = self.messages.iloc[-n:]
184
+ elif sender and role:
185
+ outs = self.messages[(self.messages['sender'] == sender) & (self.messages['role'] == role)].iloc[-n:]
186
+ elif sender:
187
+ outs = self.messages[self.messages['sender'] == sender].iloc[-n:]
188
+ else:
189
+ outs = self.messages[self.messages['role'] == role].iloc[-n:]
190
+
191
+ return sign_message(outs, sender) if sign_ else outs
192
+
193
+ def filter_messages_by(
194
+ self,
195
+ role: Optional[str] = None,
196
+ sender: Optional[str] = None,
197
+ start_time: Optional[datetime] = None,
198
+ end_time: Optional[datetime] = None,
199
+ content_keywords: Optional[Union[str, list]] = None,
200
+ case_sensitive: bool = False
201
+ ) -> pd.DataFrame:
202
+ """
203
+ Retrieves messages filtered by a specific criterion.
204
+
205
+ Args:
206
+ role (Optional[str]): The role to filter the messages.
207
+ sender (Optional[str]): The sender to filter the messages.
208
+ start_time (Optional[datetime]): The start time to filter the messages.
209
+ end_time (Optional[datetime]): The end time to filter the messages.
210
+ content_keywords (Optional[Union[str, list]]): The content to filter the messages.
211
+ case_sensitive (bool): Flag to indicate if the search should be case sensitive. Defaults to False.
212
+
213
+ Returns:
214
+ pd.DataFrame: A DataFrame containing filtered messages.
215
+
216
+ Raises:
217
+ ValueError: If more than one or none of the filtering criteria are provided.
218
+ """
219
+ outs = self.messages.copy()
220
+
221
+ if content_keywords:
222
+ outs = self.search_keywords(content_keywords, case_sensitive)
223
+
224
+ outs = outs[outs['role'] == role] if role else outs
225
+ outs = outs[outs['sender'] == sender] if sender else outs
226
+ outs = outs[outs['timestamp'] > start_time] if start_time else outs
227
+ outs = outs[outs['timestamp'] < end_time] if end_time else outs
228
+ return outs
229
+
230
+ def replace_keyword(
231
+ self,
232
+ keyword: str,
233
+ replacement: str,
234
+ case_sensitive: bool = False
235
+ ) -> None:
236
+ """
237
+ Replaces a keyword in the content of all messages with a replacement string.
238
+
239
+ Args:
240
+ keyword (str): The keyword to replace.
241
+ replacement (str): The string to replace the keyword with.
242
+ case_sensitive (bool, optional): Flag to indicate if the replacement should be case sensitive. Defaults to False.
243
+ """
244
+ if not case_sensitive:
245
+ self.messages["content"] = self.messages["content"].str.replace(
246
+ keyword, replacement, case=False
247
+ )
248
+ else:
249
+ self.messages["content"] = self.messages["content"].str.replace(
250
+ keyword, replacement
251
+ )
252
+
253
+ def search_keywords(
254
+ self,
255
+ keywords: Union[str, list],
256
+ case_sensitive: bool = False
257
+ ) -> pd.DataFrame:
258
+ """
259
+ Searches for a keyword in the content of all messages and returns the messages containing it.
260
+
261
+ Args:
262
+ keywords (str): The keywords to search for.
263
+ case_sensitive (bool, optional): Flag to indicate if the search should be case sensitive. Defaults to False.
264
+
265
+ Returns:
266
+ pd.DataFrame: A DataFrame containing messages with the specified keyword.
267
+ """
268
+ if isinstance(keywords, list):
269
+ keywords = '|'.join(keywords)
270
+ if not case_sensitive:
271
+ return self.messages[
272
+ self.messages["content"].str.contains(keywords, case=False)
273
+ ]
274
+ return self.messages[self.messages["content"].str.contains(keywords)]
275
+
276
+ def remove_from_messages(self, message_id: str) -> bool:
277
+ """
278
+ Removes a message from the conversation based on its message ID.
279
+
280
+ Args:
281
+ message_id (str): The ID of the message to be removed.
282
+
283
+ Returns:
284
+ bool: True if the message was successfully removed, False otherwise.
285
+ """
286
+ initial_length = len(self.messages)
287
+ self.messages = self.messages[self.messages["node_id"] != message_id]
288
+ return len(self.messages) < initial_length
289
+
290
+ def update_messages_content(
291
+ self,
292
+ message_id: str,
293
+ col: str,
294
+ value: Any
295
+ ) -> bool:
296
+ """
297
+ Updates the content of a specific message in the conversation.
298
+
299
+ Args:
300
+ message_id (str): The ID of the message to be updated.
301
+ col (str): The column of the message that needs to be updated.
302
+ value (Any): The new value to be set for the specified column.
303
+
304
+ Returns:
305
+ bool: True if the update was successful, False otherwise.
306
+
307
+ Examples:
308
+ >>> conversation = Conversation()
309
+ >>> conversation.add_message(system="Initial message", sender="system")
310
+ >>> success = conversation.update_messages_content(
311
+ ... message_id="1", col="content", value="Updated message")
312
+ """
313
+ index = self.messages.index[self.messages["id_"] == message_id].tolist()
314
+ if index:
315
+ self.messages.at[index[0], col] = value
316
+ return True
317
+ return False
318
+
319
+ def info(self, use_sender: bool = False) -> Dict[str, int]:
320
+ """
321
+ Provides a summary of the conversation messages.
322
+
323
+ Args:
324
+ use_sender (bool, optional): Determines whether to summarize by sender or by role. Defaults to False.
325
+
326
+ Returns:
327
+ Dict[str, int]: A dictionary containing counts of messages either by role or sender.
328
+ """
329
+ messages = self.messages['sender'] if use_sender else self.messages['role']
330
+ result = messages.value_counts().to_dict()
331
+ result['total'] = len(self.messages)
332
+ return result
333
+
334
+ @property
335
+ def describe(self) -> Dict[str, Any]:
336
+ """
337
+ Describes the conversation with various statistics and information.
338
+
339
+ Returns:
340
+ Dict[str, Any]: A dictionary containing information such as total number of messages, summary by role,
341
+ and individual messages.
342
+ """
343
+ return {
344
+ "total_messages": len(self.messages),
345
+ "summary_by_role": self.info(),
346
+ "messages": [
347
+ msg.to_dict() for _, msg in self.messages.iterrows()
348
+ ],
349
+ }
350
+
351
+ def history(
352
+ self, begin_: Optional[datetime] = None, end_: Optional[datetime] = None
353
+ ) -> pd.DataFrame:
354
+ """
355
+ Retrieves a history of messages within a specified date range.
356
+
357
+ Args:
358
+ begin_ (Optional[datetime], optional): The start date of the message history. Defaults to None.
359
+ end_ (Optional[datetime], optional): The end date of the message history. Defaults to None.
360
+
361
+ Returns:
362
+ pd.DataFrame: A DataFrame containing messages within the specified date range.
363
+ """
364
+
365
+ if isinstance(begin_, str):
366
+ begin_ = datetime.strptime(begin_, '%Y-%m-%d')
367
+ if isinstance(end_, str):
368
+ end_ = datetime.strptime(end_, '%Y-%m-%d')
369
+ if begin_ and end_:
370
+ return self.messages[
371
+ (self.messages["timestamp"].dt.date >= begin_.date())
372
+ & (self.messages["timestamp"].dt.date <= end_.date())
373
+ ]
374
+ elif begin_:
375
+ return self.messages[(self.messages["timestamp"].dt.date >= begin_.date())]
376
+ elif end_:
377
+ return self.messages[(self.messages["timestamp"].dt.date <= end_.date())]
378
+ return self.messages
379
+
380
+ def clone(self) -> 'Conversation':
381
+ """
382
+ Creates a clone of the current conversation.
383
+
384
+ Returns:
385
+ Conversation: A new Conversation object that is a clone of the current conversation.
386
+ """
387
+ cloned = Conversation()
388
+ cloned.logger.set_dir(self.logger.dir)
389
+ cloned.messages = self.messages.copy()
390
+ return cloned
391
+
392
+ # def merge_conversation(self, other: 'Conversation', update: bool = False,) -> None:
393
+ # """
394
+ # Merges another conversation into the current one.
395
+ #
396
+ # Args:
397
+ # other (Conversation): The other conversation to merge with the current one.
398
+ # update (bool, optional): If True, updates the first system message before merging. Defaults to False.
399
+ # """
400
+ # if update:
401
+ # self.first_system = other.first_system.copy()
402
+ # df = pd.concat([self.messages.copy(), other.messages.copy()], ignore_index=True)
403
+ # self.messages = df.drop_duplicates().reset_index(drop=True, inplace=True)
404
+
405
+ def rollback(self, steps: int) -> None:
406
+ """
407
+ Rollbacks the conversation by a specified number of steps (messages).
408
+
409
+ Args:
410
+ steps (int): The number of steps to rollback.
411
+
412
+ Raises:
413
+ ValueError: If steps are not a non-negative integer or greater than the number of messages.
414
+ """
415
+ if steps < 0 or steps > len(self.messages):
416
+ raise ValueError("Steps must be a non-negative integer less than or equal to the number of messages.")
417
+ self.messages = self.messages[:-steps].reset_index(drop=True)
418
+
419
+ def reset(self) -> None:
420
+ """
421
+ Resets the conversation, clearing all messages.
422
+ """
423
+ self.messages = pd.DataFrame(columns=self.messages.columns)
424
+
425
+ def to_csv(self, filepath: str, **kwargs) -> None:
426
+ """
427
+ Exports the conversation messages to a CSV file.
428
+
429
+ Args:
430
+ filepath (str): The file path where the CSV will be saved.
431
+ **kwargs: Additional keyword arguments for `pandas.DataFrame.to_csv` method.
432
+ """
433
+ self.messages.to_csv(filepath, **kwargs)
434
+
435
+ def from_csv(self, filepath: str, **kwargs) -> None:
436
+ """
437
+ Imports conversation messages from a CSV file.
438
+
439
+ Args:
440
+ filepath (str): The file path of the CSV to be read.
441
+ **kwargs: Additional keyword arguments for `pandas.read_csv` method.
442
+ """
443
+ self.messages = pd.read_csv(filepath, **kwargs)
444
+
445
+ def to_json(self, filepath: str) -> None:
446
+ """
447
+ Exports the conversation messages to a JSON file.
448
+
449
+ Args:
450
+ filepath (str): The file path where the JSON will be saved.
451
+ """
452
+ self.messages.to_json(
453
+ filepath, orient="records", lines=True, date_format="iso")
454
+
455
+ def from_json(self, filepath: str) -> None:
456
+ """
457
+ Imports conversation messages from a JSON file.
458
+
459
+ Args:
460
+ filepath (str): The file path of the JSON to be read.
461
+ """
462
+ self.reset()
463
+ self.messages = pd.read_json(filepath, orient="records", lines=True)
464
+
465
+ def extend(self, messages: pd.DataFrame, **kwargs) -> None:
466
+ """
467
+ Extends the current conversation messages with additional messages from a DataFrame.
468
+
469
+ Args:
470
+ messages (pd.DataFrame): The DataFrame containing messages to be added to the conversation.
471
+ kwargs: for pd.df.drop_duplicates
472
+ """
473
+
474
+ validate_messages(messages)
475
+ try:
476
+ if len(messages.dropna(how='all')) > 0 and len(self.messages.dropna(how='all')) > 0:
477
+ self.messages = pd.concat([self.messages, messages], ignore_index=True)
478
+ self.messages.drop_duplicates(
479
+ inplace=True, subset=['node_id'], keep='first', **kwargs
480
+ )
481
+ self.messages.reset_index(drop=True, inplace=True)
482
+ return
483
+ except Exception as e:
484
+ raise ValueError(f"Error in extending messages: {e}")
@@ -0,0 +1,59 @@
1
+ import json
2
+ from ..utils.sys_util import strip_lower
3
+
4
+
5
+ def sign_message(messages, sender: str):
6
+ """
7
+ Sign messages with a sender identifier.
8
+
9
+ Args:
10
+ messages (pd.DataFrame): A DataFrame containing messages with columns 'node_id', 'role', 'sender', 'timestamp', and 'content'.
11
+ sender (str): The sender identifier to be added to the messages.
12
+
13
+ Returns:
14
+ pd.DataFrame: A new DataFrame with the sender identifier added to each message.
15
+
16
+ Raises:
17
+ ValueError: If the 'sender' is None or 'None'.
18
+ """
19
+ if sender is None or strip_lower(sender) == 'none':
20
+ raise ValueError("sender cannot be None")
21
+ df = messages.copy()
22
+
23
+ for i in df.index:
24
+ if not df.loc[i, 'content'].startswith('Sender'):
25
+ df.loc[i, 'content'] = f"Sender {sender}: {df.loc[i, 'content']}"
26
+ else:
27
+ content = df.loc[i, 'content'].split(':', 1)[1]
28
+ df.loc[i, 'content'] = f"Sender {sender}: {content}"
29
+ return df
30
+
31
+
32
+ def validate_messages(messages):
33
+ """
34
+ Validate the structure and content of a messages DataFrame.
35
+
36
+ Args:
37
+ messages (pd.DataFrame): A DataFrame containing messages with columns 'node_id', 'role', 'sender', 'timestamp', and 'content'.
38
+
39
+ Returns:
40
+ bool: True if the messages DataFrame is valid; otherwise, raises a ValueError.
41
+
42
+ Raises:
43
+ ValueError: If the DataFrame structure is invalid or if it contains null values, roles other than ["system", "user", "assistant"],
44
+ or content that cannot be parsed as JSON strings.
45
+ """
46
+ if list(messages.columns) != ['node_id', 'role', 'sender', 'timestamp', 'content']:
47
+ raise ValueError('Invalid messages dataframe. Unmatched columns.')
48
+ if messages.isnull().values.any():
49
+ raise ValueError('Invalid messages dataframe. Cannot have null.')
50
+ if not all(role in ['system', 'user', 'assistant'] for role in messages['role'].unique()):
51
+ raise ValueError('Invalid messages dataframe. Cannot have role other than ["system", "user", "assistant"].')
52
+ for cont in messages['content']:
53
+ if cont.startswith('Sender'):
54
+ cont = cont.split(':', 1)[1]
55
+ try:
56
+ json.loads(cont)
57
+ except:
58
+ raise ValueError('Invalid messages dataframe. Content expect json string.')
59
+ return True
File without changes