llms-py 3.0.1__py3-none-any.whl → 3.0.3__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.
- llms/{extensions/app/db_manager.py → db.py} +180 -16
- llms/extensions/app/__init__.py +96 -29
- llms/extensions/app/db.py +16 -124
- llms/extensions/app/ui/threadStore.mjs +23 -2
- llms/extensions/core_tools/__init__.py +37 -0
- llms/extensions/gallery/__init__.py +15 -13
- llms/extensions/gallery/db.py +117 -172
- llms/extensions/gallery/ui/index.mjs +1 -1
- llms/extensions/providers/__init__.py +3 -1
- llms/extensions/providers/anthropic.py +7 -3
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +1 -1
- llms/extensions/providers/google.py +131 -28
- llms/extensions/providers/nvidia.py +2 -2
- llms/extensions/providers/openai.py +2 -2
- llms/extensions/providers/openrouter.py +4 -2
- llms/llms.json +3 -0
- llms/main.py +83 -34
- llms/providers.json +1 -1
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +106 -3
- llms/ui/ctx.mjs +34 -0
- llms/ui/index.mjs +2 -0
- llms/ui/modules/chat/ChatBody.mjs +245 -248
- llms/ui/modules/chat/index.mjs +93 -2
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +28 -0
- llms/ui/modules/model-selector.mjs +0 -40
- llms/ui/tailwind.input.css +1 -1
- llms/ui/utils.mjs +9 -1
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/METADATA +1 -1
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/RECORD +36 -34
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/WHEEL +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.1.dist-info → llms_py-3.0.3.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 .
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
|
@@ -115,6 +116,9 @@ function replaceThread(thread) {
|
|
|
115
116
|
if (currentThread.value?.id === thread.id) {
|
|
116
117
|
currentThread.value = thread
|
|
117
118
|
}
|
|
119
|
+
if (thread.completedAt || thread.error) {
|
|
120
|
+
threadDetails.value[thread.id] = thread
|
|
121
|
+
}
|
|
118
122
|
startWatchingThread()
|
|
119
123
|
return thread
|
|
120
124
|
}
|
|
@@ -196,7 +200,7 @@ async function loadThreads() {
|
|
|
196
200
|
isLoading.value = true
|
|
197
201
|
|
|
198
202
|
try {
|
|
199
|
-
const api = await ext.getJson('/threads?take=
|
|
203
|
+
const api = await ext.getJson('/threads?take=30')
|
|
200
204
|
threads.value = api.response || []
|
|
201
205
|
return threads.value
|
|
202
206
|
} finally {
|
|
@@ -239,6 +243,7 @@ async function setCurrentThreadFromRoute(threadId, router) {
|
|
|
239
243
|
return null
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
loadThreadDetails(threadId)
|
|
242
247
|
const thread = setCurrentThread(threadId)
|
|
243
248
|
if (thread) {
|
|
244
249
|
return thread
|
|
@@ -307,7 +312,7 @@ function getLatestCachedThread() {
|
|
|
307
312
|
return threads.value[0]
|
|
308
313
|
}
|
|
309
314
|
|
|
310
|
-
async function startNewThread({ title, model, redirect }) {
|
|
315
|
+
async function startNewThread({ title, model, tools, redirect }) {
|
|
311
316
|
if (!model) {
|
|
312
317
|
console.error('No model selected')
|
|
313
318
|
return
|
|
@@ -330,6 +335,7 @@ async function startNewThread({ title, model, redirect }) {
|
|
|
330
335
|
title,
|
|
331
336
|
model: model.name,
|
|
332
337
|
info: ctx.utils.toModelInfo(model),
|
|
338
|
+
tools,
|
|
333
339
|
})
|
|
334
340
|
|
|
335
341
|
console.log('newThread', newThread, model)
|
|
@@ -361,6 +367,19 @@ async function queueChat(ctxRequest, options = {}) {
|
|
|
361
367
|
return api
|
|
362
368
|
}
|
|
363
369
|
|
|
370
|
+
async function loadThreadDetails(id, opt = null) {
|
|
371
|
+
if (!threadDetails.value[id] || opt?.force) {
|
|
372
|
+
const api = await ctx.getJson(`/ext/app/threads/${id}`)
|
|
373
|
+
if (api.response) {
|
|
374
|
+
threadDetails.value[id] = api.response
|
|
375
|
+
}
|
|
376
|
+
if (api.error) {
|
|
377
|
+
console.error(api.error)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return threadDetails.value[id]
|
|
381
|
+
}
|
|
382
|
+
|
|
364
383
|
// Export the store
|
|
365
384
|
export function useThreadStore() {
|
|
366
385
|
return {
|
|
@@ -388,6 +407,8 @@ export function useThreadStore() {
|
|
|
388
407
|
startNewThread,
|
|
389
408
|
replaceThread,
|
|
390
409
|
queueChat,
|
|
410
|
+
threadDetails,
|
|
411
|
+
loadThreadDetails,
|
|
391
412
|
isWatchingThread,
|
|
392
413
|
startWatchingThread,
|
|
393
414
|
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
|
-
|
|
6
|
+
from .db import GalleryDB
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
31
|
-
info = context
|
|
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.
|
|
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
|
-
|
|
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
|
|