llms-py 3.0.0b10__py3-none-any.whl → 3.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. llms/{extensions/app/db_manager.py → db.py} +170 -15
  2. llms/extensions/app/__init__.py +95 -39
  3. llms/extensions/app/db.py +16 -124
  4. llms/extensions/app/ui/threadStore.mjs +20 -2
  5. llms/extensions/core_tools/__init__.py +37 -0
  6. llms/extensions/gallery/__init__.py +15 -13
  7. llms/extensions/gallery/db.py +117 -172
  8. llms/extensions/gallery/ui/index.mjs +1 -1
  9. llms/extensions/providers/__init__.py +3 -1
  10. llms/extensions/providers/anthropic.py +7 -3
  11. llms/extensions/providers/cerebras.py +37 -0
  12. llms/extensions/providers/chutes.py +1 -1
  13. llms/extensions/providers/google.py +131 -28
  14. llms/extensions/providers/nvidia.py +2 -2
  15. llms/extensions/providers/openai.py +2 -2
  16. llms/extensions/providers/openrouter.py +4 -2
  17. llms/extensions/system_prompts/ui/index.mjs +21 -26
  18. llms/extensions/system_prompts/ui/prompts.json +5 -5
  19. llms/llms.json +3 -0
  20. llms/main.py +81 -34
  21. llms/providers.json +1 -1
  22. llms/ui/ai.mjs +1 -1
  23. llms/ui/app.css +96 -3
  24. llms/ui/ctx.mjs +24 -1
  25. llms/ui/index.mjs +2 -0
  26. llms/ui/modules/chat/ChatBody.mjs +1 -0
  27. llms/ui/modules/chat/index.mjs +19 -1
  28. llms/ui/modules/icons.mjs +46 -0
  29. llms/ui/modules/layout.mjs +28 -0
  30. llms/ui/modules/model-selector.mjs +0 -40
  31. llms/ui/utils.mjs +9 -1
  32. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/METADATA +1 -1
  33. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/RECORD +37 -65
  34. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  35. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  36. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  37. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  38. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  39. llms/__pycache__/llms.cpython-312.pyc +0 -0
  40. llms/__pycache__/main.cpython-312.pyc +0 -0
  41. llms/__pycache__/main.cpython-313.pyc +0 -0
  42. llms/__pycache__/main.cpython-314.pyc +0 -0
  43. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  44. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  45. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  46. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  47. llms/extensions/app/requests.json +0 -9073
  48. llms/extensions/app/threads.json +0 -15290
  49. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  50. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
  51. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
  52. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  53. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  54. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  55. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  56. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  57. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  58. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  59. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  60. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  61. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  62. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  63. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  64. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/WHEEL +0 -0
  65. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/entry_points.txt +0 -0
  66. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/licenses/LICENSE +0 -0
  67. {llms_py-3.0.0b10.dist-info → llms_py-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,19 @@
1
- import json
1
+ import datetime
2
2
  import os
3
- import sqlite3
4
3
  from typing import Any, Dict
5
4
 
5
+ from llms.db import DbManager, order_by, to_dto
6
+
7
+
8
+ def with_user(data, user):
9
+ if user is None:
10
+ if "user" in data:
11
+ del data["user"]
12
+ return data
13
+ else:
14
+ data["user"] = user
15
+ return data
16
+
6
17
 
7
18
  def ratio_format(ratio):
8
19
  w, h = ratio.split(":")
@@ -20,7 +31,13 @@ class GalleryDB:
20
31
 
21
32
  self.ctx = ctx
22
33
  self.db_path = str(db_path)
34
+ dirname = os.path.dirname(self.db_path)
35
+ if dirname:
36
+ os.makedirs(dirname, exist_ok=True)
37
+
38
+ self.db = DbManager(ctx, self.db_path)
23
39
  self.columns = {
40
+ "id": "INTEGER",
24
41
  "name": "TEXT", # chutes-hunyuan-image-3.png (filename)
25
42
  "type": "TEXT", # image|audio|video
26
43
  "prompt": "TEXT",
@@ -59,7 +76,8 @@ class GalleryDB:
59
76
  "landscape": [ratio for ratio in ratios if ratio_format(ratio) == 1],
60
77
  "portrait": [ratio for ratio in ratios if ratio_format(ratio) == -1],
61
78
  }
62
- self.init_db()
79
+ with self.db.create_writer_connection() as conn:
80
+ self.init_db(conn)
63
81
 
64
82
  def closest_aspect_ratio(self, width, height):
65
83
  target_ratio = width / height
@@ -76,172 +94,108 @@ class GalleryDB:
76
94
  return closest_ratio
77
95
 
78
96
  def get_connection(self):
79
- return sqlite3.connect(self.db_path)
97
+ return self.db.create_reader_connection()
80
98
 
81
- def exec(self, conn, sql, parameters=None):
82
- self.ctx.dbg("SQL>" + ("\n" if "\n" in sql else " ") + sql)
83
- return conn.execute(sql, parameters or ())
84
-
85
- def all(self, conn, sql, parameters=None):
86
- conn.row_factory = sqlite3.Row
87
- cursor = self.exec(conn, sql, parameters)
88
- return [dict(row) for row in cursor.fetchall()]
89
-
90
- def init_db(self):
91
- dirname = os.path.dirname(self.db_path)
92
- if dirname:
93
- os.makedirs(dirname, exist_ok=True)
94
- with self.get_connection() as conn:
95
- # Create table with all columns
96
- self.exec(
97
- conn,
98
- """
99
- CREATE TABLE IF NOT EXISTS media (
100
- id INTEGER PRIMARY KEY AUTOINCREMENT,
101
- name TEXT,
102
- type TEXT NOT NULL,
103
- prompt TEXT,
104
- model TEXT,
105
- created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
106
- cost REAL,
107
- seed INTEGER,
108
- url TEXT NOT NULL UNIQUE,
109
- hash TEXT NOT NULL UNIQUE,
110
- aspect_ratio TEXT,
111
- width INTEGER,
112
- height INTEGER,
113
- size INTEGER,
114
- duration INTEGER,
115
- user TEXT,
116
- reactions JSON,
117
- caption TEXT,
118
- description TEXT,
119
- phash TEXT,
120
- color TEXT,
121
- category JSON,
122
- tags JSON,
123
- rating TEXT,
124
- ratings JSON,
125
- objects JSON,
126
- variantId TEXT,
127
- variantName TEXT,
128
- published TIMESTAMP,
129
- metadata JSON
130
- )
131
- """,
99
+ def init_db(self, conn):
100
+ # Create table with all columns
101
+ overrides = {
102
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
103
+ "created": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
104
+ }
105
+ sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns.items()])
106
+ self.db.exec(
107
+ conn,
108
+ f"""
109
+ CREATE TABLE IF NOT EXISTS media (
110
+ {sql_columns}
132
111
  )
112
+ """,
113
+ )
133
114
 
134
- self.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_user ON media(user)")
135
- self.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_type ON media(type)")
115
+ self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_user ON media(user)")
116
+ self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_type ON media(type)")
136
117
 
137
- # Check for missing columns and migrate if necessary
138
- cur = self.exec(conn, "PRAGMA table_info(media)")
139
- columns = {row[1] for row in cur.fetchall()}
118
+ # Check for missing columns and migrate if necessary
119
+ cur = self.db.exec(conn, "PRAGMA table_info(media)")
120
+ columns = {row[1] for row in cur.fetchall()}
140
121
 
141
- for col, dtype in self.columns.items():
142
- if col not in columns:
143
- try:
144
- self.exec(conn, f"ALTER TABLE media ADD COLUMN {col} {dtype}")
145
- except Exception as e:
146
- self.ctx.err(f"adding column {col}", e)
122
+ for col, dtype in self.columns.items():
123
+ if col not in columns:
124
+ try:
125
+ self.db.exec(conn, f"ALTER TABLE media ADD COLUMN {col} {dtype}")
126
+ except Exception as e:
127
+ self.ctx.err(f"adding column {col}", e)
147
128
 
148
- def insert_media(self, info):
149
- try:
150
- if not info:
151
- raise Exception("info is required")
152
-
153
- # Helper to safely dump JSON if value exists
154
- def db_value(val):
155
- if val is None or val == "":
156
- return None
157
- if isinstance(val, (dict, list)):
158
- return json.dumps(val)
159
- return val
160
-
161
- meta = {}
162
- metadata = {}
163
- known_columns = self.columns.keys()
164
- for k in known_columns:
165
- val = info.get(k, None)
166
- if k == "metadata":
167
- continue
168
- if k == "created" and not val:
169
- continue
170
- if k == "type":
171
- parts = val.split("/")
172
- if parts[0] == "image" or parts[0] == "video" or parts[0] == "audio":
173
- meta[k] = parts[0]
174
- else:
175
- meta[k] = db_value(val)
176
- # for items not in known_columns, add to metadata
177
- for k in info:
178
- if k not in known_columns:
179
- metadata[k] = info[k]
180
-
181
- if not meta.get("hash"):
182
- meta["hash"] = meta["url"].split("/")[-1].split(".")[0]
183
-
184
- if "width" in meta and "height" in meta and meta["width"] and meta["height"]:
185
- meta["aspect_ratio"] = self.closest_aspect_ratio(int(meta["width"]), int(meta["height"]))
186
-
187
- meta["metadata"] = db_value(metadata)
188
-
189
- with self.get_connection() as conn:
190
- insert_keys = list(meta.keys())
191
- insert_body = ", ".join(insert_keys)
192
- insert_values = ", ".join(["?" for _ in insert_keys])
193
-
194
- self.exec(
195
- conn,
196
- f"""
197
- INSERT INTO media (
198
- {insert_body}
199
- )
200
- VALUES ({insert_values})
201
- """,
202
- tuple(meta[k] for k in insert_keys),
203
- )
204
- except sqlite3.IntegrityError as e:
205
- # unique constraint failed, file already exists.
206
- self.ctx.dbg(f"media already exists {e}")
207
- except Exception as e:
208
- self.ctx.err("insert media", e)
129
+ def to_dto(self, row, json_columns):
130
+ return to_dto(self.ctx, row, json_columns)
209
131
 
210
- def get_user_filter(self, user=None):
132
+ def get_user_filter(self, user=None, params=None):
211
133
  if user is None:
212
- return "WHERE user IS NULL ", {}
134
+ return "WHERE user IS NULL", params or {}
135
+ else:
136
+ args = params.copy() if params else {}
137
+ args.update({"user": user})
138
+ return "WHERE user = :user", args
139
+
140
+ def prepare_media(self, media, id=None, user=None):
141
+ now = datetime.now()
142
+ if id:
143
+ media["id"] = id
213
144
  else:
214
- return "WHERE user = :user ", {"user": user}
145
+ media["created"] = now
146
+ return with_user(media, user=user)
147
+
148
+ def insert_media(self, info, user=None, callback=None):
149
+ if not info:
150
+ raise Exception("info is required")
151
+
152
+ media = {}
153
+ metadata = {}
154
+ known_columns = self.columns.keys()
155
+ for k in known_columns:
156
+ val = info.get(k, None)
157
+ if k == "metadata":
158
+ continue
159
+ if k == "created" and not val:
160
+ continue
161
+ if k == "type":
162
+ parts = val.split("/")
163
+ if parts[0] == "image" or parts[0] == "video" or parts[0] == "audio":
164
+ media[k] = parts[0]
165
+ else:
166
+ media[k] = self.db.value(val)
167
+ # for items not in known_columns, add to metadata
168
+ for k in info:
169
+ if k not in known_columns:
170
+ metadata[k] = info[k]
171
+
172
+ if not media.get("hash"):
173
+ media["hash"] = media["url"].split("/")[-1].split(".")[0]
174
+
175
+ if "width" in media and "height" in media and media["width"] and media["height"]:
176
+ media["aspect_ratio"] = self.closest_aspect_ratio(int(media["width"]), int(media["height"]))
177
+
178
+ media["metadata"] = self.db.value(metadata)
179
+ media = with_user(media, user=user)
180
+
181
+ insert_keys = list(media.keys())
182
+ insert_body = ", ".join(insert_keys)
183
+ insert_values = ", ".join(["?" for _ in insert_keys])
184
+
185
+ sql = f"INSERT INTO media ({insert_body}) VALUES ({insert_values})"
186
+
187
+ self.db.write(sql, tuple(media[k] for k in insert_keys), callback)
215
188
 
216
189
  def media_totals(self, user=None):
217
- try:
218
- with self.get_connection() as conn:
219
- sql_where, params = self.get_user_filter(user)
220
- return self.all(
221
- conn,
222
- f"SELECT type, COUNT(*) as count FROM media {sql_where} GROUP BY type ORDER BY count DESC",
223
- params,
224
- )
225
- except Exception as e:
226
- self.ctx.err("media_totals", e)
227
- return []
228
-
229
- def all_media(self, limit=100, offset=0, user=None):
230
- try:
231
- with self.get_connection() as conn:
232
- sql_where, params = self.get_user_filter(user)
233
- params.update({"limit": limit, "offset": offset})
234
- return self.all(
235
- conn,
236
- f"SELECT * FROM media {sql_where} ORDER BY id DESC LIMIT :limit OFFSET :offset",
237
- params,
238
- )
239
- except Exception as e:
240
- self.ctx.err(f"all_media ({limit}, {offset})", e)
241
- return []
190
+ sql_where, params = self.get_user_filter(user)
191
+ return self.db.all(
192
+ f"SELECT type, COUNT(*) as count FROM media {sql_where} GROUP BY type ORDER BY count DESC",
193
+ params,
194
+ )
242
195
 
243
196
  def query_media(self, query: Dict[str, Any], user=None):
244
197
  try:
198
+ all_columns = self.columns.keys()
245
199
  take = query.get("take", 50)
246
200
  skip = query.get("skip", 0)
247
201
  sort = query.get("sort", "-id")
@@ -275,24 +229,15 @@ class GalleryDB:
275
229
  ratios = ", ".join([f"'{ratio}'" for ratio in format_ratios])
276
230
  sql_where += f"aspect_ratio IN ({ratios})"
277
231
 
278
- sql_orderby = "ORDER BY " + sort
279
- sql_orderby = sql_orderby[1:] + " DESC" if sql_orderby.startswith("-") else sql_orderby + " ASC"
280
-
281
- with self.get_connection() as conn:
282
- return self.all(
283
- conn,
284
- f"SELECT * FROM media {sql_where} {sql_orderby} LIMIT :take OFFSET :skip",
285
- params,
286
- )
232
+ return self.db.all(
233
+ f"SELECT * FROM media {sql_where} {order_by(all_columns, sort)} LIMIT :take OFFSET :skip",
234
+ params,
235
+ )
287
236
  except Exception as e:
288
237
  self.ctx.err(f"query_media ({take}, {skip})", e)
289
238
  return []
290
239
 
291
- def delete_media(self, hash, user=None):
292
- try:
293
- with self.get_connection() as conn:
294
- sql_where, params = self.get_user_filter(user)
295
- params.update({"hash": hash})
296
- self.exec(conn, f"DELETE FROM media {sql_where} AND hash = :hash", params)
297
- except Exception as e:
298
- self.ctx.err(f"delete_media ({hash})", e)
240
+ def delete_media(self, hash, user=None, callback=None):
241
+ sql_where, params = self.get_user_filter(user)
242
+ params.update({"hash": hash})
243
+ self.db.write(f"DELETE FROM media {sql_where} AND hash = :hash", params, callback)
@@ -471,7 +471,7 @@ export default {
471
471
  ctx.setLeftIcons({
472
472
  gallery: {
473
473
  component: {
474
- template: `<svg @click="$ctx.togglePath('/gallery')" viewBox="0 0 15 15" class="w-6 h-6"><path fill="currentColor" d="M10.71 3L7.85.15a.5.5 0 0 0-.707-.003L7.14.15L4.29 3H1.5a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5zM7.5 1.21L9.29 3H5.71zM13 12H2V4h11zM5 7a1 1 0 1 1 0-2a1 1 0 0 1 0 2m7 4H4.5L6 8l1.25 2.5L9.5 6z"/></svg>`,
474
+ template: `<svg @click="$ctx.togglePath('/gallery')" viewBox="0 0 15 15"><path fill="currentColor" d="M10.71 3L7.85.15a.5.5 0 0 0-.707-.003L7.14.15L4.29 3H1.5a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5zM7.5 1.21L9.29 3H5.71zM13 12H2V4h11zM5 7a1 1 0 1 1 0-2a1 1 0 0 1 0 2m7 4H4.5L6 8l1.25 2.5L9.5 6z"/></svg>`,
475
475
  },
476
476
  isActive({ path }) { return path === '/gallery' }
477
477
  }
@@ -1,4 +1,5 @@
1
1
  from .anthropic import install_anthropic
2
+ from .cerebras import install_cerebras
2
3
  from .chutes import install_chutes
3
4
  from .google import install_google
4
5
  from .nvidia import install_nvidia
@@ -8,11 +9,12 @@ from .openrouter import install_openrouter
8
9
 
9
10
  def install(ctx):
10
11
  install_anthropic(ctx)
12
+ install_cerebras(ctx)
11
13
  install_chutes(ctx)
12
14
  install_google(ctx)
15
+ install_nvidia(ctx)
13
16
  install_openai(ctx)
14
17
  install_openrouter(ctx)
15
- install_nvidia(ctx)
16
18
 
17
19
 
18
20
  __install__ = install
@@ -27,7 +27,7 @@ def install_anthropic(ctx):
27
27
  self.headers["anthropic-version"] = "2023-06-01"
28
28
  self.chat_url = f"{self.api}/messages"
29
29
 
30
- async def chat(self, chat):
30
+ async def chat(self, chat, context=None):
31
31
  chat["model"] = self.provider_model(chat["model"]) or chat["model"]
32
32
 
33
33
  chat = await self.process_chat(chat, provider_id=self.id)
@@ -149,10 +149,14 @@ def install_anthropic(ctx):
149
149
  data=json.dumps(anthropic_request),
150
150
  timeout=aiohttp.ClientTimeout(total=120),
151
151
  ) as response:
152
- return ctx.log_json(self.to_response(await self.response_json(response), chat, started_at))
152
+ return ctx.log_json(
153
+ self.to_response(await self.response_json(response), chat, started_at, context=context)
154
+ )
153
155
 
154
- def to_response(self, response, chat, started_at):
156
+ def to_response(self, response, chat, started_at, context=None):
155
157
  """Convert Anthropic response format to OpenAI-compatible format."""
158
+ if context is not None:
159
+ context["providerResponse"] = response
156
160
  # Transform Anthropic response to OpenAI format
157
161
  ret = {
158
162
  "id": response.get("id", ""),
@@ -0,0 +1,37 @@
1
+ def install_cerebras(ctx):
2
+ from llms.main import OpenAiCompatible
3
+
4
+ class CerebrasProvider(OpenAiCompatible):
5
+ sdk = "@ai-sdk/cerebras"
6
+
7
+ def __init__(self, **kwargs):
8
+ if "api" not in kwargs:
9
+ kwargs["api"] = "https://api.cerebras.ai/v1"
10
+ super().__init__(**kwargs)
11
+
12
+ async def chat(self, chat, context=None):
13
+ # Cerebras only supports string content for text-only models
14
+ clean_chat = chat.copy()
15
+ clean_chat["messages"] = []
16
+ for msg in chat.get("messages", []):
17
+ new_msg = msg.copy()
18
+ content = msg.get("content")
19
+ if isinstance(content, list):
20
+ # Check if text only
21
+ is_text_only = True
22
+ text_parts = []
23
+ for part in content:
24
+ if part.get("type") != "text":
25
+ is_text_only = False
26
+ break
27
+ text_parts.append(part.get("text", ""))
28
+
29
+ if is_text_only:
30
+ new_msg["content"] = "".join(text_parts)
31
+ clean_chat["messages"].append(new_msg)
32
+
33
+ clean_chat.pop("modalities", None)
34
+ clean_chat.pop("systemPrompt", None)
35
+ return await super().chat(clean_chat, context)
36
+
37
+ ctx.add_provider(CerebrasProvider)
@@ -40,7 +40,7 @@ def install_chutes(ctx):
40
40
  "iLustMix",
41
41
  ]
42
42
 
43
- async def chat(self, chat, provider=None):
43
+ async def chat(self, chat, provider=None, context=None):
44
44
  headers = {"Authorization": f"Bearer {self.api_key}"}
45
45
  if provider is not None:
46
46
  headers["Authorization"] = f"Bearer {provider.api_key}"