PraisonAI 0.0.45__py3-none-any.whl → 0.0.47__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.
Potentially problematic release.
This version of PraisonAI might be problematic. Click here for more details.
- praisonai/cli.py +15 -0
- praisonai/deploy.py +3 -3
- praisonai/ui/chat.py +79 -214
- praisonai/ui/sql_alchemy.py +638 -0
- {praisonai-0.0.45.dist-info → praisonai-0.0.47.dist-info}/METADATA +77 -57
- {praisonai-0.0.45.dist-info → praisonai-0.0.47.dist-info}/RECORD +9 -8
- {praisonai-0.0.45.dist-info → praisonai-0.0.47.dist-info}/LICENSE +0 -0
- {praisonai-0.0.45.dist-info → praisonai-0.0.47.dist-info}/WHEEL +0 -0
- {praisonai-0.0.45.dist-info → praisonai-0.0.47.dist-info}/entry_points.txt +0 -0
praisonai/cli.py
CHANGED
|
@@ -60,6 +60,12 @@ class PraisonAI:
|
|
|
60
60
|
self.auto = auto
|
|
61
61
|
self.init = init
|
|
62
62
|
|
|
63
|
+
def run(self):
|
|
64
|
+
"""
|
|
65
|
+
Run the PraisonAI application.
|
|
66
|
+
"""
|
|
67
|
+
self.main()
|
|
68
|
+
|
|
63
69
|
def main(self):
|
|
64
70
|
"""
|
|
65
71
|
The main function of the PraisonAI object. It parses the command-line arguments,
|
|
@@ -188,6 +194,15 @@ class PraisonAI:
|
|
|
188
194
|
if CHAINLIT_AVAILABLE:
|
|
189
195
|
import praisonai
|
|
190
196
|
os.environ["CHAINLIT_PORT"] = "8084"
|
|
197
|
+
public_folder = os.path.join(os.path.dirname(praisonai.__file__), 'public')
|
|
198
|
+
if not os.path.exists("public"): # Check if the folder exists in the current directory
|
|
199
|
+
if os.path.exists(public_folder):
|
|
200
|
+
shutil.copytree(public_folder, 'public', dirs_exist_ok=True)
|
|
201
|
+
logging.info("Public folder copied successfully!")
|
|
202
|
+
else:
|
|
203
|
+
logging.info("Public folder not found in the package.")
|
|
204
|
+
else:
|
|
205
|
+
logging.info("Public folder already exists.")
|
|
191
206
|
chat_ui_path = os.path.join(os.path.dirname(praisonai.__file__), 'ui', 'chat.py')
|
|
192
207
|
chainlit_run([chat_ui_path])
|
|
193
208
|
else:
|
praisonai/deploy.py
CHANGED
|
@@ -56,7 +56,7 @@ class CloudDeployer:
|
|
|
56
56
|
file.write("FROM python:3.11-slim\n")
|
|
57
57
|
file.write("WORKDIR /app\n")
|
|
58
58
|
file.write("COPY . .\n")
|
|
59
|
-
file.write("RUN pip install flask praisonai==0.0.
|
|
59
|
+
file.write("RUN pip install flask praisonai==0.0.47 gunicorn markdown\n")
|
|
60
60
|
file.write("EXPOSE 8080\n")
|
|
61
61
|
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
|
62
62
|
|
|
@@ -78,8 +78,8 @@ class CloudDeployer:
|
|
|
78
78
|
file.write("import markdown\n\n")
|
|
79
79
|
file.write("app = Flask(__name__)\n\n")
|
|
80
80
|
file.write("def basic():\n")
|
|
81
|
-
file.write("
|
|
82
|
-
file.write(" return
|
|
81
|
+
file.write(" praisonai = PraisonAI(agent_file=\"agents.yaml\")\n")
|
|
82
|
+
file.write(" return praisonai.run()\n\n")
|
|
83
83
|
file.write("@app.route('/')\n")
|
|
84
84
|
file.write("def home():\n")
|
|
85
85
|
file.write(" output = basic()\n")
|
praisonai/ui/chat.py
CHANGED
|
@@ -12,6 +12,8 @@ import chainlit.data as cl_data
|
|
|
12
12
|
from chainlit.step import StepDict
|
|
13
13
|
from literalai.helper import utc_now
|
|
14
14
|
import logging
|
|
15
|
+
import json
|
|
16
|
+
from sql_alchemy import SQLAlchemyDataLayer
|
|
15
17
|
|
|
16
18
|
# Set up logging
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
@@ -38,31 +40,81 @@ now = utc_now()
|
|
|
38
40
|
|
|
39
41
|
create_step_counter = 0
|
|
40
42
|
|
|
41
|
-
import json
|
|
42
|
-
|
|
43
43
|
DB_PATH = "threads.db"
|
|
44
44
|
|
|
45
45
|
def initialize_db():
|
|
46
46
|
conn = sqlite3.connect(DB_PATH)
|
|
47
47
|
cursor = conn.cursor()
|
|
48
|
+
cursor.execute('''
|
|
49
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
50
|
+
id UUID PRIMARY KEY,
|
|
51
|
+
identifier TEXT NOT NULL UNIQUE,
|
|
52
|
+
metadata JSONB NOT NULL,
|
|
53
|
+
createdAt TEXT
|
|
54
|
+
)
|
|
55
|
+
''')
|
|
48
56
|
cursor.execute('''
|
|
49
57
|
CREATE TABLE IF NOT EXISTS threads (
|
|
50
|
-
id
|
|
51
|
-
name TEXT,
|
|
58
|
+
id UUID PRIMARY KEY,
|
|
52
59
|
createdAt TEXT,
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
name TEXT,
|
|
61
|
+
userId UUID,
|
|
62
|
+
userIdentifier TEXT,
|
|
63
|
+
tags TEXT[],
|
|
64
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
65
|
+
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
|
|
55
66
|
)
|
|
56
67
|
''')
|
|
57
68
|
cursor.execute('''
|
|
58
69
|
CREATE TABLE IF NOT EXISTS steps (
|
|
59
|
-
id
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
id UUID PRIMARY KEY,
|
|
71
|
+
name TEXT NOT NULL,
|
|
72
|
+
type TEXT NOT NULL,
|
|
73
|
+
threadId UUID NOT NULL,
|
|
74
|
+
parentId UUID,
|
|
75
|
+
disableFeedback BOOLEAN NOT NULL,
|
|
76
|
+
streaming BOOLEAN NOT NULL,
|
|
77
|
+
waitForAnswer BOOLEAN,
|
|
78
|
+
isError BOOLEAN,
|
|
79
|
+
metadata JSONB,
|
|
80
|
+
tags TEXT[],
|
|
81
|
+
input TEXT,
|
|
82
|
+
output TEXT,
|
|
62
83
|
createdAt TEXT,
|
|
84
|
+
start TEXT,
|
|
85
|
+
end TEXT,
|
|
86
|
+
generation JSONB,
|
|
87
|
+
showInput TEXT,
|
|
88
|
+
language TEXT,
|
|
89
|
+
indent INT,
|
|
90
|
+
FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE
|
|
91
|
+
)
|
|
92
|
+
''')
|
|
93
|
+
cursor.execute('''
|
|
94
|
+
CREATE TABLE IF NOT EXISTS elements (
|
|
95
|
+
id UUID PRIMARY KEY,
|
|
96
|
+
threadId UUID,
|
|
63
97
|
type TEXT,
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
url TEXT,
|
|
99
|
+
chainlitKey TEXT,
|
|
100
|
+
name TEXT NOT NULL,
|
|
101
|
+
display TEXT,
|
|
102
|
+
objectKey TEXT,
|
|
103
|
+
size TEXT,
|
|
104
|
+
page INT,
|
|
105
|
+
language TEXT,
|
|
106
|
+
forId UUID,
|
|
107
|
+
mime TEXT,
|
|
108
|
+
FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE
|
|
109
|
+
)
|
|
110
|
+
''')
|
|
111
|
+
cursor.execute('''
|
|
112
|
+
CREATE TABLE IF NOT EXISTS feedbacks (
|
|
113
|
+
id UUID PRIMARY KEY,
|
|
114
|
+
forId UUID NOT NULL,
|
|
115
|
+
value INT NOT NULL,
|
|
116
|
+
threadId UUID,
|
|
117
|
+
comment TEXT
|
|
66
118
|
)
|
|
67
119
|
''')
|
|
68
120
|
cursor.execute('''
|
|
@@ -110,210 +162,13 @@ def load_setting(key: str) -> str:
|
|
|
110
162
|
conn.close()
|
|
111
163
|
return result[0] if result else None
|
|
112
164
|
|
|
113
|
-
def save_thread_to_db(thread):
|
|
114
|
-
conn = sqlite3.connect(DB_PATH)
|
|
115
|
-
cursor = conn.cursor()
|
|
116
|
-
cursor.execute('''
|
|
117
|
-
INSERT OR REPLACE INTO threads (id, name, createdAt, userId, userIdentifier)
|
|
118
|
-
VALUES (?, ?, ?, ?, ?)
|
|
119
|
-
''', (thread['id'], thread['name'], thread['createdAt'], thread['userId'], thread['userIdentifier']))
|
|
120
|
-
|
|
121
|
-
# No steps to save as steps are empty in the provided thread data
|
|
122
|
-
conn.commit()
|
|
123
|
-
conn.close()
|
|
124
|
-
logger.debug("Thread saved to DB")
|
|
125
|
-
|
|
126
|
-
def update_thread_in_db(thread):
|
|
127
|
-
conn = sqlite3.connect(DB_PATH)
|
|
128
|
-
cursor = conn.cursor()
|
|
129
|
-
|
|
130
|
-
# Insert or update the thread
|
|
131
|
-
cursor.execute('''
|
|
132
|
-
INSERT OR REPLACE INTO threads (id, name, createdAt, userId, userIdentifier)
|
|
133
|
-
VALUES (?, ?, ?, ?, ?)
|
|
134
|
-
''', (thread['id'], thread['name'], thread['createdAt'], thread['userId'], thread['userIdentifier']))
|
|
135
|
-
|
|
136
|
-
# Fetch message_history from metadata
|
|
137
|
-
message_history = cl.user_session.get("message_history", [])
|
|
138
|
-
|
|
139
|
-
# Ensure user messages come first followed by assistant messages
|
|
140
|
-
user_messages = [msg for msg in message_history if msg['role'] == 'user']
|
|
141
|
-
assistant_messages = [msg for msg in message_history if msg['role'] == 'assistant']
|
|
142
|
-
ordered_steps = [val for pair in zip(user_messages, assistant_messages) for val in pair]
|
|
143
|
-
|
|
144
|
-
# Generate steps from ordered message_history
|
|
145
|
-
steps = []
|
|
146
|
-
for idx, message in enumerate(ordered_steps):
|
|
147
|
-
step_id = f"{thread['id']}-step-{idx}"
|
|
148
|
-
step_type = 'user_message' if message['role'] == 'user' else 'assistant_message'
|
|
149
|
-
step_name = 'user' if message['role'] == 'user' else 'assistant'
|
|
150
|
-
created_at = message.get('createdAt', thread['createdAt']) # Use thread's createdAt if no timestamp in message
|
|
151
|
-
steps.append({
|
|
152
|
-
'id': step_id,
|
|
153
|
-
'threadId': thread['id'],
|
|
154
|
-
'name': step_name,
|
|
155
|
-
'createdAt': created_at,
|
|
156
|
-
'type': step_type,
|
|
157
|
-
'output': message['content']
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
# Insert all steps into the database
|
|
161
|
-
for step in steps:
|
|
162
|
-
cursor.execute('''
|
|
163
|
-
INSERT OR REPLACE INTO steps (id, threadId, name, createdAt, type, output)
|
|
164
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
165
|
-
''', (step['id'], step['threadId'], step['name'], step['createdAt'], step['type'], step['output']))
|
|
166
|
-
|
|
167
|
-
conn.commit()
|
|
168
|
-
conn.close()
|
|
169
|
-
logger.debug("Thread updated in DB")
|
|
170
|
-
|
|
171
|
-
def delete_thread_from_db(thread_id: str):
|
|
172
|
-
"""Deletes a thread and its steps from the database.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
thread_id: The ID of the thread to delete.
|
|
176
|
-
"""
|
|
177
|
-
conn = sqlite3.connect(DB_PATH)
|
|
178
|
-
cursor = conn.cursor()
|
|
179
|
-
cursor.execute('DELETE FROM threads WHERE id = ?', (thread_id,))
|
|
180
|
-
cursor.execute('DELETE FROM steps WHERE threadId = ?', (thread_id,))
|
|
181
|
-
conn.commit()
|
|
182
|
-
conn.close()
|
|
183
|
-
|
|
184
|
-
def load_threads_from_db():
|
|
185
|
-
conn = sqlite3.connect(DB_PATH)
|
|
186
|
-
cursor = conn.cursor()
|
|
187
|
-
cursor.execute('SELECT * FROM threads ORDER BY createdAt ASC')
|
|
188
|
-
thread_rows = cursor.fetchall()
|
|
189
|
-
threads = []
|
|
190
|
-
for thread_row in thread_rows:
|
|
191
|
-
cursor.execute('SELECT * FROM steps WHERE threadId = ? ORDER BY createdAt ASC', (thread_row[0],))
|
|
192
|
-
step_rows = cursor.fetchall()
|
|
193
|
-
steps = []
|
|
194
|
-
for step_row in step_rows:
|
|
195
|
-
steps.append({
|
|
196
|
-
"id": step_row[0],
|
|
197
|
-
"threadId": step_row[1],
|
|
198
|
-
"name": step_row[2],
|
|
199
|
-
"createdAt": step_row[3],
|
|
200
|
-
"type": step_row[4],
|
|
201
|
-
"output": step_row[5]
|
|
202
|
-
})
|
|
203
|
-
threads.append({
|
|
204
|
-
"id": thread_row[0],
|
|
205
|
-
"name": thread_row[1],
|
|
206
|
-
"createdAt": thread_row[2],
|
|
207
|
-
"userId": thread_row[3],
|
|
208
|
-
"userIdentifier": thread_row[4],
|
|
209
|
-
"steps": steps
|
|
210
|
-
})
|
|
211
|
-
conn.close()
|
|
212
|
-
logger.debug("Threads loaded from DB")
|
|
213
|
-
return threads
|
|
214
165
|
|
|
215
166
|
# Initialize the database
|
|
216
167
|
initialize_db()
|
|
217
|
-
thread_history = load_threads_from_db()
|
|
218
168
|
|
|
219
169
|
deleted_thread_ids = [] # type: List[str]
|
|
220
170
|
|
|
221
|
-
|
|
222
|
-
async def get_user(self, identifier: str):
|
|
223
|
-
logger.debug(f"Getting user: {identifier}")
|
|
224
|
-
return cl.PersistedUser(id="test", createdAt=now, identifier=identifier)
|
|
225
|
-
|
|
226
|
-
async def create_user(self, user: cl.User):
|
|
227
|
-
logger.debug(f"Creating user: {user.identifier}")
|
|
228
|
-
return cl.PersistedUser(id="test", createdAt=now, identifier=user.identifier)
|
|
229
|
-
|
|
230
|
-
async def update_thread(
|
|
231
|
-
self,
|
|
232
|
-
thread_id: str,
|
|
233
|
-
name: Optional[str] = None,
|
|
234
|
-
user_id: Optional[str] = None,
|
|
235
|
-
metadata: Optional[Dict] = None,
|
|
236
|
-
tags: Optional[List[str]] = None,
|
|
237
|
-
):
|
|
238
|
-
logger.debug(f"Updating thread: {thread_id}")
|
|
239
|
-
thread = next((t for t in thread_history if t["id"] == thread_id), None)
|
|
240
|
-
if thread:
|
|
241
|
-
if name:
|
|
242
|
-
thread["name"] = name
|
|
243
|
-
if metadata:
|
|
244
|
-
thread["metadata"] = metadata
|
|
245
|
-
if tags:
|
|
246
|
-
thread["tags"] = tags
|
|
247
|
-
|
|
248
|
-
logger.debug(f"Thread: {thread}")
|
|
249
|
-
cl.user_session.set("message_history", thread['metadata']['message_history'])
|
|
250
|
-
cl.user_session.set("thread_id", thread["id"])
|
|
251
|
-
update_thread_in_db(thread)
|
|
252
|
-
logger.debug(f"Thread updated: {thread_id}")
|
|
253
|
-
|
|
254
|
-
else:
|
|
255
|
-
thread_history.append(
|
|
256
|
-
{
|
|
257
|
-
"id": thread_id,
|
|
258
|
-
"name": name,
|
|
259
|
-
"metadata": metadata,
|
|
260
|
-
"tags": tags,
|
|
261
|
-
"createdAt": utc_now(),
|
|
262
|
-
"userId": user_id,
|
|
263
|
-
"userIdentifier": "admin",
|
|
264
|
-
"steps": [],
|
|
265
|
-
}
|
|
266
|
-
)
|
|
267
|
-
thread = {
|
|
268
|
-
"id": thread_id,
|
|
269
|
-
"name": name,
|
|
270
|
-
"metadata": metadata,
|
|
271
|
-
"tags": tags,
|
|
272
|
-
"createdAt": utc_now(),
|
|
273
|
-
"userId": user_id,
|
|
274
|
-
"userIdentifier": "admin",
|
|
275
|
-
"steps": [],
|
|
276
|
-
}
|
|
277
|
-
save_thread_to_db(thread)
|
|
278
|
-
logger.debug(f"Thread created: {thread_id}")
|
|
279
|
-
|
|
280
|
-
@cl_data.queue_until_user_message()
|
|
281
|
-
async def create_step(self, step_dict: StepDict):
|
|
282
|
-
global create_step_counter
|
|
283
|
-
create_step_counter += 1
|
|
284
|
-
|
|
285
|
-
thread = next(
|
|
286
|
-
(t for t in thread_history if t["id"] == step_dict.get("threadId")), None
|
|
287
|
-
)
|
|
288
|
-
if thread:
|
|
289
|
-
thread["steps"].append(step_dict)
|
|
290
|
-
|
|
291
|
-
async def get_thread_author(self, thread_id: str):
|
|
292
|
-
logger.debug(f"Getting thread author: {thread_id}")
|
|
293
|
-
return "admin"
|
|
294
|
-
|
|
295
|
-
async def list_threads(
|
|
296
|
-
self, pagination: cl_data.Pagination, filters: cl_data.ThreadFilter
|
|
297
|
-
) -> cl_data.PaginatedResponse[cl_data.ThreadDict]:
|
|
298
|
-
logger.debug(f"Listing threads")
|
|
299
|
-
return cl_data.PaginatedResponse(
|
|
300
|
-
data=[t for t in thread_history if t["id"] not in deleted_thread_ids][::-1],
|
|
301
|
-
pageInfo=cl_data.PageInfo(
|
|
302
|
-
hasNextPage=False, startCursor=None, endCursor=None
|
|
303
|
-
),
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
async def get_thread(self, thread_id: str):
|
|
307
|
-
logger.debug(f"Getting thread: {thread_id}")
|
|
308
|
-
thread_history = load_threads_from_db()
|
|
309
|
-
return next((t for t in thread_history if t["id"] == thread_id), None)
|
|
310
|
-
|
|
311
|
-
async def delete_thread(self, thread_id: str):
|
|
312
|
-
deleted_thread_ids.append(thread_id)
|
|
313
|
-
delete_thread_from_db(thread_id)
|
|
314
|
-
logger.debug(f"Deleted thread: {thread_id}")
|
|
315
|
-
|
|
316
|
-
cl_data._data_layer = TestDataLayer()
|
|
171
|
+
cl_data._data_layer = SQLAlchemyDataLayer(conninfo=f"sqlite+aiosqlite:///{DB_PATH}")
|
|
317
172
|
|
|
318
173
|
@cl.on_chat_start
|
|
319
174
|
async def start():
|
|
@@ -357,7 +212,12 @@ async def setup_agent(settings):
|
|
|
357
212
|
if thread:
|
|
358
213
|
metadata = thread.get("metadata", {})
|
|
359
214
|
metadata["model_name"] = model_name
|
|
360
|
-
|
|
215
|
+
|
|
216
|
+
# Always store metadata as a JSON string
|
|
217
|
+
await cl_data.update_thread(thread_id, metadata=json.dumps(metadata))
|
|
218
|
+
|
|
219
|
+
# Update the user session with the new metadata
|
|
220
|
+
cl.user_session.set("metadata", metadata)
|
|
361
221
|
|
|
362
222
|
@cl.on_message
|
|
363
223
|
async def main(message: cl.Message):
|
|
@@ -372,9 +232,9 @@ async def main(message: cl.Message):
|
|
|
372
232
|
model=model_name,
|
|
373
233
|
messages=message_history,
|
|
374
234
|
stream=True,
|
|
375
|
-
temperature=0.7,
|
|
376
|
-
max_tokens=500,
|
|
377
|
-
top_p=1
|
|
235
|
+
# temperature=0.7,
|
|
236
|
+
# max_tokens=500,
|
|
237
|
+
# top_p=1
|
|
378
238
|
)
|
|
379
239
|
|
|
380
240
|
full_response = ""
|
|
@@ -423,6 +283,11 @@ async def on_chat_resume(thread: cl_data.ThreadDict):
|
|
|
423
283
|
await settings.send()
|
|
424
284
|
thread_id = thread["id"]
|
|
425
285
|
cl.user_session.set("thread_id", thread["id"])
|
|
286
|
+
|
|
287
|
+
# The metadata should now already be a dictionary
|
|
288
|
+
metadata = thread.get("metadata", {})
|
|
289
|
+
cl.user_session.set("metadata", metadata)
|
|
290
|
+
|
|
426
291
|
message_history = cl.user_session.get("message_history", [])
|
|
427
292
|
steps = thread["steps"]
|
|
428
293
|
|
|
@@ -435,4 +300,4 @@ async def on_chat_resume(thread: cl_data.ThreadDict):
|
|
|
435
300
|
else:
|
|
436
301
|
logger.warning(f"Message without type: {message}")
|
|
437
302
|
|
|
438
|
-
|
|
303
|
+
cl.user_session.set("message_history", message_history)
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import ssl
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import aiofiles
|
|
9
|
+
import aiohttp
|
|
10
|
+
from chainlit.context import context
|
|
11
|
+
from chainlit.data import BaseDataLayer, BaseStorageClient, queue_until_user_message
|
|
12
|
+
from chainlit.element import ElementDict
|
|
13
|
+
from chainlit.logger import logger
|
|
14
|
+
from chainlit.step import StepDict
|
|
15
|
+
from chainlit.types import (
|
|
16
|
+
Feedback,
|
|
17
|
+
FeedbackDict,
|
|
18
|
+
PageInfo,
|
|
19
|
+
PaginatedResponse,
|
|
20
|
+
Pagination,
|
|
21
|
+
ThreadDict,
|
|
22
|
+
ThreadFilter,
|
|
23
|
+
)
|
|
24
|
+
from chainlit.user import PersistedUser, User
|
|
25
|
+
from sqlalchemy import text
|
|
26
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
27
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
|
28
|
+
from sqlalchemy.orm import sessionmaker
|
|
29
|
+
import chainlit as cl
|
|
30
|
+
from literalai.helper import utc_now
|
|
31
|
+
now = utc_now()
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from chainlit.element import Element, ElementDict
|
|
35
|
+
from chainlit.step import StepDict
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SQLAlchemyDataLayer(BaseDataLayer):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
conninfo: str,
|
|
42
|
+
ssl_require: bool = False,
|
|
43
|
+
storage_provider: Optional[BaseStorageClient] = None,
|
|
44
|
+
user_thread_limit: Optional[int] = 1000,
|
|
45
|
+
show_logger: Optional[bool] = False,
|
|
46
|
+
):
|
|
47
|
+
self._conninfo = conninfo
|
|
48
|
+
self.user_thread_limit = user_thread_limit
|
|
49
|
+
self.show_logger = show_logger
|
|
50
|
+
ssl_args = {}
|
|
51
|
+
if ssl_require:
|
|
52
|
+
# Create an SSL context to require an SSL connection
|
|
53
|
+
ssl_context = ssl.create_default_context()
|
|
54
|
+
ssl_context.check_hostname = False
|
|
55
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
56
|
+
ssl_args["ssl"] = ssl_context
|
|
57
|
+
self.engine: AsyncEngine = create_async_engine(
|
|
58
|
+
self._conninfo, connect_args=ssl_args
|
|
59
|
+
)
|
|
60
|
+
self.async_session = sessionmaker(bind=self.engine, expire_on_commit=False, class_=AsyncSession) # type: ignore
|
|
61
|
+
if storage_provider:
|
|
62
|
+
self.storage_provider: Optional[BaseStorageClient] = storage_provider
|
|
63
|
+
if self.show_logger:
|
|
64
|
+
logger.info("SQLAlchemyDataLayer storage client initialized")
|
|
65
|
+
else:
|
|
66
|
+
self.storage_provider = None
|
|
67
|
+
logger.warn(
|
|
68
|
+
"SQLAlchemyDataLayer storage client is not initialized and elements will not be persisted!"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def build_debug_url(self) -> str:
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
###### SQL Helpers ######
|
|
75
|
+
async def execute_sql(
|
|
76
|
+
self, query: str, parameters: dict
|
|
77
|
+
) -> Union[List[Dict[str, Any]], int, None]:
|
|
78
|
+
parameterized_query = text(query)
|
|
79
|
+
async with self.async_session() as session:
|
|
80
|
+
try:
|
|
81
|
+
await session.begin()
|
|
82
|
+
result = await session.execute(parameterized_query, parameters)
|
|
83
|
+
await session.commit()
|
|
84
|
+
if result.returns_rows:
|
|
85
|
+
json_result = [dict(row._mapping) for row in result.fetchall()]
|
|
86
|
+
clean_json_result = self.clean_result(json_result)
|
|
87
|
+
return clean_json_result
|
|
88
|
+
else:
|
|
89
|
+
return result.rowcount
|
|
90
|
+
except SQLAlchemyError as e:
|
|
91
|
+
await session.rollback()
|
|
92
|
+
logger.warn(f"An error occurred: {e}")
|
|
93
|
+
return None
|
|
94
|
+
except Exception as e:
|
|
95
|
+
await session.rollback()
|
|
96
|
+
logger.warn(f"An unexpected error occurred: {e}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
async def get_current_timestamp(self) -> str:
|
|
100
|
+
return datetime.now().isoformat() + "Z"
|
|
101
|
+
|
|
102
|
+
def clean_result(self, obj):
|
|
103
|
+
"""Recursively change UUID -> str and serialize dictionaries"""
|
|
104
|
+
if isinstance(obj, dict):
|
|
105
|
+
return {k: self.clean_result(v) for k, v in obj.items()}
|
|
106
|
+
elif isinstance(obj, list):
|
|
107
|
+
return [self.clean_result(item) for item in obj]
|
|
108
|
+
elif isinstance(obj, uuid.UUID):
|
|
109
|
+
return str(obj)
|
|
110
|
+
return obj
|
|
111
|
+
|
|
112
|
+
###### User ######
|
|
113
|
+
async def get_user(self, identifier: str) -> Optional[PersistedUser]:
|
|
114
|
+
logger.debug(f"Getting user: {identifier}")
|
|
115
|
+
return cl.PersistedUser(id="test", createdAt=now, identifier=identifier)
|
|
116
|
+
if self.show_logger:
|
|
117
|
+
logger.info(f"SQLAlchemy: get_user, identifier={identifier}")
|
|
118
|
+
query = "SELECT * FROM users WHERE identifier = :identifier"
|
|
119
|
+
parameters = {"identifier": identifier}
|
|
120
|
+
result = await self.execute_sql(query=query, parameters=parameters)
|
|
121
|
+
if result and isinstance(result, list):
|
|
122
|
+
user_data = result[0]
|
|
123
|
+
return PersistedUser(**user_data)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def create_user(self, user: User) -> Optional[PersistedUser]:
|
|
127
|
+
logger.debug(f"Creating user: {user.identifier}")
|
|
128
|
+
return cl.PersistedUser(id="test", createdAt=now, identifier=user.identifier)
|
|
129
|
+
if self.show_logger:
|
|
130
|
+
logger.info(f"SQLAlchemy: create_user, user_identifier={user.identifier}")
|
|
131
|
+
existing_user: Optional["PersistedUser"] = await self.get_user(user.identifier)
|
|
132
|
+
user_dict: Dict[str, Any] = {
|
|
133
|
+
"identifier": str(user.identifier),
|
|
134
|
+
"metadata": json.dumps(user.metadata) or {},
|
|
135
|
+
}
|
|
136
|
+
if not existing_user: # create the user
|
|
137
|
+
if self.show_logger:
|
|
138
|
+
logger.info("SQLAlchemy: create_user, creating the user")
|
|
139
|
+
user_dict["id"] = str(uuid.uuid4())
|
|
140
|
+
user_dict["createdAt"] = await self.get_current_timestamp()
|
|
141
|
+
query = """INSERT INTO users ("id", "identifier", "createdAt", "metadata") VALUES (:id, :identifier, :createdAt, :metadata)"""
|
|
142
|
+
await self.execute_sql(query=query, parameters=user_dict)
|
|
143
|
+
else: # update the user
|
|
144
|
+
if self.show_logger:
|
|
145
|
+
logger.info("SQLAlchemy: update user metadata")
|
|
146
|
+
query = """UPDATE users SET "metadata" = :metadata WHERE "identifier" = :identifier"""
|
|
147
|
+
await self.execute_sql(
|
|
148
|
+
query=query, parameters=user_dict
|
|
149
|
+
) # We want to update the metadata
|
|
150
|
+
return await self.get_user(user.identifier)
|
|
151
|
+
|
|
152
|
+
###### Threads ######
|
|
153
|
+
async def get_thread_author(self, thread_id: str) -> str:
|
|
154
|
+
logger.debug(f"Getting thread author: {thread_id}")
|
|
155
|
+
return "admin"
|
|
156
|
+
if self.show_logger:
|
|
157
|
+
logger.info(f"SQLAlchemy: get_thread_author, thread_id={thread_id}")
|
|
158
|
+
query = """SELECT "userIdentifier" FROM threads WHERE "id" = :id"""
|
|
159
|
+
parameters = {"id": thread_id}
|
|
160
|
+
result = await self.execute_sql(query=query, parameters=parameters)
|
|
161
|
+
if isinstance(result, list) and result:
|
|
162
|
+
author_identifier = result[0].get("userIdentifier")
|
|
163
|
+
if author_identifier is not None:
|
|
164
|
+
return author_identifier
|
|
165
|
+
raise ValueError(f"Author not found for thread_id {thread_id}")
|
|
166
|
+
|
|
167
|
+
async def get_thread(self, thread_id: str) -> Optional[ThreadDict]:
|
|
168
|
+
if self.show_logger:
|
|
169
|
+
logger.info(f"SQLAlchemy: get_thread, thread_id={thread_id}")
|
|
170
|
+
user_threads: Optional[List[ThreadDict]] = await self.get_all_user_threads(
|
|
171
|
+
thread_id=thread_id
|
|
172
|
+
)
|
|
173
|
+
if user_threads:
|
|
174
|
+
thread = user_threads[0]
|
|
175
|
+
# Parse the metadata here
|
|
176
|
+
if isinstance(thread['metadata'], str):
|
|
177
|
+
try:
|
|
178
|
+
thread['metadata'] = json.loads(thread['metadata'])
|
|
179
|
+
except json.JSONDecodeError:
|
|
180
|
+
thread['metadata'] = {}
|
|
181
|
+
elif thread['metadata'] is None:
|
|
182
|
+
thread['metadata'] = {}
|
|
183
|
+
return thread
|
|
184
|
+
else:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
async def update_thread(
|
|
188
|
+
self,
|
|
189
|
+
thread_id: str,
|
|
190
|
+
name: Optional[str] = None,
|
|
191
|
+
user_id: Optional[str] = None,
|
|
192
|
+
metadata: Optional[Dict] = None,
|
|
193
|
+
tags: Optional[List[str]] = None,
|
|
194
|
+
):
|
|
195
|
+
if self.show_logger:
|
|
196
|
+
logger.info(f"SQLAlchemy: update_thread, thread_id={thread_id}")
|
|
197
|
+
if context.session.user is not None:
|
|
198
|
+
user_identifier = context.session.user.identifier
|
|
199
|
+
else:
|
|
200
|
+
raise ValueError("User not found in session context")
|
|
201
|
+
data = {
|
|
202
|
+
"id": thread_id,
|
|
203
|
+
"createdAt": (
|
|
204
|
+
await self.get_current_timestamp() if metadata is None else None
|
|
205
|
+
),
|
|
206
|
+
"name": (
|
|
207
|
+
name
|
|
208
|
+
if name is not None
|
|
209
|
+
else (metadata.get("name") if metadata and "name" in metadata else None)
|
|
210
|
+
),
|
|
211
|
+
"userId": user_id,
|
|
212
|
+
"userIdentifier": user_identifier,
|
|
213
|
+
"tags": tags,
|
|
214
|
+
"metadata": json.dumps(metadata) if metadata else None,
|
|
215
|
+
}
|
|
216
|
+
parameters = {
|
|
217
|
+
key: value for key, value in data.items() if value is not None
|
|
218
|
+
} # Remove keys with None values
|
|
219
|
+
columns = ", ".join(f'"{key}"' for key in parameters.keys())
|
|
220
|
+
values = ", ".join(f":{key}" for key in parameters.keys())
|
|
221
|
+
updates = ", ".join(
|
|
222
|
+
f'"{key}" = EXCLUDED."{key}"' for key in parameters.keys() if key != "id"
|
|
223
|
+
)
|
|
224
|
+
query = f"""
|
|
225
|
+
INSERT INTO threads ({columns})
|
|
226
|
+
VALUES ({values})
|
|
227
|
+
ON CONFLICT ("id") DO UPDATE
|
|
228
|
+
SET {updates};
|
|
229
|
+
"""
|
|
230
|
+
await self.execute_sql(query=query, parameters=parameters)
|
|
231
|
+
|
|
232
|
+
async def delete_thread(self, thread_id: str):
|
|
233
|
+
if self.show_logger:
|
|
234
|
+
logger.info(f"SQLAlchemy: delete_thread, thread_id={thread_id}")
|
|
235
|
+
# Delete feedbacks/elements/steps/thread
|
|
236
|
+
feedbacks_query = """DELETE FROM feedbacks WHERE "forId" IN (SELECT "id" FROM steps WHERE "threadId" = :id)"""
|
|
237
|
+
elements_query = """DELETE FROM elements WHERE "threadId" = :id"""
|
|
238
|
+
steps_query = """DELETE FROM steps WHERE "threadId" = :id"""
|
|
239
|
+
thread_query = """DELETE FROM threads WHERE "id" = :id"""
|
|
240
|
+
parameters = {"id": thread_id}
|
|
241
|
+
await self.execute_sql(query=feedbacks_query, parameters=parameters)
|
|
242
|
+
await self.execute_sql(query=elements_query, parameters=parameters)
|
|
243
|
+
await self.execute_sql(query=steps_query, parameters=parameters)
|
|
244
|
+
await self.execute_sql(query=thread_query, parameters=parameters)
|
|
245
|
+
|
|
246
|
+
async def list_threads(
|
|
247
|
+
self, pagination: Pagination, filters: ThreadFilter
|
|
248
|
+
) -> PaginatedResponse:
|
|
249
|
+
if self.show_logger:
|
|
250
|
+
logger.info(
|
|
251
|
+
f"SQLAlchemy: list_threads, pagination={pagination}, filters={filters}"
|
|
252
|
+
)
|
|
253
|
+
if not filters.userId:
|
|
254
|
+
raise ValueError("userId is required")
|
|
255
|
+
all_user_threads: List[ThreadDict] = (
|
|
256
|
+
await self.get_all_user_threads(user_id=filters.userId) or []
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
search_keyword = filters.search.lower() if filters.search else None
|
|
260
|
+
feedback_value = int(filters.feedback) if filters.feedback else None
|
|
261
|
+
|
|
262
|
+
filtered_threads = []
|
|
263
|
+
for thread in all_user_threads:
|
|
264
|
+
keyword_match = True
|
|
265
|
+
feedback_match = True
|
|
266
|
+
if search_keyword or feedback_value is not None:
|
|
267
|
+
if search_keyword:
|
|
268
|
+
keyword_match = any(
|
|
269
|
+
search_keyword in step["output"].lower()
|
|
270
|
+
for step in thread["steps"]
|
|
271
|
+
if "output" in step
|
|
272
|
+
)
|
|
273
|
+
if feedback_value is not None:
|
|
274
|
+
feedback_match = False # Assume no match until found
|
|
275
|
+
for step in thread["steps"]:
|
|
276
|
+
feedback = step.get("feedback")
|
|
277
|
+
if feedback and feedback.get("value") == feedback_value:
|
|
278
|
+
feedback_match = True
|
|
279
|
+
break
|
|
280
|
+
if keyword_match and feedback_match:
|
|
281
|
+
filtered_threads.append(thread)
|
|
282
|
+
|
|
283
|
+
start = 0
|
|
284
|
+
if pagination.cursor:
|
|
285
|
+
for i, thread in enumerate(filtered_threads):
|
|
286
|
+
if (
|
|
287
|
+
thread["id"] == pagination.cursor
|
|
288
|
+
): # Find the start index using pagination.cursor
|
|
289
|
+
start = i + 1
|
|
290
|
+
break
|
|
291
|
+
end = start + pagination.first
|
|
292
|
+
paginated_threads = filtered_threads[start:end] or []
|
|
293
|
+
|
|
294
|
+
has_next_page = len(filtered_threads) > end
|
|
295
|
+
start_cursor = paginated_threads[0]["id"] if paginated_threads else None
|
|
296
|
+
end_cursor = paginated_threads[-1]["id"] if paginated_threads else None
|
|
297
|
+
|
|
298
|
+
return PaginatedResponse(
|
|
299
|
+
pageInfo=PageInfo(
|
|
300
|
+
hasNextPage=has_next_page,
|
|
301
|
+
startCursor=start_cursor,
|
|
302
|
+
endCursor=end_cursor,
|
|
303
|
+
),
|
|
304
|
+
data=paginated_threads,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
###### Steps ######
|
|
308
|
+
@queue_until_user_message()
|
|
309
|
+
async def create_step(self, step_dict: "StepDict"):
|
|
310
|
+
if self.show_logger:
|
|
311
|
+
logger.info(f"SQLAlchemy: create_step, step_id={step_dict.get('id')}")
|
|
312
|
+
if not getattr(context.session.user, "id", None):
|
|
313
|
+
raise ValueError("No authenticated user in context")
|
|
314
|
+
step_dict["showInput"] = (
|
|
315
|
+
str(step_dict.get("showInput", "")).lower()
|
|
316
|
+
if "showInput" in step_dict
|
|
317
|
+
else None
|
|
318
|
+
)
|
|
319
|
+
parameters = {
|
|
320
|
+
key: value
|
|
321
|
+
for key, value in step_dict.items()
|
|
322
|
+
if value is not None and not (isinstance(value, dict) and not value)
|
|
323
|
+
}
|
|
324
|
+
parameters["metadata"] = json.dumps(step_dict.get("metadata", {}))
|
|
325
|
+
parameters["generation"] = json.dumps(step_dict.get("generation", {}))
|
|
326
|
+
columns = ", ".join(f'"{key}"' for key in parameters.keys())
|
|
327
|
+
values = ", ".join(f":{key}" for key in parameters.keys())
|
|
328
|
+
updates = ", ".join(
|
|
329
|
+
f'"{key}" = :{key}' for key in parameters.keys() if key != "id"
|
|
330
|
+
)
|
|
331
|
+
query = f"""
|
|
332
|
+
INSERT INTO steps ({columns})
|
|
333
|
+
VALUES ({values})
|
|
334
|
+
ON CONFLICT (id) DO UPDATE
|
|
335
|
+
SET {updates};
|
|
336
|
+
"""
|
|
337
|
+
await self.execute_sql(query=query, parameters=parameters)
|
|
338
|
+
|
|
339
|
+
@queue_until_user_message()
|
|
340
|
+
async def update_step(self, step_dict: "StepDict"):
|
|
341
|
+
if self.show_logger:
|
|
342
|
+
logger.info(f"SQLAlchemy: update_step, step_id={step_dict.get('id')}")
|
|
343
|
+
await self.create_step(step_dict)
|
|
344
|
+
|
|
345
|
+
@queue_until_user_message()
|
|
346
|
+
async def delete_step(self, step_id: str):
|
|
347
|
+
if self.show_logger:
|
|
348
|
+
logger.info(f"SQLAlchemy: delete_step, step_id={step_id}")
|
|
349
|
+
# Delete feedbacks/elements/steps
|
|
350
|
+
feedbacks_query = """DELETE FROM feedbacks WHERE "forId" = :id"""
|
|
351
|
+
elements_query = """DELETE FROM elements WHERE "forId" = :id"""
|
|
352
|
+
steps_query = """DELETE FROM steps WHERE "id" = :id"""
|
|
353
|
+
parameters = {"id": step_id}
|
|
354
|
+
await self.execute_sql(query=feedbacks_query, parameters=parameters)
|
|
355
|
+
await self.execute_sql(query=elements_query, parameters=parameters)
|
|
356
|
+
await self.execute_sql(query=steps_query, parameters=parameters)
|
|
357
|
+
|
|
358
|
+
###### Feedback ######
|
|
359
|
+
async def upsert_feedback(self, feedback: Feedback) -> str:
|
|
360
|
+
if self.show_logger:
|
|
361
|
+
logger.info(f"SQLAlchemy: upsert_feedback, feedback_id={feedback.id}")
|
|
362
|
+
feedback.id = feedback.id or str(uuid.uuid4())
|
|
363
|
+
feedback_dict = asdict(feedback)
|
|
364
|
+
parameters = {
|
|
365
|
+
key: value for key, value in feedback_dict.items() if value is not None
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
columns = ", ".join(f'"{key}"' for key in parameters.keys())
|
|
369
|
+
values = ", ".join(f":{key}" for key in parameters.keys())
|
|
370
|
+
updates = ", ".join(
|
|
371
|
+
f'"{key}" = :{key}' for key in parameters.keys() if key != "id"
|
|
372
|
+
)
|
|
373
|
+
query = f"""
|
|
374
|
+
INSERT INTO feedbacks ({columns})
|
|
375
|
+
VALUES ({values})
|
|
376
|
+
ON CONFLICT (id) DO UPDATE
|
|
377
|
+
SET {updates};
|
|
378
|
+
"""
|
|
379
|
+
await self.execute_sql(query=query, parameters=parameters)
|
|
380
|
+
return feedback.id
|
|
381
|
+
|
|
382
|
+
async def delete_feedback(self, feedback_id: str) -> bool:
|
|
383
|
+
if self.show_logger:
|
|
384
|
+
logger.info(f"SQLAlchemy: delete_feedback, feedback_id={feedback_id}")
|
|
385
|
+
query = """DELETE FROM feedbacks WHERE "id" = :feedback_id"""
|
|
386
|
+
parameters = {"feedback_id": feedback_id}
|
|
387
|
+
await self.execute_sql(query=query, parameters=parameters)
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
###### Elements ######
|
|
391
|
+
@queue_until_user_message()
|
|
392
|
+
async def create_element(self, element: "Element"):
|
|
393
|
+
if self.show_logger:
|
|
394
|
+
logger.info(f"SQLAlchemy: create_element, element_id = {element.id}")
|
|
395
|
+
if not getattr(context.session.user, "id", None):
|
|
396
|
+
raise ValueError("No authenticated user in context")
|
|
397
|
+
if not self.storage_provider:
|
|
398
|
+
logger.warn(
|
|
399
|
+
f"SQLAlchemy: create_element error. No blob_storage_client is configured!"
|
|
400
|
+
)
|
|
401
|
+
return
|
|
402
|
+
if not element.for_id:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
content: Optional[Union[bytes, str]] = None
|
|
406
|
+
|
|
407
|
+
if element.path:
|
|
408
|
+
async with aiofiles.open(element.path, "rb") as f:
|
|
409
|
+
content = await f.read()
|
|
410
|
+
elif element.url:
|
|
411
|
+
async with aiohttp.ClientSession() as session:
|
|
412
|
+
async with session.get(element.url) as response:
|
|
413
|
+
if response.status == 200:
|
|
414
|
+
content = await response.read()
|
|
415
|
+
else:
|
|
416
|
+
content = None
|
|
417
|
+
elif element.content:
|
|
418
|
+
content = element.content
|
|
419
|
+
else:
|
|
420
|
+
raise ValueError("Element url, path or content must be provided")
|
|
421
|
+
if content is None:
|
|
422
|
+
raise ValueError("Content is None, cannot upload file")
|
|
423
|
+
|
|
424
|
+
context_user = context.session.user
|
|
425
|
+
|
|
426
|
+
user_folder = getattr(context_user, "id", "unknown")
|
|
427
|
+
file_object_key = f"{user_folder}/{element.id}" + (
|
|
428
|
+
f"/{element.name}" if element.name else ""
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if not element.mime:
|
|
432
|
+
element.mime = "application/octet-stream"
|
|
433
|
+
|
|
434
|
+
uploaded_file = await self.storage_provider.upload_file(
|
|
435
|
+
object_key=file_object_key, data=content, mime=element.mime, overwrite=True
|
|
436
|
+
)
|
|
437
|
+
if not uploaded_file:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
"SQLAlchemy Error: create_element, Failed to persist data in storage_provider"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
element_dict: ElementDict = element.to_dict()
|
|
443
|
+
|
|
444
|
+
element_dict["url"] = uploaded_file.get("url")
|
|
445
|
+
element_dict["objectKey"] = uploaded_file.get("object_key")
|
|
446
|
+
element_dict_cleaned = {k: v for k, v in element_dict.items() if v is not None}
|
|
447
|
+
|
|
448
|
+
columns = ", ".join(f'"{column}"' for column in element_dict_cleaned.keys())
|
|
449
|
+
placeholders = ", ".join(f":{column}" for column in element_dict_cleaned.keys())
|
|
450
|
+
query = f"INSERT INTO elements ({columns}) VALUES ({placeholders})"
|
|
451
|
+
await self.execute_sql(query=query, parameters=element_dict_cleaned)
|
|
452
|
+
|
|
453
|
+
@queue_until_user_message()
|
|
454
|
+
async def delete_element(self, element_id: str, thread_id: Optional[str] = None):
|
|
455
|
+
if self.show_logger:
|
|
456
|
+
logger.info(f"SQLAlchemy: delete_element, element_id={element_id}")
|
|
457
|
+
query = """DELETE FROM elements WHERE "id" = :id"""
|
|
458
|
+
parameters = {"id": element_id}
|
|
459
|
+
await self.execute_sql(query=query, parameters=parameters)
|
|
460
|
+
|
|
461
|
+
async def delete_user_session(self, id: str) -> bool:
|
|
462
|
+
return False # Not sure why documentation wants this
|
|
463
|
+
|
|
464
|
+
async def get_all_user_threads(
|
|
465
|
+
self, user_id: Optional[str] = None, thread_id: Optional[str] = None
|
|
466
|
+
) -> Optional[List[ThreadDict]]:
|
|
467
|
+
"""Fetch all user threads up to self.user_thread_limit, or one thread by id if thread_id is provided."""
|
|
468
|
+
if self.show_logger:
|
|
469
|
+
logger.info(f"SQLAlchemy: get_all_user_threads")
|
|
470
|
+
user_threads_query = """
|
|
471
|
+
SELECT
|
|
472
|
+
"id" AS thread_id,
|
|
473
|
+
"createdAt" AS thread_createdat,
|
|
474
|
+
"name" AS thread_name,
|
|
475
|
+
"userId" AS user_id,
|
|
476
|
+
"userIdentifier" AS user_identifier,
|
|
477
|
+
"tags" AS thread_tags,
|
|
478
|
+
"metadata" AS thread_metadata
|
|
479
|
+
FROM threads
|
|
480
|
+
WHERE "userId" = :user_id OR "id" = :thread_id
|
|
481
|
+
ORDER BY "createdAt" DESC
|
|
482
|
+
LIMIT :limit
|
|
483
|
+
"""
|
|
484
|
+
user_threads = await self.execute_sql(
|
|
485
|
+
query=user_threads_query,
|
|
486
|
+
parameters={
|
|
487
|
+
"user_id": user_id,
|
|
488
|
+
"limit": self.user_thread_limit,
|
|
489
|
+
"thread_id": thread_id,
|
|
490
|
+
},
|
|
491
|
+
)
|
|
492
|
+
if not isinstance(user_threads, list):
|
|
493
|
+
return None
|
|
494
|
+
if not user_threads:
|
|
495
|
+
return []
|
|
496
|
+
else:
|
|
497
|
+
thread_ids = (
|
|
498
|
+
"('"
|
|
499
|
+
+ "','".join(map(str, [thread["thread_id"] for thread in user_threads]))
|
|
500
|
+
+ "')"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
steps_feedbacks_query = f"""
|
|
504
|
+
SELECT
|
|
505
|
+
s."id" AS step_id,
|
|
506
|
+
s."name" AS step_name,
|
|
507
|
+
s."type" AS step_type,
|
|
508
|
+
s."threadId" AS step_threadid,
|
|
509
|
+
s."parentId" AS step_parentid,
|
|
510
|
+
s."streaming" AS step_streaming,
|
|
511
|
+
s."waitForAnswer" AS step_waitforanswer,
|
|
512
|
+
s."isError" AS step_iserror,
|
|
513
|
+
s."metadata" AS step_metadata,
|
|
514
|
+
s."tags" AS step_tags,
|
|
515
|
+
s."input" AS step_input,
|
|
516
|
+
s."output" AS step_output,
|
|
517
|
+
s."createdAt" AS step_createdat,
|
|
518
|
+
s."start" AS step_start,
|
|
519
|
+
s."end" AS step_end,
|
|
520
|
+
s."generation" AS step_generation,
|
|
521
|
+
s."showInput" AS step_showinput,
|
|
522
|
+
s."language" AS step_language,
|
|
523
|
+
s."indent" AS step_indent,
|
|
524
|
+
f."value" AS feedback_value,
|
|
525
|
+
f."comment" AS feedback_comment
|
|
526
|
+
FROM steps s LEFT JOIN feedbacks f ON s."id" = f."forId"
|
|
527
|
+
WHERE s."threadId" IN {thread_ids}
|
|
528
|
+
ORDER BY s."createdAt" ASC
|
|
529
|
+
"""
|
|
530
|
+
steps_feedbacks = await self.execute_sql(
|
|
531
|
+
query=steps_feedbacks_query, parameters={}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
elements_query = f"""
|
|
535
|
+
SELECT
|
|
536
|
+
e."id" AS element_id,
|
|
537
|
+
e."threadId" as element_threadid,
|
|
538
|
+
e."type" AS element_type,
|
|
539
|
+
e."chainlitKey" AS element_chainlitkey,
|
|
540
|
+
e."url" AS element_url,
|
|
541
|
+
e."objectKey" as element_objectkey,
|
|
542
|
+
e."name" AS element_name,
|
|
543
|
+
e."display" AS element_display,
|
|
544
|
+
e."size" AS element_size,
|
|
545
|
+
e."language" AS element_language,
|
|
546
|
+
e."page" AS element_page,
|
|
547
|
+
e."forId" AS element_forid,
|
|
548
|
+
e."mime" AS element_mime
|
|
549
|
+
FROM elements e
|
|
550
|
+
WHERE e."threadId" IN {thread_ids}
|
|
551
|
+
"""
|
|
552
|
+
elements = await self.execute_sql(query=elements_query, parameters={})
|
|
553
|
+
|
|
554
|
+
thread_dicts = {}
|
|
555
|
+
for thread in user_threads:
|
|
556
|
+
thread_id = thread["thread_id"]
|
|
557
|
+
if thread_id is not None:
|
|
558
|
+
thread_dicts[thread_id] = ThreadDict(
|
|
559
|
+
id=thread_id,
|
|
560
|
+
createdAt=thread["thread_createdat"],
|
|
561
|
+
name=thread["thread_name"],
|
|
562
|
+
userId=thread["user_id"],
|
|
563
|
+
userIdentifier=thread["user_identifier"],
|
|
564
|
+
tags=thread["thread_tags"],
|
|
565
|
+
metadata=thread["thread_metadata"],
|
|
566
|
+
steps=[],
|
|
567
|
+
elements=[],
|
|
568
|
+
)
|
|
569
|
+
# Process steps_feedbacks to populate the steps in the corresponding ThreadDict
|
|
570
|
+
if isinstance(steps_feedbacks, list):
|
|
571
|
+
for step_feedback in steps_feedbacks:
|
|
572
|
+
thread_id = step_feedback["step_threadid"]
|
|
573
|
+
if thread_id is not None:
|
|
574
|
+
feedback = None
|
|
575
|
+
if step_feedback["feedback_value"] is not None:
|
|
576
|
+
feedback = FeedbackDict(
|
|
577
|
+
forId=step_feedback["step_id"],
|
|
578
|
+
id=step_feedback.get("feedback_id"),
|
|
579
|
+
value=step_feedback["feedback_value"],
|
|
580
|
+
comment=step_feedback.get("feedback_comment"),
|
|
581
|
+
)
|
|
582
|
+
step_dict = StepDict(
|
|
583
|
+
id=step_feedback["step_id"],
|
|
584
|
+
name=step_feedback["step_name"],
|
|
585
|
+
type=step_feedback["step_type"],
|
|
586
|
+
threadId=thread_id,
|
|
587
|
+
parentId=step_feedback.get("step_parentid"),
|
|
588
|
+
streaming=step_feedback.get("step_streaming", False),
|
|
589
|
+
waitForAnswer=step_feedback.get("step_waitforanswer"),
|
|
590
|
+
isError=step_feedback.get("step_iserror"),
|
|
591
|
+
metadata=(
|
|
592
|
+
step_feedback["step_metadata"]
|
|
593
|
+
if step_feedback.get("step_metadata") is not None
|
|
594
|
+
else {}
|
|
595
|
+
),
|
|
596
|
+
tags=step_feedback.get("step_tags"),
|
|
597
|
+
input=(
|
|
598
|
+
step_feedback.get("step_input", "")
|
|
599
|
+
if step_feedback["step_showinput"] == "true"
|
|
600
|
+
else None
|
|
601
|
+
),
|
|
602
|
+
output=step_feedback.get("step_output", ""),
|
|
603
|
+
createdAt=step_feedback.get("step_createdat"),
|
|
604
|
+
start=step_feedback.get("step_start"),
|
|
605
|
+
end=step_feedback.get("step_end"),
|
|
606
|
+
generation=step_feedback.get("step_generation"),
|
|
607
|
+
showInput=step_feedback.get("step_showinput"),
|
|
608
|
+
language=step_feedback.get("step_language"),
|
|
609
|
+
indent=step_feedback.get("step_indent"),
|
|
610
|
+
feedback=feedback,
|
|
611
|
+
)
|
|
612
|
+
# Append the step to the steps list of the corresponding ThreadDict
|
|
613
|
+
thread_dicts[thread_id]["steps"].append(step_dict)
|
|
614
|
+
|
|
615
|
+
if isinstance(elements, list):
|
|
616
|
+
for element in elements:
|
|
617
|
+
thread_id = element["element_threadid"]
|
|
618
|
+
if thread_id is not None:
|
|
619
|
+
element_dict = ElementDict(
|
|
620
|
+
id=element["element_id"],
|
|
621
|
+
threadId=thread_id,
|
|
622
|
+
type=element["element_type"],
|
|
623
|
+
chainlitKey=element.get("element_chainlitkey"),
|
|
624
|
+
url=element.get("element_url"),
|
|
625
|
+
objectKey=element.get("element_objectkey"),
|
|
626
|
+
name=element["element_name"],
|
|
627
|
+
display=element["element_display"],
|
|
628
|
+
size=element.get("element_size"),
|
|
629
|
+
language=element.get("element_language"),
|
|
630
|
+
autoPlay=element.get("element_autoPlay"),
|
|
631
|
+
playerConfig=element.get("element_playerconfig"),
|
|
632
|
+
page=element.get("element_page"),
|
|
633
|
+
forId=element.get("element_forid"),
|
|
634
|
+
mime=element.get("element_mime"),
|
|
635
|
+
)
|
|
636
|
+
thread_dicts[thread_id]["elements"].append(element_dict) # type: ignore
|
|
637
|
+
|
|
638
|
+
return list(thread_dicts.values())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PraisonAI
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.47
|
|
4
4
|
Summary: PraisonAI application combines AutoGen and CrewAI or similar frameworks into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customization, and efficient human-agent collaboration.
|
|
5
5
|
Author: Mervin Praison
|
|
6
6
|
Requires-Python: >=3.10,<3.13
|
|
@@ -51,6 +51,11 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
|
|
52
52
|
Praison AI, leveraging both AutoGen and CrewAI or any other agent framework, represents a low-code, centralised framework designed to simplify the creation and orchestration of multi-agent systems for various LLM applications, emphasizing ease of use, customization, and human-agent interaction.
|
|
53
53
|
|
|
54
|
+
| | Cookbook | Open in Colab |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| Basic | PraisonAI | <a target="_blank" href="https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/cookbooks/praisonai-googlecolab.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a> |
|
|
57
|
+
| Include Tools | PraisonAI Tools | <a target="_blank" href="https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/cookbooks/praisonai-tools-googlecolab.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a> |
|
|
58
|
+
|
|
54
59
|
## TL;DR
|
|
55
60
|
```bash
|
|
56
61
|
pip install praisonai
|
|
@@ -59,6 +64,22 @@ praisonai --init create a movie script about dog in moon
|
|
|
59
64
|
praisonai
|
|
60
65
|
```
|
|
61
66
|
|
|
67
|
+
## Table of Contents
|
|
68
|
+
|
|
69
|
+
- [Installation](#installation)
|
|
70
|
+
- [Initialise](#initialise)
|
|
71
|
+
- [Run](#run)
|
|
72
|
+
- [Full Automatic Mode](#full-automatic-mode)
|
|
73
|
+
- [User Interface](#user-interface)
|
|
74
|
+
- [Praison AI Chat](#praison-ai-chat)
|
|
75
|
+
- [Create Custom Tools](#create-custom-tools)
|
|
76
|
+
- [Agents Playbook](#agents-playbook)
|
|
77
|
+
- [Include praisonai package in your project](#include-praisonai-package-in-your-project)
|
|
78
|
+
- [Commands to Install Dev Dependencies](#commands-to-install-dependencies)
|
|
79
|
+
- [Other Models](#other-models)
|
|
80
|
+
- [Contributing](#contributing)
|
|
81
|
+
- [Star History](#star-history)
|
|
82
|
+
|
|
62
83
|
## Installation
|
|
63
84
|
|
|
64
85
|
```bash
|
|
@@ -126,29 +147,26 @@ or
|
|
|
126
147
|
python -m praisonai ui
|
|
127
148
|
```
|
|
128
149
|
|
|
129
|
-
##
|
|
150
|
+
## Praison AI Chat
|
|
130
151
|
|
|
131
|
-
|
|
152
|
+
* https://docs.praison.ai/chat/
|
|
132
153
|
|
|
133
154
|
```bash
|
|
134
|
-
pip install praisonai
|
|
155
|
+
pip install "praisonai[chat]"
|
|
135
156
|
export OPENAI_API_KEY="Enter your API key"
|
|
136
|
-
praisonai
|
|
157
|
+
praisonai chat
|
|
137
158
|
```
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
- Create a file called tools.py and add this code [tools.py](./tools.py)
|
|
160
|
+
## Create Custom Tools
|
|
141
161
|
|
|
142
|
-
|
|
143
|
-
praisonai
|
|
144
|
-
```
|
|
162
|
+
* https://docs.praison.ai/tools/custom/
|
|
145
163
|
|
|
146
|
-
### Pre-requisite to Create a Custom Tool
|
|
164
|
+
### Step 1: Pre-requisite to Create a Custom Tool
|
|
147
165
|
`agents.yaml` file should be present in the current directory.
|
|
148
166
|
|
|
149
167
|
If it doesn't exist, create it by running the command `praisonai --init research about the latest AI News and prepare a detailed report`.
|
|
150
168
|
|
|
151
|
-
### Step
|
|
169
|
+
### Step 2: to Create a Custom Tool
|
|
152
170
|
|
|
153
171
|
Create a file called tools.py in the same directory as the agents.yaml file.
|
|
154
172
|
|
|
@@ -167,7 +185,7 @@ class InternetSearchTool(BaseTool):
|
|
|
167
185
|
return results
|
|
168
186
|
```
|
|
169
187
|
|
|
170
|
-
### Step
|
|
188
|
+
### Step 3: to Create a Custom Tool
|
|
171
189
|
|
|
172
190
|
Add the tool to the agents.yaml file as show below under the tools section `- InternetSearchTool`.
|
|
173
191
|
|
|
@@ -206,37 +224,63 @@ roles:
|
|
|
206
224
|
expected_output: 'Complete script ready for production.'
|
|
207
225
|
```
|
|
208
226
|
|
|
227
|
+
## Use 100+ Models
|
|
228
|
+
|
|
229
|
+
* https://docs.praison.ai/models/
|
|
230
|
+
|
|
209
231
|
## Include praisonai package in your project
|
|
210
232
|
|
|
233
|
+
* https://docs.praison.ai/developers/wrapper
|
|
234
|
+
* https://docs.praison.ai/developers/wrapper-tools/
|
|
235
|
+
|
|
236
|
+
## Option 1: Using RAW YAML
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from praisonai import PraisonAI
|
|
240
|
+
|
|
241
|
+
# Example agent_yaml content
|
|
242
|
+
agent_yaml = """
|
|
243
|
+
framework: "crewai"
|
|
244
|
+
topic: "Space Exploration"
|
|
245
|
+
|
|
246
|
+
roles:
|
|
247
|
+
astronomer:
|
|
248
|
+
role: "Space Researcher"
|
|
249
|
+
goal: "Discover new insights about {topic}"
|
|
250
|
+
backstory: "You are a curious and dedicated astronomer with a passion for unraveling the mysteries of the cosmos."
|
|
251
|
+
tasks:
|
|
252
|
+
investigate_exoplanets:
|
|
253
|
+
description: "Research and compile information about exoplanets discovered in the last decade."
|
|
254
|
+
expected_output: "A summarized report on exoplanet discoveries, including their size, potential habitability, and distance from Earth."
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
# Create a PraisonAI instance with the agent_yaml content
|
|
258
|
+
praisonai = PraisonAI(agent_yaml=agent_yaml)
|
|
259
|
+
|
|
260
|
+
# Run PraisonAI
|
|
261
|
+
result = praisonai.run()
|
|
262
|
+
|
|
263
|
+
# Print the result
|
|
264
|
+
print(result)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Option 2: Using separate agents.yaml file
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
Note: Please create agents.yaml file before hand.
|
|
271
|
+
|
|
211
272
|
```python
|
|
212
273
|
from praisonai import PraisonAI
|
|
213
274
|
|
|
214
275
|
def basic(): # Basic Mode
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def advanced(): # Advanced Mode with options
|
|
219
|
-
praison_ai = PraisonAI(
|
|
220
|
-
agent_file="agents.yaml",
|
|
221
|
-
framework="autogen",
|
|
222
|
-
)
|
|
223
|
-
praison_ai.main()
|
|
224
|
-
|
|
225
|
-
def auto(): # Full Automatic Mode
|
|
226
|
-
praison_ai = PraisonAI(
|
|
227
|
-
auto="Create a movie script about car in mars",
|
|
228
|
-
framework="autogen"
|
|
229
|
-
)
|
|
230
|
-
print(praison_ai.framework)
|
|
231
|
-
praison_ai.main()
|
|
276
|
+
praisonai = PraisonAI(agent_file="agents.yaml")
|
|
277
|
+
praisonai.run()
|
|
232
278
|
|
|
233
279
|
if __name__ == "__main__":
|
|
234
280
|
basic()
|
|
235
|
-
advanced()
|
|
236
|
-
auto()
|
|
237
281
|
```
|
|
238
282
|
|
|
239
|
-
|
|
283
|
+
## Commands to Install Dependencies:
|
|
240
284
|
|
|
241
285
|
1. **Install all dependencies, including dev dependencies:**
|
|
242
286
|
```sh
|
|
@@ -260,30 +304,6 @@ if __name__ == "__main__":
|
|
|
260
304
|
|
|
261
305
|
This configuration ensures that your development dependencies are correctly categorized and installed as needed.
|
|
262
306
|
|
|
263
|
-
## Other Models
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
# Ollama
|
|
267
|
-
OPENAI_API_BASE='http://localhost:11434/v1'
|
|
268
|
-
OPENAI_MODEL_NAME='mistral'
|
|
269
|
-
OPENAI_API_KEY='NA'
|
|
270
|
-
|
|
271
|
-
# FastChat
|
|
272
|
-
OPENAI_API_BASE="http://localhost:8001/v1"
|
|
273
|
-
OPENAI_MODEL_NAME='oh-2.5m7b-q51'
|
|
274
|
-
OPENAI_API_KEY=NA
|
|
275
|
-
|
|
276
|
-
# LM Studio
|
|
277
|
-
OPENAI_API_BASE="http://localhost:1234/v1"
|
|
278
|
-
OPENAI_MODEL_NAME=NA
|
|
279
|
-
OPENAI_API_KEY=NA
|
|
280
|
-
|
|
281
|
-
# Mistral API
|
|
282
|
-
OPENAI_API_BASE=https://api.mistral.ai/v1
|
|
283
|
-
OPENAI_MODEL_NAME="mistral-small"
|
|
284
|
-
OPENAI_API_KEY=your-mistral-api-key
|
|
285
|
-
```
|
|
286
|
-
|
|
287
307
|
## Contributing
|
|
288
308
|
|
|
289
309
|
- Fork on GitHub: Use the "Fork" button on the repository page.
|
|
@@ -3,8 +3,8 @@ praisonai/__main__.py,sha256=MVgsjMThjBexHt4nhd760JCqvP4x0IQcwo8kULOK4FQ,144
|
|
|
3
3
|
praisonai/agents_generator.py,sha256=8d1WRbubvEkBrW1HZ7_xnGyqgJi0yxmXa3MgTIqef1c,19127
|
|
4
4
|
praisonai/auto.py,sha256=9spTXqj47Hmmqv5QHRYE_RzSVHH_KoPbaZjskUj2UcE,7895
|
|
5
5
|
praisonai/chainlit_ui.py,sha256=bNR7s509lp0I9JlJNvwCZRUZosC64qdvlFCt8NmFamQ,12216
|
|
6
|
-
praisonai/cli.py,sha256=
|
|
7
|
-
praisonai/deploy.py,sha256=
|
|
6
|
+
praisonai/cli.py,sha256=JBI3JjI7RB0QQipuy-utJpP3jxj8gnG5roMLW2Zjq6A,12851
|
|
7
|
+
praisonai/deploy.py,sha256=4lahg-H78WZXKbJynxA1IvSOUPyV8USS5QFvFGTPGro,6028
|
|
8
8
|
praisonai/inbuilt_tools/__init__.py,sha256=mUKnbL6Gram9c9f2m8wJwEzURBLmPEOcHzwySBH89YA,74
|
|
9
9
|
praisonai/inbuilt_tools/autogen_tools.py,sha256=svYkM2N7DVFvbiwgoAS7U_MqTOD8rHf8VD3BaFUV5_Y,14907
|
|
10
10
|
praisonai/inc/__init__.py,sha256=sPDlYBBwdk0VlWzaaM_lG0_LD07lS2HRGvPdxXJFiYg,62
|
|
@@ -22,16 +22,17 @@ praisonai/public/logo_light.png,sha256=8cQRti_Ysa30O3_7C3ku2w40LnVUUlUok47H-3ZZH
|
|
|
22
22
|
praisonai/public/movie.svg,sha256=aJ2EQ8vXZusVsF2SeuAVxP4RFJzQ14T26ejrGYdBgzk,1289
|
|
23
23
|
praisonai/public/thriller.svg,sha256=2dYY72EcgbEyTxS4QzjAm37Y4srtPWEW4vCMFki98ZI,3163
|
|
24
24
|
praisonai/test.py,sha256=RZKq3UEFb6AnFFiHER3zBXfNmlteSLBlrTmOvnpnZLo,4092
|
|
25
|
-
praisonai/ui/chat.py,sha256=
|
|
25
|
+
praisonai/ui/chat.py,sha256=S3a5u0mI7RO5QFbKckz4z8b32gRTiX8kauSHvQBTMco,9238
|
|
26
26
|
praisonai/ui/public/fantasy.svg,sha256=4Gs3kIOux-pjGtw6ogI_rv5_viVJxnE5gRwGilsSg0o,1553
|
|
27
27
|
praisonai/ui/public/game.svg,sha256=y2QMaA01m8XzuDjTOBWzupOC3-TpnUl9ah89mIhviUw,2406
|
|
28
28
|
praisonai/ui/public/logo_dark.png,sha256=frHz1zkrnivGssJgk9iy1cabojkVgm8B4MllFwL_CnI,17050
|
|
29
29
|
praisonai/ui/public/logo_light.png,sha256=8cQRti_Ysa30O3_7C3ku2w40LnVUUlUok47H-3ZZHSU,19656
|
|
30
30
|
praisonai/ui/public/movie.svg,sha256=aJ2EQ8vXZusVsF2SeuAVxP4RFJzQ14T26ejrGYdBgzk,1289
|
|
31
31
|
praisonai/ui/public/thriller.svg,sha256=2dYY72EcgbEyTxS4QzjAm37Y4srtPWEW4vCMFki98ZI,3163
|
|
32
|
+
praisonai/ui/sql_alchemy.py,sha256=HsyeRq-G9qbQobHWpTJHHKQiT4FvYw_7iuv-2PNh0IU,27419
|
|
32
33
|
praisonai/version.py,sha256=ugyuFliEqtAwQmH4sTlc16YXKYbFWDmfyk87fErB8-8,21
|
|
33
|
-
praisonai-0.0.
|
|
34
|
-
praisonai-0.0.
|
|
35
|
-
praisonai-0.0.
|
|
36
|
-
praisonai-0.0.
|
|
37
|
-
praisonai-0.0.
|
|
34
|
+
praisonai-0.0.47.dist-info/LICENSE,sha256=kqvFysVlnFxYOu0HxCe2HlmZmJtdmNGOxWRRkT9TsWc,1035
|
|
35
|
+
praisonai-0.0.47.dist-info/METADATA,sha256=FH2jJ9hDTdpt4pNpl4hR8Pige1GuzlISpLBs45F-FK0,9301
|
|
36
|
+
praisonai-0.0.47.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
37
|
+
praisonai-0.0.47.dist-info/entry_points.txt,sha256=Qg41eW3A1-dvdV5tF7LqChfYof8Rihk2rN1fiEE3vnk,53
|
|
38
|
+
praisonai-0.0.47.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|