lionagi 0.0.115__py3-none-any.whl → 0.0.204__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- lionagi/__init__.py +1 -2
- lionagi/_services/__init__.py +5 -0
- lionagi/_services/anthropic.py +79 -0
- lionagi/_services/base_service.py +414 -0
- lionagi/_services/oai.py +98 -0
- lionagi/_services/openrouter.py +44 -0
- lionagi/_services/services.py +91 -0
- lionagi/_services/transformers.py +46 -0
- lionagi/bridge/langchain.py +26 -16
- lionagi/bridge/llama_index.py +50 -20
- lionagi/configs/oai_configs.py +2 -14
- lionagi/configs/openrouter_configs.py +2 -2
- lionagi/core/__init__.py +7 -8
- lionagi/core/branch/branch.py +589 -0
- lionagi/core/branch/branch_manager.py +139 -0
- lionagi/core/branch/conversation.py +484 -0
- lionagi/core/core_util.py +59 -0
- lionagi/core/flow/flow.py +19 -0
- lionagi/core/flow/flow_util.py +62 -0
- lionagi/core/instruction_set/__init__.py +0 -5
- lionagi/core/instruction_set/instruction_set.py +343 -0
- lionagi/core/messages/messages.py +176 -0
- lionagi/core/sessions/__init__.py +0 -5
- lionagi/core/sessions/session.py +428 -0
- lionagi/loaders/chunker.py +51 -47
- lionagi/loaders/load_util.py +2 -2
- lionagi/loaders/reader.py +45 -39
- lionagi/models/imodel.py +53 -0
- lionagi/schema/async_queue.py +158 -0
- lionagi/schema/base_node.py +318 -147
- lionagi/schema/base_tool.py +31 -1
- lionagi/schema/data_logger.py +74 -38
- lionagi/schema/data_node.py +57 -6
- lionagi/structures/graph.py +132 -10
- lionagi/structures/relationship.py +58 -20
- lionagi/structures/structure.py +36 -25
- lionagi/tests/test_utils/test_api_util.py +219 -0
- lionagi/tests/test_utils/test_call_util.py +785 -0
- lionagi/tests/test_utils/test_encrypt_util.py +323 -0
- lionagi/tests/test_utils/test_io_util.py +238 -0
- lionagi/tests/test_utils/test_nested_util.py +338 -0
- lionagi/tests/test_utils/test_sys_util.py +358 -0
- lionagi/tools/tool_manager.py +186 -0
- lionagi/tools/tool_util.py +266 -3
- lionagi/utils/__init__.py +21 -61
- lionagi/utils/api_util.py +359 -71
- lionagi/utils/call_util.py +839 -264
- lionagi/utils/encrypt_util.py +283 -16
- lionagi/utils/io_util.py +178 -93
- lionagi/utils/nested_util.py +672 -0
- lionagi/utils/pd_util.py +57 -0
- lionagi/utils/sys_util.py +284 -156
- lionagi/utils/url_util.py +55 -0
- lionagi/version.py +1 -1
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/METADATA +21 -17
- lionagi-0.0.204.dist-info/RECORD +106 -0
- lionagi/core/conversations/__init__.py +0 -5
- lionagi/core/conversations/conversation.py +0 -107
- lionagi/core/flows/__init__.py +0 -8
- lionagi/core/flows/flow.py +0 -8
- lionagi/core/flows/flow_util.py +0 -62
- lionagi/core/instruction_set/instruction_sets.py +0 -7
- lionagi/core/sessions/sessions.py +0 -185
- lionagi/endpoints/__init__.py +0 -5
- lionagi/endpoints/audio.py +0 -17
- lionagi/endpoints/chatcompletion.py +0 -54
- lionagi/messages/__init__.py +0 -11
- lionagi/messages/instruction.py +0 -15
- lionagi/messages/message.py +0 -110
- lionagi/messages/response.py +0 -33
- lionagi/messages/system.py +0 -12
- lionagi/objs/__init__.py +0 -11
- lionagi/objs/abc_objs.py +0 -39
- lionagi/objs/async_queue.py +0 -135
- lionagi/objs/messenger.py +0 -85
- lionagi/objs/tool_manager.py +0 -253
- lionagi/services/__init__.py +0 -11
- lionagi/services/base_api_service.py +0 -230
- lionagi/services/oai.py +0 -34
- lionagi/services/openrouter.py +0 -31
- lionagi/tests/test_api_util.py +0 -46
- lionagi/tests/test_call_util.py +0 -115
- lionagi/tests/test_convert_util.py +0 -202
- lionagi/tests/test_encrypt_util.py +0 -33
- lionagi/tests/test_flat_util.py +0 -426
- lionagi/tests/test_sys_util.py +0 -0
- lionagi/utils/convert_util.py +0 -229
- lionagi/utils/flat_util.py +0 -599
- lionagi-0.0.115.dist-info/RECORD +0 -110
- /lionagi/{services → _services}/anyscale.py +0 -0
- /lionagi/{services → _services}/azure.py +0 -0
- /lionagi/{services → _services}/bedrock.py +0 -0
- /lionagi/{services → _services}/everlyai.py +0 -0
- /lionagi/{services → _services}/gemini.py +0 -0
- /lionagi/{services → _services}/gpt4all.py +0 -0
- /lionagi/{services → _services}/huggingface.py +0 -0
- /lionagi/{services → _services}/litellm.py +0 -0
- /lionagi/{services → _services}/localai.py +0 -0
- /lionagi/{services → _services}/mistralai.py +0 -0
- /lionagi/{services → _services}/ollama.py +0 -0
- /lionagi/{services → _services}/openllm.py +0 -0
- /lionagi/{services → _services}/perplexity.py +0 -0
- /lionagi/{services → _services}/predibase.py +0 -0
- /lionagi/{services → _services}/rungpt.py +0 -0
- /lionagi/{services → _services}/vllm.py +0 -0
- /lionagi/{services → _services}/xinference.py +0 -0
- /lionagi/{endpoints/assistants.py → agents/__init__.py} +0 -0
- /lionagi/{tools → agents}/planner.py +0 -0
- /lionagi/{tools → agents}/prompter.py +0 -0
- /lionagi/{tools → agents}/scorer.py +0 -0
- /lionagi/{tools → agents}/summarizer.py +0 -0
- /lionagi/{tools → agents}/validator.py +0 -0
- /lionagi/{endpoints/embeddings.py → core/branch/__init__.py} +0 -0
- /lionagi/{services/anthropic.py → core/branch/cluster.py} +0 -0
- /lionagi/{endpoints/finetune.py → core/flow/__init__.py} +0 -0
- /lionagi/{endpoints/image.py → core/messages/__init__.py} +0 -0
- /lionagi/{endpoints/moderation.py → models/__init__.py} +0 -0
- /lionagi/{endpoints/vision.py → models/base_model.py} +0 -0
- /lionagi/{objs → schema}/status_tracker.py +0 -0
- /lionagi/tests/{test_io_util.py → test_utils/__init__.py} +0 -0
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/LICENSE +0 -0
- {lionagi-0.0.115.dist-info → lionagi-0.0.204.dist-info}/WHEEL +0 -0
- {lionagi-0.0.115.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,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
|