llms-py 2.0.9__py3-none-any.whl → 3.0.10__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/__init__.py +4 -0
- llms/__main__.py +9 -0
- llms/db.py +359 -0
- llms/extensions/analytics/ui/index.mjs +1444 -0
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
- llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
- llms/extensions/app/ui/threadStore.mjs +433 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +637 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +233 -0
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +481 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +280 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +144 -0
- llms/extensions/tools/ui/index.mjs +706 -0
- llms/index.html +58 -0
- llms/llms.json +400 -0
- llms/main.py +4407 -0
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +188 -0
- llms/ui/ai.mjs +217 -0
- llms/ui/app.css +7081 -0
- llms/ui/ctx.mjs +412 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/modules/chat/SettingsDialog.mjs +374 -0
- llms/ui/modules/chat/index.mjs +991 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +742 -0
- {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
- llms/ui/utils.mjs +261 -0
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- llms_py-3.0.10.dist-info/entry_points.txt +2 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms.py +0 -1402
- llms_py-2.0.9.data/data/index.html +0 -64
- llms_py-2.0.9.data/data/llms.json +0 -447
- llms_py-2.0.9.data/data/requirements.txt +0 -1
- llms_py-2.0.9.data/data/ui/App.mjs +0 -20
- llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
- llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
- llms_py-2.0.9.data/data/ui/app.css +0 -3951
- llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
- llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
- llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
- llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
- llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
- llms_py-2.0.9.data/data/ui.json +0 -1069
- llms_py-2.0.9.dist-info/METADATA +0 -941
- llms_py-2.0.9.dist-info/RECORD +0 -30
- llms_py-2.0.9.dist-info/entry_points.txt +0 -2
- {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
- /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
from llms.db import DbManager, order_by, select_columns, to_dto, valid_columns
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def with_user(data, user):
|
|
10
|
+
if user is None:
|
|
11
|
+
if "user" in data:
|
|
12
|
+
del data["user"]
|
|
13
|
+
return data
|
|
14
|
+
else:
|
|
15
|
+
data["user"] = user
|
|
16
|
+
return data
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AppDB:
|
|
20
|
+
def __init__(self, ctx, db_path):
|
|
21
|
+
if db_path is None:
|
|
22
|
+
raise ValueError("db_path is required")
|
|
23
|
+
|
|
24
|
+
self.ctx = ctx
|
|
25
|
+
self.db_path = str(db_path)
|
|
26
|
+
|
|
27
|
+
dirname = os.path.dirname(self.db_path)
|
|
28
|
+
if dirname:
|
|
29
|
+
os.makedirs(dirname, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
self.db = DbManager(ctx, self.db_path)
|
|
32
|
+
self.columns = {
|
|
33
|
+
"thread": {
|
|
34
|
+
"id": "INTEGER",
|
|
35
|
+
"user": "TEXT",
|
|
36
|
+
"createdAt": "TIMESTAMP",
|
|
37
|
+
"updatedAt": "TIMESTAMP",
|
|
38
|
+
"title": "TEXT",
|
|
39
|
+
"systemPrompt": "TEXT",
|
|
40
|
+
"model": "TEXT",
|
|
41
|
+
"modelInfo": "JSON",
|
|
42
|
+
"modalities": "JSON",
|
|
43
|
+
"messages": "JSON",
|
|
44
|
+
"args": "JSON",
|
|
45
|
+
"tools": "JSON",
|
|
46
|
+
"toolHistory": "JSON",
|
|
47
|
+
"cost": "REAL",
|
|
48
|
+
"inputTokens": "INTEGER",
|
|
49
|
+
"outputTokens": "INTEGER",
|
|
50
|
+
"stats": "JSON",
|
|
51
|
+
"provider": "TEXT",
|
|
52
|
+
"providerModel": "TEXT",
|
|
53
|
+
"publishedAt": "TIMESTAMP",
|
|
54
|
+
"startedAt": "TIMESTAMP",
|
|
55
|
+
"completedAt": "TIMESTAMP",
|
|
56
|
+
"metadata": "JSON",
|
|
57
|
+
"error": "TEXT",
|
|
58
|
+
"ref": "TEXT",
|
|
59
|
+
"providerResponse": "JSON",
|
|
60
|
+
},
|
|
61
|
+
"request": {
|
|
62
|
+
"id": "INTEGER",
|
|
63
|
+
"user": "TEXT",
|
|
64
|
+
"threadId": "INTEGER",
|
|
65
|
+
"createdAt": "TIMESTAMP",
|
|
66
|
+
"updatedAt": "TIMESTAMP",
|
|
67
|
+
"title": "TEXT",
|
|
68
|
+
"model": "TEXT",
|
|
69
|
+
"duration": "INTEGER",
|
|
70
|
+
"cost": "REAL",
|
|
71
|
+
"inputPrice": "REAL",
|
|
72
|
+
"inputTokens": "INTEGER",
|
|
73
|
+
"inputCachedTokens": "INTEGER",
|
|
74
|
+
"outputPrice": "REAL",
|
|
75
|
+
"outputTokens": "INTEGER",
|
|
76
|
+
"totalTokens": "INTEGER",
|
|
77
|
+
"usage": "JSON",
|
|
78
|
+
"provider": "TEXT",
|
|
79
|
+
"providerModel": "TEXT",
|
|
80
|
+
"providerRef": "TEXT",
|
|
81
|
+
"finishReason": "TEXT",
|
|
82
|
+
"startedAt": "TIMESTAMP",
|
|
83
|
+
"completedAt": "TIMESTAMP",
|
|
84
|
+
"error": "TEXT",
|
|
85
|
+
"stackTrace": "TEXT",
|
|
86
|
+
"ref": "TEXT",
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
with self.create_writer_connection() as conn:
|
|
90
|
+
self.init_db(conn)
|
|
91
|
+
|
|
92
|
+
def get_connection(self):
|
|
93
|
+
return self.create_reader_connection()
|
|
94
|
+
|
|
95
|
+
def create_reader_connection(self):
|
|
96
|
+
return self.db.create_reader_connection()
|
|
97
|
+
|
|
98
|
+
def create_writer_connection(self):
|
|
99
|
+
return self.db.create_writer_connection()
|
|
100
|
+
|
|
101
|
+
# Check for missing columns and migrate if necessary
|
|
102
|
+
def add_missing_columns(self, conn, table):
|
|
103
|
+
cur = self.db.exec(conn, f"PRAGMA table_info({table})")
|
|
104
|
+
columns = {row[1] for row in cur.fetchall()}
|
|
105
|
+
|
|
106
|
+
for col, dtype in self.columns[table].items():
|
|
107
|
+
if col not in columns:
|
|
108
|
+
try:
|
|
109
|
+
self.db.exec(conn, f"ALTER TABLE {table} ADD COLUMN {col} {dtype}")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self.ctx.err(f"adding {table} column {col}", e)
|
|
112
|
+
|
|
113
|
+
def init_db(self, conn):
|
|
114
|
+
# Create table with all columns
|
|
115
|
+
# Note: default SQLite timestamp has different tz to datetime.now()
|
|
116
|
+
overrides = {
|
|
117
|
+
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
118
|
+
"createdAt": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
|
|
119
|
+
"updatedAt": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
|
|
120
|
+
}
|
|
121
|
+
sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns["thread"].items()])
|
|
122
|
+
self.db.exec(
|
|
123
|
+
conn,
|
|
124
|
+
f"""
|
|
125
|
+
CREATE TABLE IF NOT EXISTS thread (
|
|
126
|
+
{sql_columns}
|
|
127
|
+
)
|
|
128
|
+
""",
|
|
129
|
+
)
|
|
130
|
+
self.add_missing_columns(conn, "thread")
|
|
131
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_user ON thread(user)")
|
|
132
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_createdat ON thread(createdAt)")
|
|
133
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_updatedat ON thread(updatedAt)")
|
|
134
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_model ON thread(model)")
|
|
135
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_cost ON thread(cost)")
|
|
136
|
+
|
|
137
|
+
sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns["request"].items()])
|
|
138
|
+
self.db.exec(
|
|
139
|
+
conn,
|
|
140
|
+
f"""
|
|
141
|
+
CREATE TABLE IF NOT EXISTS request (
|
|
142
|
+
{sql_columns}
|
|
143
|
+
)
|
|
144
|
+
""",
|
|
145
|
+
)
|
|
146
|
+
self.add_missing_columns(conn, "request")
|
|
147
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_user ON request(user)")
|
|
148
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_createdat ON request(createdAt)")
|
|
149
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_cost ON request(cost)")
|
|
150
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_threadid ON request(threadId)")
|
|
151
|
+
|
|
152
|
+
def import_db(self, threads, requests):
|
|
153
|
+
self.ctx.log("import threads and requests")
|
|
154
|
+
with self.create_writer_connection() as conn:
|
|
155
|
+
conn.execute("DROP TABLE IF EXISTS thread")
|
|
156
|
+
conn.execute("DROP TABLE IF EXISTS request")
|
|
157
|
+
self.init_db(conn)
|
|
158
|
+
thread_id_map = {}
|
|
159
|
+
for thread in threads:
|
|
160
|
+
thread_id = self.import_thread(conn, thread)
|
|
161
|
+
thread_id_map[thread["id"]] = thread_id
|
|
162
|
+
self.ctx.log(f"imported {len(threads)} threads")
|
|
163
|
+
for request in requests:
|
|
164
|
+
self.import_request(conn, request, thread_id_map)
|
|
165
|
+
self.ctx.log(f"imported {len(requests)} requests")
|
|
166
|
+
|
|
167
|
+
def import_date(self, date):
|
|
168
|
+
# "1765794035" or "2025-12-31T05:41:46.686Z" or "2026-01-02 05:00:16"
|
|
169
|
+
str = date or datetime.now().isoformat()
|
|
170
|
+
if isinstance(str, int):
|
|
171
|
+
return datetime.fromtimestamp(str)
|
|
172
|
+
if isinstance(str, float):
|
|
173
|
+
return datetime.fromtimestamp(str)
|
|
174
|
+
return (
|
|
175
|
+
datetime.strptime(str, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
176
|
+
if "T" in str
|
|
177
|
+
else datetime.strptime(str, "%Y-%m-%d %H:%M:%S")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def import_thread(self, conn, orig):
|
|
181
|
+
thread = orig.copy()
|
|
182
|
+
thread["refId"] = thread["id"]
|
|
183
|
+
del thread["id"]
|
|
184
|
+
|
|
185
|
+
info = thread.get("modelInfo", thread.get("info", {}))
|
|
186
|
+
created_at = self.import_date(thread.get("createdAt"))
|
|
187
|
+
thread["createdAt"] = created_at
|
|
188
|
+
if "updateAt" not in thread:
|
|
189
|
+
thread["updateAt"] = created_at
|
|
190
|
+
thread["modelInfo"] = info
|
|
191
|
+
if "modalities" not in thread:
|
|
192
|
+
if "modalities" in info:
|
|
193
|
+
modalities = info["modalities"]
|
|
194
|
+
if isinstance(modalities, dict):
|
|
195
|
+
input = modalities.get("input", ["text"])
|
|
196
|
+
output = modalities.get("output", ["text"])
|
|
197
|
+
thread["modalities"] = list(set(input + output))
|
|
198
|
+
else:
|
|
199
|
+
thread["modalities"] = modalities
|
|
200
|
+
else:
|
|
201
|
+
thread["modalities"] = ["text"]
|
|
202
|
+
if "provider" not in thread and "provider" in info:
|
|
203
|
+
thread["provider"] = info["provider"]
|
|
204
|
+
if "providerModel" not in thread and "id" in info:
|
|
205
|
+
thread["providerModel"] = info["id"]
|
|
206
|
+
|
|
207
|
+
stats = thread.get("stats", {})
|
|
208
|
+
if "inputTokens" not in thread and "inputTokens" in stats:
|
|
209
|
+
thread["inputTokens"] = stats["inputTokens"]
|
|
210
|
+
if "outputTokens" not in thread and "outputTokens" in stats:
|
|
211
|
+
thread["outputTokens"] = stats["outputTokens"]
|
|
212
|
+
if "cost" not in thread and "cost" in stats:
|
|
213
|
+
thread["cost"] = stats["cost"]
|
|
214
|
+
if "completedAt" not in thread:
|
|
215
|
+
thread["completedAt"] = created_at + timedelta(milliseconds=stats.get("duration", 0))
|
|
216
|
+
|
|
217
|
+
sql_columns = []
|
|
218
|
+
sql_params = []
|
|
219
|
+
columns = self.columns["thread"]
|
|
220
|
+
for col in columns:
|
|
221
|
+
if col == "id":
|
|
222
|
+
continue
|
|
223
|
+
sql_columns.append(col)
|
|
224
|
+
val = thread.get(col, None)
|
|
225
|
+
if columns[col] == "JSON" and val is not None:
|
|
226
|
+
val = json.dumps(val)
|
|
227
|
+
sql_params.append(val)
|
|
228
|
+
|
|
229
|
+
return conn.execute(
|
|
230
|
+
f"INSERT INTO thread ({', '.join(sql_columns)}) VALUES ({', '.join(['?'] * len(sql_params))})",
|
|
231
|
+
sql_params,
|
|
232
|
+
).lastrowid
|
|
233
|
+
|
|
234
|
+
# run on startup
|
|
235
|
+
def import_request(self, conn, orig, id_map):
|
|
236
|
+
request = orig.copy()
|
|
237
|
+
del request["id"]
|
|
238
|
+
thread_id = request.get("threadId")
|
|
239
|
+
if thread_id:
|
|
240
|
+
request["threadId"] = id_map.get(thread_id, None)
|
|
241
|
+
|
|
242
|
+
created_at = self.import_date(request.get("created"))
|
|
243
|
+
request["createdAt"] = created_at
|
|
244
|
+
if "updateAt" not in request:
|
|
245
|
+
request["updateAt"] = created_at
|
|
246
|
+
if "completedAt" not in request:
|
|
247
|
+
request["completedAt"] = created_at + timedelta(milliseconds=request.get("duration", 0))
|
|
248
|
+
|
|
249
|
+
sql_columns = []
|
|
250
|
+
sql_params = []
|
|
251
|
+
columns = self.columns["request"]
|
|
252
|
+
for col in columns:
|
|
253
|
+
if col == "id":
|
|
254
|
+
continue
|
|
255
|
+
sql_columns.append(col)
|
|
256
|
+
val = request.get(col, None)
|
|
257
|
+
if columns[col] == "JSON" and val is not None:
|
|
258
|
+
val = json.dumps(val)
|
|
259
|
+
sql_params.append(val)
|
|
260
|
+
|
|
261
|
+
return conn.execute(
|
|
262
|
+
f"INSERT INTO request ({', '.join(sql_columns)}) VALUES ({', '.join(['?'] * len(sql_params))})",
|
|
263
|
+
sql_params,
|
|
264
|
+
).lastrowid
|
|
265
|
+
|
|
266
|
+
def to_dto(self, row, json_columns):
|
|
267
|
+
return to_dto(self.ctx, row, json_columns)
|
|
268
|
+
|
|
269
|
+
def get_user_filter(self, user=None, params=None):
|
|
270
|
+
if user is None:
|
|
271
|
+
return "WHERE user IS NULL", params or {}
|
|
272
|
+
else:
|
|
273
|
+
args = params.copy() if params else {}
|
|
274
|
+
args.update({"user": user})
|
|
275
|
+
return "WHERE user = :user", args
|
|
276
|
+
|
|
277
|
+
def get_thread(self, id, user=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)
|
|
280
|
+
|
|
281
|
+
def get_thread_column(self, id, column, user=None):
|
|
282
|
+
if column not in self.columns["thread"]:
|
|
283
|
+
self.ctx.err(f"get_thread_column invalid column ({id}, {column}, {user})", None)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
sql_where, params = self.get_user_filter(user, {"id": id})
|
|
288
|
+
return self.db.scalar(f"SELECT {column} FROM thread {sql_where} AND id = :id", params)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.ctx.err(f"get_thread_column ({id}, {column}, {user})", e)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def query_threads(self, query: Dict[str, Any], user=None):
|
|
294
|
+
try:
|
|
295
|
+
columns = self.columns["thread"]
|
|
296
|
+
all_columns = columns.keys()
|
|
297
|
+
|
|
298
|
+
take = min(int(query.get("take", "50")), 1000)
|
|
299
|
+
skip = int(query.get("skip", "0"))
|
|
300
|
+
sort = query.get("sort", "-id")
|
|
301
|
+
|
|
302
|
+
# always filter by user
|
|
303
|
+
sql_where, params = self.get_user_filter(user, {"take": take, "skip": skip})
|
|
304
|
+
|
|
305
|
+
filter = {}
|
|
306
|
+
for k in query:
|
|
307
|
+
if k in all_columns:
|
|
308
|
+
filter[k] = query[k]
|
|
309
|
+
params[k] = query[k]
|
|
310
|
+
|
|
311
|
+
if len(filter) > 0:
|
|
312
|
+
sql_where += " AND " + " AND ".join([f"{k} = :{k}" for k in filter])
|
|
313
|
+
|
|
314
|
+
if "null" in query:
|
|
315
|
+
cols = valid_columns(all_columns, query["null"])
|
|
316
|
+
if len(cols) > 0:
|
|
317
|
+
sql_where += " AND " + " AND ".join([f"{k} IS NULL" for k in cols])
|
|
318
|
+
|
|
319
|
+
if "not_null" in query:
|
|
320
|
+
cols = valid_columns(all_columns, query.get("not_null"))
|
|
321
|
+
if len(cols) > 0:
|
|
322
|
+
sql_where += " AND " + " AND ".join([f"{k} IS NOT NULL" for k in cols])
|
|
323
|
+
|
|
324
|
+
if "q" in query:
|
|
325
|
+
sql_where += " AND " if sql_where else "WHERE "
|
|
326
|
+
sql_where += "(title LIKE :q OR messages LIKE :q)"
|
|
327
|
+
params["q"] = f"%{query['q']}%"
|
|
328
|
+
|
|
329
|
+
sql = f"{select_columns(all_columns, query.get('fields'), select=query.get('select'))} FROM thread {sql_where} {order_by(all_columns, sort)} LIMIT :take OFFSET :skip"
|
|
330
|
+
|
|
331
|
+
if query.get("as") == "column":
|
|
332
|
+
return self.db.column(sql, params)
|
|
333
|
+
else:
|
|
334
|
+
return self.db.all(sql, params)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.ctx.err(f"query_threads ({take}, {skip})", e)
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
def prepare_thread(self, thread, id=None, user=None):
|
|
341
|
+
now = datetime.now()
|
|
342
|
+
if id:
|
|
343
|
+
thread["id"] = id
|
|
344
|
+
else:
|
|
345
|
+
thread["createdAt"] = now
|
|
346
|
+
thread["updatedAt"] = now
|
|
347
|
+
if "messages" in thread:
|
|
348
|
+
for m in thread["messages"]:
|
|
349
|
+
self.ctx.cache_message_inline_data(m)
|
|
350
|
+
return with_user(thread, user=user)
|
|
351
|
+
|
|
352
|
+
def create_thread(self, thread: Dict[str, Any], user=None):
|
|
353
|
+
return self.db.insert("thread", self.columns["thread"], self.prepare_thread(thread, user=user))
|
|
354
|
+
|
|
355
|
+
async def create_thread_async(self, thread: Dict[str, Any], user=None):
|
|
356
|
+
return await self.db.insert_async("thread", self.columns["thread"], self.prepare_thread(thread, user=user))
|
|
357
|
+
|
|
358
|
+
def update_thread(self, id, thread: Dict[str, Any], user=None):
|
|
359
|
+
return self.db.update("thread", self.columns["thread"], self.prepare_thread(thread, id, user=user))
|
|
360
|
+
|
|
361
|
+
async def update_thread_async(self, id, thread: Dict[str, Any], user=None):
|
|
362
|
+
return await self.db.update_async("thread", self.columns["thread"], self.prepare_thread(thread, id, user=user))
|
|
363
|
+
|
|
364
|
+
def delete_thread(self, id, user=None, callback=None):
|
|
365
|
+
sql_where, params = self.get_user_filter(user, {"id": id})
|
|
366
|
+
self.db.write(f"DELETE FROM thread {sql_where} AND id = :id", params, callback)
|
|
367
|
+
|
|
368
|
+
def query_requests(self, query: Dict[str, Any], user=None):
|
|
369
|
+
try:
|
|
370
|
+
columns = self.columns["request"]
|
|
371
|
+
all_columns = columns.keys()
|
|
372
|
+
|
|
373
|
+
take = min(int(query.get("take", "50")), 10000)
|
|
374
|
+
skip = int(query.get("skip", 0))
|
|
375
|
+
sort = query.get("sort", "-id")
|
|
376
|
+
|
|
377
|
+
# always filter by user
|
|
378
|
+
sql_where, params = self.get_user_filter(user, {"take": take, "skip": skip})
|
|
379
|
+
|
|
380
|
+
filter = {}
|
|
381
|
+
for k in query:
|
|
382
|
+
if k in all_columns:
|
|
383
|
+
filter[k] = query[k]
|
|
384
|
+
params[k] = query[k]
|
|
385
|
+
|
|
386
|
+
if len(filter) > 0:
|
|
387
|
+
sql_where += " AND " + " AND ".join([f"{k} = :{k}" for k in filter])
|
|
388
|
+
|
|
389
|
+
if "null" in query:
|
|
390
|
+
cols = valid_columns(all_columns, query["null"])
|
|
391
|
+
if len(cols) > 0:
|
|
392
|
+
sql_where += " AND " + " AND ".join([f"{k} IS NULL" for k in cols])
|
|
393
|
+
|
|
394
|
+
if "not_null" in query:
|
|
395
|
+
cols = valid_columns(all_columns, query.get("not_null"))
|
|
396
|
+
if len(cols) > 0:
|
|
397
|
+
sql_where += " AND " + " AND ".join([f"{k} IS NOT NULL" for k in cols])
|
|
398
|
+
|
|
399
|
+
if "q" in query:
|
|
400
|
+
sql_where += " AND " if sql_where else "WHERE "
|
|
401
|
+
sql_where += "(title LIKE :q)"
|
|
402
|
+
params["q"] = f"%{query['q']}%"
|
|
403
|
+
|
|
404
|
+
if "month" in query:
|
|
405
|
+
sql_where += " AND strftime('%Y-%m', createdAt) = :month"
|
|
406
|
+
params["month"] = query["month"]
|
|
407
|
+
|
|
408
|
+
sql = f"{select_columns(all_columns, query.get('fields'), select=query.get('select'))} FROM request {sql_where} {order_by(all_columns, sort)}LIMIT :take OFFSET :skip"
|
|
409
|
+
|
|
410
|
+
if query.get("as") == "column":
|
|
411
|
+
return self.db.column(sql, params)
|
|
412
|
+
else:
|
|
413
|
+
return self.db.all(sql, params)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
self.ctx.err(f"query_requests ({take}, {skip})", e)
|
|
416
|
+
return []
|
|
417
|
+
|
|
418
|
+
def get_request_summary(self, user=None):
|
|
419
|
+
try:
|
|
420
|
+
sql_where, params = self.get_user_filter(user)
|
|
421
|
+
# Use strftime to format date as YYYY-MM-DD
|
|
422
|
+
sql = f"""
|
|
423
|
+
SELECT
|
|
424
|
+
strftime('%Y-%m-%d', createdAt) as date,
|
|
425
|
+
count(id) as requests,
|
|
426
|
+
sum(cost) as cost,
|
|
427
|
+
sum(inputTokens) as inputTokens,
|
|
428
|
+
sum(outputTokens) as outputTokens
|
|
429
|
+
FROM request
|
|
430
|
+
{sql_where}
|
|
431
|
+
GROUP BY date
|
|
432
|
+
ORDER BY date
|
|
433
|
+
"""
|
|
434
|
+
return self.db.all(sql, params)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
self.ctx.err(f"get_request_summary ({user})", e)
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
def get_daily_request_summary(self, day, user=None):
|
|
440
|
+
try:
|
|
441
|
+
sql_where, params = self.get_user_filter(user)
|
|
442
|
+
# Add date filter
|
|
443
|
+
sql_where += " AND strftime('%Y-%m-%d', createdAt) = :day"
|
|
444
|
+
params["day"] = day
|
|
445
|
+
|
|
446
|
+
# Model aggregation
|
|
447
|
+
sql_model = f"""
|
|
448
|
+
SELECT
|
|
449
|
+
model,
|
|
450
|
+
count(id) as count,
|
|
451
|
+
sum(cost) as cost,
|
|
452
|
+
sum(duration) as duration,
|
|
453
|
+
sum(inputTokens + outputTokens) as tokens,
|
|
454
|
+
sum(inputTokens) as inputTokens,
|
|
455
|
+
sum(outputTokens) as outputTokens
|
|
456
|
+
FROM request
|
|
457
|
+
{sql_where}
|
|
458
|
+
GROUP BY model
|
|
459
|
+
"""
|
|
460
|
+
model_data = {}
|
|
461
|
+
for row in self.db.all(sql_model, params):
|
|
462
|
+
model_data[row["model"]] = {
|
|
463
|
+
"cost": row["cost"] or 0,
|
|
464
|
+
"count": row["count"],
|
|
465
|
+
"duration": row["duration"] or 0,
|
|
466
|
+
"tokens": row["tokens"] or 0,
|
|
467
|
+
"inputTokens": row["inputTokens"] or 0,
|
|
468
|
+
"outputTokens": row["outputTokens"] or 0,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
# Provider aggregation
|
|
472
|
+
sql_provider = f"""
|
|
473
|
+
SELECT
|
|
474
|
+
provider,
|
|
475
|
+
count(id) as count,
|
|
476
|
+
sum(cost) as cost,
|
|
477
|
+
sum(duration) as duration,
|
|
478
|
+
sum(inputTokens + outputTokens) as tokens,
|
|
479
|
+
sum(inputTokens) as inputTokens,
|
|
480
|
+
sum(outputTokens) as outputTokens
|
|
481
|
+
FROM request
|
|
482
|
+
{sql_where}
|
|
483
|
+
AND provider IS NOT NULL
|
|
484
|
+
GROUP BY provider
|
|
485
|
+
"""
|
|
486
|
+
provider_data = {}
|
|
487
|
+
for row in self.db.all(sql_provider, params):
|
|
488
|
+
provider_data[row["provider"]] = {
|
|
489
|
+
"cost": row["cost"] or 0,
|
|
490
|
+
"count": row["count"],
|
|
491
|
+
"duration": row["duration"] or 0,
|
|
492
|
+
"tokens": row["tokens"] or 0,
|
|
493
|
+
"inputTokens": row["inputTokens"] or 0,
|
|
494
|
+
"outputTokens": row["outputTokens"] or 0,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {"modelData": model_data, "providerData": provider_data}
|
|
498
|
+
except Exception as e:
|
|
499
|
+
self.ctx.err(f"get_daily_request_summary ({day}, {user})", e)
|
|
500
|
+
return {"modelData": {}, "providerData": {}}
|
|
501
|
+
|
|
502
|
+
def create_request(self, request: Dict[str, Any], user=None):
|
|
503
|
+
request["createdAt"] = request["updatedAt"] = datetime.now()
|
|
504
|
+
return self.db.insert("request", self.columns["request"], with_user(request, user=user))
|
|
505
|
+
|
|
506
|
+
async def create_request_async(self, request: Dict[str, Any], user=None):
|
|
507
|
+
request["createdAt"] = request["updatedAt"] = datetime.now()
|
|
508
|
+
return await self.db.insert_async("request", self.columns["request"], with_user(request, user=user))
|
|
509
|
+
|
|
510
|
+
def update_request(self, id, request: Dict[str, Any], user=None):
|
|
511
|
+
request["id"] = id
|
|
512
|
+
request["updatedAt"] = datetime.now()
|
|
513
|
+
return self.db.update("request", self.columns["request"], with_user(request, user=user))
|
|
514
|
+
|
|
515
|
+
async def update_request_async(self, id, request: Dict[str, Any], user=None):
|
|
516
|
+
request["id"] = id
|
|
517
|
+
request["updatedAt"] = datetime.now()
|
|
518
|
+
return await self.db.update_async("request", self.columns["request"], with_user(request, user=user))
|
|
519
|
+
|
|
520
|
+
def delete_request(self, id, user=None, callback=None):
|
|
521
|
+
sql_where, params = self.get_user_filter(user, {"id": id})
|
|
522
|
+
self.db.write(f"DELETE FROM request {sql_where} AND id = :id", params, callback)
|
|
523
|
+
|
|
524
|
+
def close(self):
|
|
525
|
+
self.db.close()
|
|
526
|
+
|
|
527
|
+
# complete all in progress tasks
|
|
528
|
+
with self.db.create_writer_connection() as conn:
|
|
529
|
+
conn.execute(
|
|
530
|
+
"UPDATE thread SET completedAt = :completedAt, error = :error WHERE completedAt IS NULL",
|
|
531
|
+
{"completedAt": datetime.now(), "error": "Server Shutdown"},
|
|
532
|
+
)
|
|
533
|
+
conn.execute(
|
|
534
|
+
"UPDATE request SET completedAt = :completedAt, error = :error WHERE completedAt IS NULL",
|
|
535
|
+
{"completedAt": datetime.now(), "error": "Server Shutdown"},
|
|
536
|
+
)
|