PraisonAI 1.0.4__tar.gz → 1.0.6__tar.gz

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.

Files changed (55) hide show
  1. {praisonai-1.0.4 → praisonai-1.0.6}/PKG-INFO +1 -1
  2. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/deploy.py +1 -1
  3. praisonai-1.0.6/praisonai/ui/README.md +21 -0
  4. praisonai-1.0.6/praisonai/ui/chat.py +598 -0
  5. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/sql_alchemy.py +165 -169
  6. {praisonai-1.0.4 → praisonai-1.0.6}/pyproject.toml +1 -1
  7. praisonai-1.0.4/praisonai/ui/chat.py +0 -654
  8. {praisonai-1.0.4 → praisonai-1.0.6}/LICENSE +0 -0
  9. {praisonai-1.0.4 → praisonai-1.0.6}/README.md +0 -0
  10. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/__init__.py +0 -0
  11. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/__main__.py +0 -0
  12. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/agents_generator.py +0 -0
  13. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/api/call.py +0 -0
  14. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/auto.py +0 -0
  15. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/chainlit_ui.py +0 -0
  16. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/cli.py +0 -0
  17. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/inbuilt_tools/__init__.py +0 -0
  18. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/inbuilt_tools/autogen_tools.py +0 -0
  19. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/inc/__init__.py +0 -0
  20. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/inc/config.py +0 -0
  21. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/inc/models.py +0 -0
  22. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/android-chrome-192x192.png +0 -0
  23. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/android-chrome-512x512.png +0 -0
  24. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/apple-touch-icon.png +0 -0
  25. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/fantasy.svg +0 -0
  26. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/favicon-16x16.png +0 -0
  27. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/favicon-32x32.png +0 -0
  28. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/favicon.ico +0 -0
  29. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/game.svg +0 -0
  30. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/logo_dark.png +0 -0
  31. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/logo_light.png +0 -0
  32. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/movie.svg +0 -0
  33. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/public/thriller.svg +0 -0
  34. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/__init__.py +0 -0
  35. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/build.py +0 -0
  36. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/config.yaml +0 -0
  37. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/post_install.py +0 -0
  38. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/setup_conda_env.py +0 -0
  39. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup/setup_conda_env.sh +0 -0
  40. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/setup.py +0 -0
  41. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/test.py +0 -0
  42. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/train.py +0 -0
  43. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/code.py +0 -0
  44. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/context.py +0 -0
  45. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/fantasy.svg +0 -0
  46. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/game.svg +0 -0
  47. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/logo_dark.png +0 -0
  48. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/logo_light.png +0 -0
  49. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/movie.svg +0 -0
  50. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/public/thriller.svg +0 -0
  51. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/realtime.py +0 -0
  52. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/realtimeclient/__init__.py +0 -0
  53. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/realtimeclient/realtimedocs.txt +0 -0
  54. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/ui/realtimeclient/tools.py +0 -0
  55. {praisonai-1.0.4 → praisonai-1.0.6}/praisonai/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PraisonAI
3
- Version: 1.0.4
3
+ Version: 1.0.6
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
@@ -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==1.0.4 gunicorn markdown\n")
59
+ file.write("RUN pip install flask praisonai==1.0.6 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
 
@@ -0,0 +1,21 @@
1
+ # Changes to DB
2
+
3
+ The following columns are renamed or modified between the first and second versions of the code:
4
+
5
+ | Table Name | Original Column Name | New Column Name |
6
+ |-------------|----------------------------|--------------------|
7
+ | `users` | `metadata` | `meta` |
8
+ | `users` | `created_at` | `createdAt` |
9
+ | `threads` | `metadata` | `meta` |
10
+ | `threads` | `created_at` | `createdAt` |
11
+ | `steps` | `metadata` | `meta` |
12
+ | `steps` | `start_time` | `startTime` |
13
+ | `steps` | `end_time` | `endTime` |
14
+ | `elements` | `metadata` | (Removed) |
15
+
16
+ Key changes:
17
+ 1. The `metadata` column in several tables is renamed to `meta`.
18
+ 2. Timestamps (`created_at`, `start_time`, and `end_time`) are renamed to PascalCase (`createdAt`, `startTime`, and `endTime`).
19
+ 3. Some columns are removed (e.g., `metadata` in `elements`).
20
+
21
+ These changes make the column names consistent and follow a specific naming convention.
@@ -0,0 +1,598 @@
1
+ import chainlit as cl
2
+ from chainlit.input_widget import TextInput
3
+ from chainlit.types import ThreadDict
4
+ from litellm import acompletion
5
+ import os
6
+ import sqlite3
7
+ from datetime import datetime
8
+ from typing import Dict, List, Optional
9
+ from dotenv import load_dotenv
10
+ load_dotenv()
11
+ import chainlit.data as cl_data
12
+ from chainlit.step import StepDict
13
+ from literalai.helper import utc_now
14
+ import logging
15
+ import json
16
+ from sql_alchemy import SQLAlchemyDataLayer
17
+ from tavily import TavilyClient
18
+ from crawl4ai import AsyncWebCrawler
19
+ import asyncio
20
+ from PIL import Image
21
+ import io
22
+ import base64
23
+
24
+ from sqlalchemy.ext.asyncio import create_async_engine
25
+ from sqlalchemy import text
26
+
27
+ logger = logging.getLogger(__name__)
28
+ log_level = os.getenv("LOGLEVEL", "INFO").upper()
29
+ logger.handlers = []
30
+ console_handler = logging.StreamHandler()
31
+ console_handler.setLevel(log_level)
32
+ console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
33
+ console_handler.setFormatter(console_formatter)
34
+ logger.addHandler(console_handler)
35
+ logger.setLevel(log_level)
36
+
37
+ CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
38
+ if not CHAINLIT_AUTH_SECRET:
39
+ os.environ["CHAINLIT_AUTH_SECRET"] = "p8BPhQChpg@J>jBz$wGxqLX2V>yTVgP*7Ky9H$aV:axW~ANNX-7_T:o@lnyCBu^U"
40
+ CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
41
+
42
+ now = utc_now()
43
+ create_step_counter = 0
44
+
45
+ DATABASE_URL = os.getenv("DATABASE_URL")
46
+
47
+ async def create_schema_async():
48
+ if not DATABASE_URL:
49
+ return
50
+ engine = create_async_engine(DATABASE_URL, echo=False)
51
+ async with engine.begin() as conn:
52
+ # Use double quotes for column names and renamed start/end to startTime/endTime
53
+ await conn.execute(text('''
54
+ CREATE TABLE IF NOT EXISTS users (
55
+ "id" TEXT PRIMARY KEY,
56
+ "identifier" TEXT NOT NULL UNIQUE,
57
+ "meta" TEXT NOT NULL DEFAULT '{}',
58
+ "createdAt" TEXT
59
+ );
60
+ '''))
61
+ await conn.execute(text('''
62
+ CREATE TABLE IF NOT EXISTS threads (
63
+ "id" TEXT PRIMARY KEY,
64
+ "createdAt" TEXT,
65
+ "name" TEXT,
66
+ "userId" TEXT,
67
+ "userIdentifier" TEXT,
68
+ "tags" TEXT DEFAULT '[]',
69
+ "meta" TEXT NOT NULL DEFAULT '{}',
70
+ FOREIGN KEY ("userId") REFERENCES users("id") ON DELETE CASCADE
71
+ );
72
+ '''))
73
+ await conn.execute(text('''
74
+ CREATE TABLE IF NOT EXISTS steps (
75
+ "id" TEXT PRIMARY KEY,
76
+ "name" TEXT NOT NULL,
77
+ "type" TEXT NOT NULL,
78
+ "threadId" TEXT NOT NULL,
79
+ "parentId" TEXT,
80
+ "disableFeedback" BOOLEAN NOT NULL DEFAULT FALSE,
81
+ "streaming" BOOLEAN NOT NULL DEFAULT FALSE,
82
+ "waitForAnswer" BOOLEAN DEFAULT FALSE,
83
+ "isError" BOOLEAN NOT NULL DEFAULT FALSE,
84
+ "meta" TEXT DEFAULT '{}',
85
+ "tags" TEXT DEFAULT '[]',
86
+ "input" TEXT,
87
+ "output" TEXT,
88
+ "createdAt" TEXT,
89
+ "startTime" TEXT,
90
+ "endTime" TEXT,
91
+ "generation" TEXT,
92
+ "showInput" TEXT,
93
+ "language" TEXT,
94
+ "indent" INT,
95
+ FOREIGN KEY ("threadId") REFERENCES threads("id") ON DELETE CASCADE
96
+ );
97
+ '''))
98
+ await conn.execute(text('''
99
+ CREATE TABLE IF NOT EXISTS elements (
100
+ "id" TEXT PRIMARY KEY,
101
+ "threadId" TEXT,
102
+ "type" TEXT,
103
+ "url" TEXT,
104
+ "chainlitKey" TEXT,
105
+ "name" TEXT NOT NULL,
106
+ "display" TEXT,
107
+ "objectKey" TEXT,
108
+ "size" TEXT,
109
+ "page" INT,
110
+ "language" TEXT,
111
+ "forId" TEXT,
112
+ "mime" TEXT,
113
+ FOREIGN KEY ("threadId") REFERENCES threads("id") ON DELETE CASCADE
114
+ );
115
+ '''))
116
+ await conn.execute(text('''
117
+ CREATE TABLE IF NOT EXISTS feedbacks (
118
+ "id" TEXT PRIMARY KEY,
119
+ "forId" TEXT NOT NULL,
120
+ "value" INT NOT NULL,
121
+ "threadId" TEXT,
122
+ "comment" TEXT
123
+ );
124
+ '''))
125
+ await conn.execute(text('''
126
+ CREATE TABLE IF NOT EXISTS settings (
127
+ "id" SERIAL PRIMARY KEY,
128
+ "key" TEXT UNIQUE,
129
+ "value" TEXT
130
+ );
131
+ '''))
132
+ await engine.dispose()
133
+
134
+ def create_schema_sqlite(db_path: str):
135
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
136
+ conn = sqlite3.connect(db_path)
137
+ cursor = conn.cursor()
138
+ # Renamed start to startTime, end to endTime
139
+ cursor.execute('''
140
+ CREATE TABLE IF NOT EXISTS users (
141
+ "id" TEXT PRIMARY KEY,
142
+ "identifier" TEXT NOT NULL UNIQUE,
143
+ "meta" TEXT NOT NULL DEFAULT '{}',
144
+ "createdAt" TEXT
145
+ )
146
+ ''')
147
+ cursor.execute('''
148
+ CREATE TABLE IF NOT EXISTS threads (
149
+ "id" TEXT PRIMARY KEY,
150
+ "createdAt" TEXT,
151
+ "name" TEXT,
152
+ "userId" TEXT,
153
+ "userIdentifier" TEXT,
154
+ "tags" TEXT DEFAULT '[]',
155
+ "meta" TEXT NOT NULL DEFAULT '{}',
156
+ FOREIGN KEY ("userId") REFERENCES users("id") ON DELETE CASCADE
157
+ )
158
+ ''')
159
+ cursor.execute('''
160
+ CREATE TABLE IF NOT EXISTS steps (
161
+ "id" TEXT PRIMARY KEY,
162
+ "name" TEXT NOT NULL,
163
+ "type" TEXT NOT NULL,
164
+ "threadId" TEXT NOT NULL,
165
+ "parentId" TEXT,
166
+ "disableFeedback" BOOLEAN NOT NULL DEFAULT 0,
167
+ "streaming" BOOLEAN NOT NULL DEFAULT 0,
168
+ "waitForAnswer" BOOLEAN DEFAULT 0,
169
+ "isError" BOOLEAN NOT NULL DEFAULT 0,
170
+ "meta" TEXT DEFAULT '{}',
171
+ "tags" TEXT DEFAULT '[]',
172
+ "input" TEXT,
173
+ "output" TEXT,
174
+ "createdAt" TEXT,
175
+ "startTime" TEXT,
176
+ "endTime" TEXT,
177
+ "generation" TEXT,
178
+ "showInput" TEXT,
179
+ "language" TEXT,
180
+ "indent" INT,
181
+ FOREIGN KEY ("threadId") REFERENCES threads ("id") ON DELETE CASCADE
182
+ )
183
+ ''')
184
+ cursor.execute('''
185
+ CREATE TABLE IF NOT EXISTS elements (
186
+ "id" TEXT PRIMARY KEY,
187
+ "threadId" TEXT,
188
+ "type" TEXT,
189
+ "url" TEXT,
190
+ "chainlitKey" TEXT,
191
+ "name" TEXT NOT NULL,
192
+ "display" TEXT,
193
+ "objectKey" TEXT,
194
+ "size" TEXT,
195
+ "page" INT,
196
+ "language" TEXT,
197
+ "forId" TEXT,
198
+ "mime" TEXT,
199
+ FOREIGN KEY ("threadId") REFERENCES threads ("id") ON DELETE CASCADE
200
+ )
201
+ ''')
202
+ cursor.execute('''
203
+ CREATE TABLE IF NOT EXISTS feedbacks (
204
+ "id" TEXT PRIMARY KEY,
205
+ "forId" TEXT NOT NULL,
206
+ "value" INT NOT NULL,
207
+ "threadId" TEXT,
208
+ "comment" TEXT
209
+ )
210
+ ''')
211
+ cursor.execute('''
212
+ CREATE TABLE IF NOT EXISTS settings (
213
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
214
+ "key" TEXT UNIQUE,
215
+ "value" TEXT
216
+ )
217
+ ''')
218
+ conn.commit()
219
+ conn.close()
220
+
221
+ if DATABASE_URL:
222
+ asyncio.run(create_schema_async())
223
+ conninfo = DATABASE_URL
224
+ else:
225
+ DB_PATH = os.path.expanduser("~/.praison/database.sqlite")
226
+ create_schema_sqlite(DB_PATH)
227
+ conninfo = f"sqlite+aiosqlite:///{DB_PATH}"
228
+
229
+ def save_setting(key: str, value: str):
230
+ if DATABASE_URL:
231
+ async def save_setting_async():
232
+ engine = create_async_engine(DATABASE_URL, echo=False)
233
+ async with engine.begin() as conn:
234
+ await conn.execute(text("""
235
+ INSERT INTO settings ("key", "value") VALUES (:key, :value)
236
+ ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value"
237
+ """), {"key": key, "value": value})
238
+ await engine.dispose()
239
+ asyncio.run(save_setting_async())
240
+ else:
241
+ conn = sqlite3.connect(DB_PATH)
242
+ cursor = conn.cursor()
243
+ cursor.execute(
244
+ """
245
+ INSERT OR REPLACE INTO settings (id, "key", "value")
246
+ VALUES ((SELECT id FROM settings WHERE "key" = ?), ?, ?)
247
+ """,
248
+ (key, key, value),
249
+ )
250
+ conn.commit()
251
+ conn.close()
252
+
253
+ def load_setting(key: str) -> str:
254
+ if DATABASE_URL:
255
+ async def load_setting_async():
256
+ engine = create_async_engine(DATABASE_URL, echo=False)
257
+ async with engine.connect() as conn:
258
+ result = await conn.execute(text('SELECT "value" FROM settings WHERE "key" = :key'), {"key": key})
259
+ row = result.fetchone()
260
+ await engine.dispose()
261
+ return row[0] if row else None
262
+ return asyncio.run(load_setting_async())
263
+ else:
264
+ conn = sqlite3.connect(DB_PATH)
265
+ cursor = conn.cursor()
266
+ cursor.execute('SELECT "value" FROM settings WHERE "key" = ?', (key,))
267
+ result = cursor.fetchone()
268
+ conn.close()
269
+ return result[0] if result else None
270
+
271
+ cl_data._data_layer = SQLAlchemyDataLayer(conninfo=conninfo)
272
+
273
+ tavily_api_key = os.getenv("TAVILY_API_KEY")
274
+ tavily_client = TavilyClient(api_key=tavily_api_key) if tavily_api_key else None
275
+
276
+ async def tavily_web_search(query):
277
+ if not tavily_client:
278
+ return json.dumps({
279
+ "query": query,
280
+ "error": "Tavily API key is not set. Web search is unavailable."
281
+ })
282
+
283
+ response = tavily_client.search(query)
284
+ logger.debug(f"Tavily search response: {response}")
285
+
286
+ async with AsyncWebCrawler() as crawler:
287
+ results = []
288
+ for result in response.get('results', []):
289
+ url = result.get('url')
290
+ if url:
291
+ try:
292
+ crawl_result = await crawler.arun(url=url)
293
+ results.append({
294
+ "content": result.get('content'),
295
+ "url": url,
296
+ "full_content": crawl_result.markdown
297
+ })
298
+ except Exception as e:
299
+ logger.error(f"Error crawling {url}: {str(e)}")
300
+ results.append({
301
+ "content": result.get('content'),
302
+ "url": url,
303
+ "full_content": "Error: Unable to crawl this URL"
304
+ })
305
+
306
+ return json.dumps({
307
+ "query": query,
308
+ "results": results
309
+ })
310
+
311
+ tools = [{
312
+ "type": "function",
313
+ "function": {
314
+ "name": "tavily_web_search",
315
+ "description": "Search the web using Tavily API and crawl the resulting URLs",
316
+ "parameters": {
317
+ "type": "object",
318
+ "properties": {
319
+ "query": {"type": "string", "description": "Search query"}
320
+ },
321
+ "required": ["query"]
322
+ }
323
+ }
324
+ }] if tavily_api_key else []
325
+
326
+ # Authentication configuration
327
+ AUTH_PASSWORD_ENABLED = os.getenv("AUTH_PASSWORD_ENABLED", "true").lower() == "true" # Password authentication enabled by default
328
+ AUTH_OAUTH_ENABLED = os.getenv("AUTH_OAUTH_ENABLED", "false").lower() == "true" # OAuth authentication disabled by default
329
+
330
+ username = os.getenv("CHAINLIT_USERNAME", "admin")
331
+ password = os.getenv("CHAINLIT_PASSWORD", "admin")
332
+
333
+ def auth_callback(u: str, p: str):
334
+ if (u, p) == (username, password):
335
+ return cl.User(identifier=username, metadata={"role": "ADMIN", "provider": "credentials"})
336
+ return None
337
+
338
+ def oauth_callback(
339
+ provider_id: str,
340
+ token: str,
341
+ raw_user_data: Dict[str, str],
342
+ default_user: cl.User,
343
+ ) -> Optional[cl.User]:
344
+ return default_user
345
+
346
+ if AUTH_PASSWORD_ENABLED:
347
+ auth_callback = cl.password_auth_callback(auth_callback)
348
+
349
+ if AUTH_OAUTH_ENABLED:
350
+ oauth_callback = cl.oauth_callback(oauth_callback)
351
+
352
+ async def send_count():
353
+ await cl.Message(
354
+ f"Create step counter: {create_step_counter}", disable_feedback=True
355
+ ).send()
356
+
357
+ @cl.on_chat_start
358
+ async def start():
359
+ model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
360
+ cl.user_session.set("model_name", model_name)
361
+ logger.debug(f"Model name: {model_name}")
362
+ settings = cl.ChatSettings(
363
+ [
364
+ TextInput(
365
+ id="model_name",
366
+ label="Enter the Model Name",
367
+ placeholder="e.g., gpt-4o-mini",
368
+ initial=model_name
369
+ )
370
+ ]
371
+ )
372
+ cl.user_session.set("settings", settings)
373
+ await settings.send()
374
+
375
+ @cl.on_settings_update
376
+ async def setup_agent(settings):
377
+ logger.debug(settings)
378
+ cl.user_session.set("settings", settings)
379
+ model_name = settings["model_name"]
380
+ cl.user_session.set("model_name", model_name)
381
+
382
+ save_setting("model_name", model_name)
383
+
384
+ thread_id = cl.user_session.get("thread_id")
385
+ if thread_id:
386
+ thread = await cl_data.get_thread(thread_id)
387
+ if thread:
388
+ metadata = thread.get("metadata", {})
389
+ if isinstance(metadata, str):
390
+ try:
391
+ metadata = json.loads(metadata)
392
+ except:
393
+ metadata = {}
394
+ metadata["model_name"] = model_name
395
+ await cl_data.update_thread(thread_id, metadata=metadata)
396
+ cl.user_session.set("metadata", metadata)
397
+
398
+ @cl.on_message
399
+ async def main(message: cl.Message):
400
+ model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
401
+ message_history = cl.user_session.get("message_history", [])
402
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
403
+
404
+ image = None
405
+ if message.elements and isinstance(message.elements[0], cl.Image):
406
+ image_element = message.elements[0]
407
+ try:
408
+ image = Image.open(image_element.path)
409
+ image.load()
410
+ cl.user_session.set("image", image)
411
+ except Exception as e:
412
+ logger.error(f"Error processing image: {str(e)}")
413
+ await cl.Message(content="Error processing the image. Please try again.").send()
414
+ return
415
+
416
+ user_message = f"""
417
+ Answer the question and use tools if needed:
418
+
419
+ Current Date and Time: {now}
420
+
421
+ User Question: {message.content}
422
+ """
423
+
424
+ if image:
425
+ user_message = f"Image uploaded. {user_message}"
426
+
427
+ message_history.append({"role": "user", "content": user_message})
428
+ msg = cl.Message(content="")
429
+ await msg.send()
430
+
431
+ completion_params = {
432
+ "model": model_name,
433
+ "messages": message_history,
434
+ "stream": True,
435
+ }
436
+
437
+ if image:
438
+ buffered = io.BytesIO()
439
+ image.save(buffered, format="PNG")
440
+ img_str = base64.b64encode(buffered.getvalue()).decode()
441
+ completion_params["messages"][-1] = {
442
+ "role": "user",
443
+ "content": [
444
+ {"type": "text", "text": user_message},
445
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_str}"}}
446
+ ]
447
+ }
448
+
449
+ if tavily_api_key:
450
+ completion_params["tools"] = tools
451
+ completion_params["tool_choice"] = "auto"
452
+
453
+ response = await acompletion(**completion_params)
454
+
455
+ full_response = ""
456
+ tool_calls = []
457
+ current_tool_call = None
458
+
459
+ async for part in response:
460
+ if 'choices' in part and len(part['choices']) > 0:
461
+ delta = part['choices'][0].get('delta', {})
462
+
463
+ if 'content' in delta and delta['content'] is not None:
464
+ token = delta['content']
465
+ await msg.stream_token(token)
466
+ full_response += token
467
+
468
+ if tavily_api_key and 'tool_calls' in delta and delta['tool_calls'] is not None:
469
+ for tool_call in delta['tool_calls']:
470
+ if current_tool_call is None or tool_call.index != current_tool_call['index']:
471
+ if current_tool_call:
472
+ tool_calls.append(current_tool_call)
473
+ current_tool_call = {
474
+ 'id': tool_call.id,
475
+ 'type': tool_call.type,
476
+ 'index': tool_call.index,
477
+ 'function': {
478
+ 'name': tool_call.function.name if tool_call.function else None,
479
+ 'arguments': ''
480
+ }
481
+ }
482
+ if tool_call.function:
483
+ if tool_call.function.name:
484
+ current_tool_call['function']['name'] = tool_call.function.name
485
+ if tool_call.function.arguments:
486
+ current_tool_call['function']['arguments'] += tool_call.function.arguments
487
+
488
+ if current_tool_call:
489
+ tool_calls.append(current_tool_call)
490
+
491
+ logger.debug(f"Full response: {full_response}")
492
+ logger.debug(f"Tool calls: {tool_calls}")
493
+ message_history.append({"role": "assistant", "content": full_response})
494
+ logger.debug(f"Message history: {message_history}")
495
+ cl.user_session.set("message_history", message_history)
496
+ await msg.update()
497
+
498
+ if tavily_api_key and tool_calls:
499
+ available_functions = {
500
+ "tavily_web_search": tavily_web_search,
501
+ }
502
+ messages = message_history + [{"role": "assistant", "content": None, "function_call": {
503
+ "name": tool_calls[0]['function']['name'],
504
+ "arguments": tool_calls[0]['function']['arguments']
505
+ }}]
506
+
507
+ for tool_call in tool_calls:
508
+ function_name = tool_call['function']['name']
509
+ if function_name in available_functions:
510
+ function_to_call = available_functions[function_name]
511
+ function_args = tool_call['function']['arguments']
512
+ if function_args:
513
+ try:
514
+ function_args = json.loads(function_args)
515
+ function_response = await function_to_call(
516
+ query=function_args.get("query"),
517
+ )
518
+ messages.append(
519
+ {
520
+ "role": "function",
521
+ "name": function_name,
522
+ "content": function_response,
523
+ }
524
+ )
525
+ except json.JSONDecodeError:
526
+ logger.error(f"Failed to parse function arguments: {function_args}")
527
+
528
+ second_response = await acompletion(
529
+ model=model_name,
530
+ stream=True,
531
+ messages=messages,
532
+ )
533
+ logger.debug(f"Second LLM response: {second_response}")
534
+
535
+ full_response = ""
536
+ async for part in second_response:
537
+ if 'choices' in part and len(part['choices']) > 0:
538
+ delta = part['choices'][0].get('delta', {})
539
+ if 'content' in delta and delta['content'] is not None:
540
+ token = delta['content']
541
+ await msg.stream_token(token)
542
+ full_response += token
543
+
544
+ msg.content = full_response
545
+ await msg.update()
546
+ else:
547
+ msg.content = full_response
548
+ await msg.update()
549
+
550
+ @cl.on_chat_resume
551
+ async def on_chat_resume(thread: ThreadDict):
552
+ logger.info(f"Resuming chat: {thread['id']}")
553
+ model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
554
+ logger.debug(f"Model name: {model_name}")
555
+ settings = cl.ChatSettings(
556
+ [
557
+ TextInput(
558
+ id="model_name",
559
+ label="Enter the Model Name",
560
+ placeholder="e.g., gpt-4o-mini",
561
+ initial=model_name
562
+ )
563
+ ]
564
+ )
565
+ await settings.send()
566
+ thread_id = thread["id"]
567
+ cl.user_session.set("thread_id", thread_id)
568
+
569
+ metadata = thread.get("metadata", {})
570
+ if isinstance(metadata, str):
571
+ try:
572
+ metadata = json.loads(metadata)
573
+ except json.JSONDecodeError:
574
+ metadata = {}
575
+ cl.user_session.set("metadata", metadata)
576
+
577
+ message_history = cl.user_session.get("message_history", [])
578
+ steps = thread["steps"]
579
+
580
+ for m in steps:
581
+ msg_type = m.get("type")
582
+ if msg_type == "user_message":
583
+ message_history.append({"role": "user", "content": m.get("output", "")})
584
+ elif msg_type == "assistant_message":
585
+ message_history.append({"role": "assistant", "content": m.get("output", "")})
586
+ elif msg_type == "run":
587
+ if m.get("isError"):
588
+ message_history.append({"role": "system", "content": f"Error: {m.get('output', '')}"})
589
+ else:
590
+ logger.warning(f"Message without recognized type: {m}")
591
+
592
+ cl.user_session.set("message_history", message_history)
593
+
594
+ image_data = metadata.get("image")
595
+ if image_data:
596
+ image = Image.open(io.BytesIO(base64.b64decode(image_data)))
597
+ cl.user_session.set("image", image)
598
+ await cl.Message(content="Previous image loaded.").send()