PraisonAI 0.0.46__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/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.46 gunicorn markdown\n")
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(" praison_ai = PraisonAI(agent_file=\"agents.yaml\")\n")
82
- file.write(" return praison_ai.main()\n\n")
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 TEXT PRIMARY KEY,
51
- name TEXT,
58
+ id UUID PRIMARY KEY,
52
59
  createdAt TEXT,
53
- userId TEXT,
54
- userIdentifier TEXT
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 TEXT PRIMARY KEY,
60
- threadId TEXT,
61
- name TEXT,
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
- output TEXT,
65
- FOREIGN KEY (threadId) REFERENCES threads (id)
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
- class TestDataLayer(cl_data.BaseDataLayer): # Implement SQLAlchemyDataLayer
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
- await cl_data.update_thread(thread_id, metadata=metadata)
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
- cl.user_session.set("message_history", message_history)
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.46
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
@@ -255,10 +255,10 @@ roles:
255
255
  """
256
256
 
257
257
  # Create a PraisonAI instance with the agent_yaml content
258
- praison_ai = PraisonAI(agent_yaml=agent_yaml)
258
+ praisonai = PraisonAI(agent_yaml=agent_yaml)
259
259
 
260
260
  # Run PraisonAI
261
- result = praison_ai.main()
261
+ result = praisonai.run()
262
262
 
263
263
  # Print the result
264
264
  print(result)
@@ -273,8 +273,8 @@ Note: Please create agents.yaml file before hand.
273
273
  from praisonai import PraisonAI
274
274
 
275
275
  def basic(): # Basic Mode
276
- praison_ai = PraisonAI(agent_file="agents.yaml")
277
- praison_ai.main()
276
+ praisonai = PraisonAI(agent_file="agents.yaml")
277
+ praisonai.run()
278
278
 
279
279
  if __name__ == "__main__":
280
280
  basic()
@@ -4,7 +4,7 @@ praisonai/agents_generator.py,sha256=8d1WRbubvEkBrW1HZ7_xnGyqgJi0yxmXa3MgTIqef1c
4
4
  praisonai/auto.py,sha256=9spTXqj47Hmmqv5QHRYE_RzSVHH_KoPbaZjskUj2UcE,7895
5
5
  praisonai/chainlit_ui.py,sha256=bNR7s509lp0I9JlJNvwCZRUZosC64qdvlFCt8NmFamQ,12216
6
6
  praisonai/cli.py,sha256=JBI3JjI7RB0QQipuy-utJpP3jxj8gnG5roMLW2Zjq6A,12851
7
- praisonai/deploy.py,sha256=UmebXvdSfKGk4A-nQeOkbj2bieEOcMK9VZMTn2z1AOY,6031
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=Gx0SmnE5P6f8csS--gfnInJVmLF-5bWyOBLu1o4AXgc,14643
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.46.dist-info/LICENSE,sha256=kqvFysVlnFxYOu0HxCe2HlmZmJtdmNGOxWRRkT9TsWc,1035
34
- praisonai-0.0.46.dist-info/METADATA,sha256=HFsoACSl5CZakd3MXJtl4pFGB3_6fV-6jBEYyY07wR8,9307
35
- praisonai-0.0.46.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- praisonai-0.0.46.dist-info/entry_points.txt,sha256=Qg41eW3A1-dvdV5tF7LqChfYof8Rihk2rN1fiEE3vnk,53
37
- praisonai-0.0.46.dist-info/RECORD,,
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,,