llms-py 3.0.1__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 (35) hide show
  1. llms/{extensions/app/db_manager.py → db.py} +170 -15
  2. llms/extensions/app/__init__.py +95 -29
  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/llms.json +3 -0
  18. llms/main.py +81 -34
  19. llms/providers.json +1 -1
  20. llms/ui/ai.mjs +1 -1
  21. llms/ui/app.css +96 -3
  22. llms/ui/ctx.mjs +21 -0
  23. llms/ui/index.mjs +2 -0
  24. llms/ui/modules/chat/ChatBody.mjs +1 -0
  25. llms/ui/modules/chat/index.mjs +19 -1
  26. llms/ui/modules/icons.mjs +46 -0
  27. llms/ui/modules/layout.mjs +28 -0
  28. llms/ui/modules/model-selector.mjs +0 -40
  29. llms/ui/utils.mjs +9 -1
  30. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/METADATA +1 -1
  31. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/RECORD +35 -33
  32. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/WHEEL +0 -0
  33. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/entry_points.txt +0 -0
  34. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/licenses/LICENSE +0 -0
  35. {llms_py-3.0.1.dist-info → llms_py-3.0.2.dist-info}/top_level.txt +0 -0
llms/extensions/app/db.py CHANGED
@@ -1,10 +1,9 @@
1
1
  import json
2
2
  import os
3
- import threading
4
3
  from datetime import datetime, timedelta
5
4
  from typing import Any, Dict
6
5
 
7
- from .db_manager import DbManager
6
+ from llms.db import DbManager, order_by, select_columns, to_dto, valid_columns
8
7
 
9
8
 
10
9
  def with_user(data, user):
@@ -17,44 +16,6 @@ def with_user(data, user):
17
16
  return data
18
17
 
19
18
 
20
- def valid_columns(all_columns, fields):
21
- if fields:
22
- if not isinstance(fields, list):
23
- fields = fields.split(",")
24
- cols = []
25
- for k in fields:
26
- k = k.strip()
27
- if k in all_columns:
28
- cols.append(k)
29
- return cols
30
- return []
31
-
32
-
33
- def table_columns(all_columns, fields):
34
- cols = valid_columns(all_columns, fields)
35
- return ", ".join(cols) if len(cols) > 0 else ", ".join(all_columns)
36
-
37
-
38
- def select_columns(all_columns, fields, select=None):
39
- columns = table_columns(all_columns, fields)
40
- if select == "distinct":
41
- return f"SELECT DISTINCT {columns}"
42
- return f"SELECT {columns}"
43
-
44
-
45
- def order_by(all_columns, sort):
46
- cols = []
47
- for k in sort.split(","):
48
- k = k.strip()
49
- by = ""
50
- if k[0] == "-":
51
- by = " DESC"
52
- k = k[1:]
53
- if k in all_columns:
54
- cols.append(f"{k}{by}")
55
- return f"ORDER BY {', '.join(cols)} " if len(cols) > 0 else ""
56
-
57
-
58
19
  class AppDB:
59
20
  def __init__(self, ctx, db_path):
60
21
  if db_path is None:
@@ -81,6 +42,7 @@ class AppDB:
81
42
  "modalities": "JSON",
82
43
  "messages": "JSON",
83
44
  "args": "JSON",
45
+ "tools": "JSON",
84
46
  "toolHistory": "JSON",
85
47
  "cost": "REAL",
86
48
  "inputTokens": "INTEGER",
@@ -94,6 +56,7 @@ class AppDB:
94
56
  "metadata": "JSON",
95
57
  "error": "TEXT",
96
58
  "ref": "TEXT",
59
+ "providerResponse": "JSON",
97
60
  },
98
61
  "request": {
99
62
  "id": "INTEGER",
@@ -300,6 +263,9 @@ class AppDB:
300
263
  sql_params,
301
264
  ).lastrowid
302
265
 
266
+ def to_dto(self, row, json_columns):
267
+ return to_dto(self.ctx, row, json_columns)
268
+
303
269
  def get_user_filter(self, user=None, params=None):
304
270
  if user is None:
305
271
  return "WHERE user IS NULL", params or {}
@@ -309,12 +275,8 @@ class AppDB:
309
275
  return "WHERE user = :user", args
310
276
 
311
277
  def get_thread(self, id, user=None):
312
- try:
313
- sql_where, params = self.get_user_filter(user, {"id": id})
314
- return self.db.one(f"SELECT * FROM thread {sql_where} AND id = :id", params)
315
- except Exception as e:
316
- self.ctx.err(f"get_thread ({id}, {user})", e)
317
- return None
278
+ sql_where, params = self.get_user_filter(user, {"id": id})
279
+ return self.db.one(f"SELECT * FROM thread {sql_where} AND id = :id", params)
318
280
 
319
281
  def get_thread_column(self, id, column, user=None):
320
282
  if column not in self.columns["thread"]:
@@ -375,76 +337,6 @@ class AppDB:
375
337
  self.ctx.err(f"query_threads ({take}, {skip})", e)
376
338
  return []
377
339
 
378
- def insert(self, table, info, callback=None):
379
- if not info:
380
- raise Exception("info is required")
381
-
382
- columns = self.columns[table]
383
- args = {}
384
- known_columns = columns.keys()
385
- for k, val in info.items():
386
- if k in known_columns and k != "id":
387
- args[k] = self.db.value(val)
388
-
389
- insert_keys = list(args.keys())
390
- insert_body = ", ".join(insert_keys)
391
- insert_values = ", ".join(["?" for _ in insert_keys])
392
-
393
- sql = f"INSERT INTO {table} ({insert_body}) VALUES ({insert_values})"
394
-
395
- self.db.write(sql, tuple(args[k] for k in insert_keys), callback)
396
-
397
- async def insert_async(self, table, info):
398
- event = threading.Event()
399
-
400
- ret = [None]
401
-
402
- def cb(lastrowid, rowcount, error=None):
403
- nonlocal ret
404
- if error:
405
- raise error
406
- ret[0] = lastrowid
407
- event.set()
408
-
409
- self.insert(table, info, cb)
410
- event.wait()
411
- return ret[0]
412
-
413
- def update(self, table, info, callback=None):
414
- if not info:
415
- raise Exception("info is required")
416
-
417
- columns = self.columns[table]
418
- args = {}
419
- known_columns = columns.keys()
420
- for k, val in info.items():
421
- if k in known_columns and k != "id":
422
- args[k] = self.db.value(val)
423
-
424
- update_keys = list(args.keys())
425
- update_body = ", ".join([f"{k} = :{k}" for k in update_keys])
426
-
427
- args["id"] = info["id"]
428
- sql = f"UPDATE {table} SET {update_body} WHERE id = :id"
429
-
430
- self.db.write(sql, args, callback)
431
-
432
- async def update_async(self, table, info):
433
- event = threading.Event()
434
-
435
- ret = [None]
436
-
437
- def cb(lastrowid, rowcount, error=None):
438
- nonlocal ret
439
- if error:
440
- raise error
441
- ret[0] = rowcount
442
- event.set()
443
-
444
- self.update(table, info, cb)
445
- event.wait()
446
- return ret[0]
447
-
448
340
  def prepare_thread(self, thread, id=None, user=None):
449
341
  now = datetime.now()
450
342
  if id:
@@ -458,16 +350,16 @@ class AppDB:
458
350
  return with_user(thread, user=user)
459
351
 
460
352
  def create_thread(self, thread: Dict[str, Any], user=None):
461
- return self.insert("thread", self.prepare_thread(thread, user=user))
353
+ return self.db.insert("thread", self.columns["thread"], self.prepare_thread(thread, user=user))
462
354
 
463
355
  async def create_thread_async(self, thread: Dict[str, Any], user=None):
464
- return await self.insert_async("thread", self.prepare_thread(thread, user=user))
356
+ return await self.db.insert_async("thread", self.columns["thread"], self.prepare_thread(thread, user=user))
465
357
 
466
358
  def update_thread(self, id, thread: Dict[str, Any], user=None):
467
- return self.update("thread", self.prepare_thread(thread, id, user=user))
359
+ return self.db.update("thread", self.columns["thread"], self.prepare_thread(thread, id, user=user))
468
360
 
469
361
  async def update_thread_async(self, id, thread: Dict[str, Any], user=None):
470
- return await self.update_async("thread", self.prepare_thread(thread, id, user=user))
362
+ return await self.db.update_async("thread", self.columns["thread"], self.prepare_thread(thread, id, user=user))
471
363
 
472
364
  def delete_thread(self, id, user=None, callback=None):
473
365
  sql_where, params = self.get_user_filter(user, {"id": id})
@@ -609,21 +501,21 @@ class AppDB:
609
501
 
610
502
  def create_request(self, request: Dict[str, Any], user=None):
611
503
  request["createdAt"] = request["updatedAt"] = datetime.now()
612
- return self.insert("request", with_user(request, user=user))
504
+ return self.db.insert("request", self.columns["request"], with_user(request, user=user))
613
505
 
614
506
  async def create_request_async(self, request: Dict[str, Any], user=None):
615
507
  request["createdAt"] = request["updatedAt"] = datetime.now()
616
- return await self.insert_async("request", with_user(request, user=user))
508
+ return await self.db.insert_async("request", self.columns["request"], with_user(request, user=user))
617
509
 
618
510
  def update_request(self, id, request: Dict[str, Any], user=None):
619
511
  request["id"] = id
620
512
  request["updatedAt"] = datetime.now()
621
- return self.update("request", with_user(request, user=user))
513
+ return self.db.update("request", self.columns["request"], with_user(request, user=user))
622
514
 
623
515
  async def update_request_async(self, id, request: Dict[str, Any], user=None):
624
516
  request["id"] = id
625
517
  request["updatedAt"] = datetime.now()
626
- return await self.update_async("request", with_user(request, user=user))
518
+ return await self.db.update_async("request", self.columns["request"], with_user(request, user=user))
627
519
 
628
520
  def delete_request(self, id, user=None, callback=None):
629
521
  sql_where, params = self.get_user_filter(user, {"id": id})
@@ -15,6 +15,7 @@ export const nextId = (() => {
15
15
 
16
16
 
17
17
  const threads = ref([])
18
+ const threadDetails = ref({})
18
19
  const currentThread = ref(null)
19
20
  const isLoading = ref(false)
20
21
 
@@ -196,7 +197,7 @@ async function loadThreads() {
196
197
  isLoading.value = true
197
198
 
198
199
  try {
199
- const api = await ext.getJson('/threads?take=50')
200
+ const api = await ext.getJson('/threads?take=30')
200
201
  threads.value = api.response || []
201
202
  return threads.value
202
203
  } finally {
@@ -239,6 +240,7 @@ async function setCurrentThreadFromRoute(threadId, router) {
239
240
  return null
240
241
  }
241
242
 
243
+ loadThreadDetails(threadId)
242
244
  const thread = setCurrentThread(threadId)
243
245
  if (thread) {
244
246
  return thread
@@ -307,7 +309,7 @@ function getLatestCachedThread() {
307
309
  return threads.value[0]
308
310
  }
309
311
 
310
- async function startNewThread({ title, model, redirect }) {
312
+ async function startNewThread({ title, model, tools, redirect }) {
311
313
  if (!model) {
312
314
  console.error('No model selected')
313
315
  return
@@ -330,6 +332,7 @@ async function startNewThread({ title, model, redirect }) {
330
332
  title,
331
333
  model: model.name,
332
334
  info: ctx.utils.toModelInfo(model),
335
+ tools,
333
336
  })
334
337
 
335
338
  console.log('newThread', newThread, model)
@@ -361,6 +364,19 @@ async function queueChat(ctxRequest, options = {}) {
361
364
  return api
362
365
  }
363
366
 
367
+ async function loadThreadDetails(id, opt = null) {
368
+ if (!threadDetails.value[id] || opt?.force) {
369
+ const api = await ctx.getJson(`/ext/app/threads/${id}`)
370
+ if (api.response) {
371
+ threadDetails.value[id] = api.response
372
+ }
373
+ if (api.error) {
374
+ console.error(api.error)
375
+ }
376
+ }
377
+ return threadDetails.value[id]
378
+ }
379
+
364
380
  // Export the store
365
381
  export function useThreadStore() {
366
382
  return {
@@ -388,6 +404,8 @@ export function useThreadStore() {
388
404
  startNewThread,
389
405
  replaceThread,
390
406
  queueChat,
407
+ threadDetails,
408
+ loadThreadDetails,
391
409
  isWatchingThread,
392
410
  startWatchingThread,
393
411
  stopWatchingThread,
@@ -102,6 +102,24 @@ def write_file(path: str, content: str) -> bool:
102
102
  return True
103
103
 
104
104
 
105
+ def edit_file(path: str, old_str: str, new_str: str) -> Dict[str, Any]:
106
+ """
107
+ Replaces first occurrence of old_str with new_str in file. If old_str is empty,
108
+ create/overwrite file with new_str.
109
+ :return: A dictionary with the path to the file and the action taken.
110
+ """
111
+ safe_path = _resolve_safe_path(path)
112
+ if old_str == "":
113
+ safe_path.write_text(new_str, encoding="utf-8")
114
+ return {"path": str(safe_path), "action": "created_file"}
115
+ original = safe_path.read_text(encoding="utf-8")
116
+ if original.find(old_str) == -1:
117
+ return {"path": str(safe_path), "action": "old_str not found"}
118
+ edited = original.replace(old_str, new_str, 1)
119
+ safe_path.write_text(edited, encoding="utf-8")
120
+ return {"path": str(safe_path), "action": "edited"}
121
+
122
+
105
123
  def list_directory(path: str) -> str:
106
124
  """List directory contents"""
107
125
  safe_path = _resolve_safe_path(path)
@@ -526,6 +544,25 @@ def install(ctx):
526
544
  # ctx.register_tool(semantic_search) # TODO: implement
527
545
  ctx.register_tool(read_file)
528
546
  ctx.register_tool(write_file)
547
+ ctx.register_tool(
548
+ edit_file,
549
+ {
550
+ "type": "function",
551
+ "function": {
552
+ "name": "edit_file",
553
+ "description": "Replaces first occurrence of old_str with new_str in file. If old_str is empty, create/overwrite file with new_str.",
554
+ "parameters": {
555
+ "type": "object",
556
+ "properties": {
557
+ "path": {"type": "string", "description": "Path to the file to edit."},
558
+ "old_str": {"type": "string", "description": "String to replace."},
559
+ "new_str": {"type": "string", "description": "String to replace with."},
560
+ },
561
+ "required": ["path", "old_str", "new_str"],
562
+ },
563
+ },
564
+ },
565
+ )
529
566
  ctx.register_tool(list_directory)
530
567
  ctx.register_tool(glob_paths)
531
568
  ctx.register_tool(calc)
@@ -3,44 +3,46 @@ import os
3
3
 
4
4
  from aiohttp import web
5
5
 
6
- g_db = None
6
+ from .db import GalleryDB
7
7
 
8
- try:
9
- from llms.extensions.gallery.db import GalleryDB
10
- except ImportError as e:
11
- print(f"Failed to import GalleryDB: {e}")
12
- GalleryDB = None
8
+ g_db = None
13
9
 
14
10
 
15
11
  def install(ctx):
16
- def get_gallery_db():
12
+ def get_db():
17
13
  global g_db
18
14
  if g_db is None and GalleryDB:
19
15
  try:
20
16
  db_path = os.path.join(ctx.get_user_path(), "gallery", "gallery.sqlite")
21
17
  g_db = GalleryDB(ctx, db_path)
18
+ ctx.register_shutdown_handler(g_db.db.close)
22
19
  except Exception as e:
23
20
  ctx.err("Failed to init GalleryDB", e)
24
21
  return g_db
25
22
 
26
- if not get_gallery_db():
23
+ if not get_db():
27
24
  return
28
25
 
26
+ def media_dto(row):
27
+ return row and g_db.to_dto(row, ["reactions", "category", "tags", "ratings", "objects", "metadata"])
28
+
29
29
  def on_cache_save(context):
30
- url = context["url"]
31
- info = context["info"]
30
+ url = context.get("url", None)
31
+ info = context.get("info", {})
32
+ user = context.get("user", None)
32
33
  ctx.log(f"cache saved: {url}")
33
- ctx.log(json.dumps(info, indent=2))
34
+ ctx.dbg(json.dumps(info, indent=2))
34
35
 
35
36
  if "url" not in info:
36
37
  info["url"] = url
37
- g_db.insert_media(info)
38
+ g_db.insert_media(info, user=user)
38
39
 
39
40
  ctx.register_cache_saved_filter(on_cache_save)
40
41
 
41
42
  async def query_media(request):
42
43
  rows = g_db.query_media(request.query, user=ctx.get_username(request))
43
- return web.json_response(rows)
44
+ dtos = [media_dto(row) for row in rows]
45
+ return web.json_response(dtos)
44
46
 
45
47
  ctx.add_get("media", query_media)
46
48