llms-py 3.0.0__py3-none-any.whl → 3.0.0b1__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 (206) hide show
  1. llms/index.html +77 -35
  2. llms/llms.json +23 -72
  3. llms/main.py +732 -1786
  4. llms/providers.json +1 -1
  5. llms/{extensions/analytics/ui/index.mjs → ui/Analytics.mjs} +238 -154
  6. llms/ui/App.mjs +60 -151
  7. llms/ui/Avatar.mjs +85 -0
  8. llms/ui/Brand.mjs +52 -0
  9. llms/ui/ChatPrompt.mjs +606 -0
  10. llms/ui/Main.mjs +873 -0
  11. llms/ui/ModelSelector.mjs +693 -0
  12. llms/ui/OAuthSignIn.mjs +92 -0
  13. llms/ui/ProviderIcon.mjs +36 -0
  14. llms/ui/ProviderStatus.mjs +105 -0
  15. llms/{extensions/app/ui → ui}/Recents.mjs +65 -91
  16. llms/ui/{modules/chat/SettingsDialog.mjs → SettingsDialog.mjs} +9 -9
  17. llms/{extensions/app/ui/index.mjs → ui/Sidebar.mjs} +58 -124
  18. llms/ui/SignIn.mjs +64 -0
  19. llms/ui/SystemPromptEditor.mjs +31 -0
  20. llms/ui/SystemPromptSelector.mjs +56 -0
  21. llms/ui/Welcome.mjs +8 -0
  22. llms/ui/ai.mjs +53 -125
  23. llms/ui/app.css +111 -1837
  24. llms/ui/lib/charts.mjs +13 -9
  25. llms/ui/lib/servicestack-vue.mjs +3 -3
  26. llms/ui/lib/vue.min.mjs +9 -10
  27. llms/ui/lib/vue.mjs +1602 -1763
  28. llms/ui/markdown.mjs +2 -10
  29. llms/ui/tailwind.input.css +80 -496
  30. llms/ui/threadStore.mjs +572 -0
  31. llms/ui/utils.mjs +117 -113
  32. llms/ui.json +1069 -0
  33. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/METADATA +1 -1
  34. llms_py-3.0.0b1.dist-info/RECORD +49 -0
  35. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  36. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  37. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  38. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  39. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  40. llms/__pycache__/llms.cpython-312.pyc +0 -0
  41. llms/__pycache__/main.cpython-312.pyc +0 -0
  42. llms/__pycache__/main.cpython-313.pyc +0 -0
  43. llms/__pycache__/main.cpython-314.pyc +0 -0
  44. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  45. llms/extensions/app/README.md +0 -20
  46. llms/extensions/app/__init__.py +0 -530
  47. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  48. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  49. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  50. llms/extensions/app/db.py +0 -644
  51. llms/extensions/app/db_manager.py +0 -195
  52. llms/extensions/app/requests.json +0 -9073
  53. llms/extensions/app/threads.json +0 -15290
  54. llms/extensions/app/ui/threadStore.mjs +0 -411
  55. llms/extensions/core_tools/CALCULATOR.md +0 -32
  56. llms/extensions/core_tools/__init__.py +0 -598
  57. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  58. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +0 -201
  59. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +0 -185
  60. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +0 -101
  61. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +0 -160
  62. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +0 -66
  63. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +0 -27
  64. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +0 -72
  65. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +0 -119
  66. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +0 -98
  67. llms/extensions/core_tools/ui/codemirror/doc/docs.css +0 -225
  68. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  69. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +0 -344
  70. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +0 -9884
  71. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +0 -942
  72. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +0 -118
  73. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +0 -962
  74. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +0 -62
  75. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +0 -402
  76. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +0 -40
  77. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +0 -135
  78. llms/extensions/core_tools/ui/index.mjs +0 -650
  79. llms/extensions/gallery/README.md +0 -61
  80. llms/extensions/gallery/__init__.py +0 -61
  81. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  82. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  83. llms/extensions/gallery/db.py +0 -298
  84. llms/extensions/gallery/ui/index.mjs +0 -482
  85. llms/extensions/katex/README.md +0 -39
  86. llms/extensions/katex/__init__.py +0 -6
  87. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  88. llms/extensions/katex/ui/README.md +0 -125
  89. llms/extensions/katex/ui/contrib/auto-render.js +0 -338
  90. llms/extensions/katex/ui/contrib/auto-render.min.js +0 -1
  91. llms/extensions/katex/ui/contrib/auto-render.mjs +0 -244
  92. llms/extensions/katex/ui/contrib/copy-tex.js +0 -127
  93. llms/extensions/katex/ui/contrib/copy-tex.min.js +0 -1
  94. llms/extensions/katex/ui/contrib/copy-tex.mjs +0 -105
  95. llms/extensions/katex/ui/contrib/mathtex-script-type.js +0 -109
  96. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +0 -1
  97. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +0 -24
  98. llms/extensions/katex/ui/contrib/mhchem.js +0 -3213
  99. llms/extensions/katex/ui/contrib/mhchem.min.js +0 -1
  100. llms/extensions/katex/ui/contrib/mhchem.mjs +0 -3109
  101. llms/extensions/katex/ui/contrib/render-a11y-string.js +0 -887
  102. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +0 -1
  103. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +0 -800
  104. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  133. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  134. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  135. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  136. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  137. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  138. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  139. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  140. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  141. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  142. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  143. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  144. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  145. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  146. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  147. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  148. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  149. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  150. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  151. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  152. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  153. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  154. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  155. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  156. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  157. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  158. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  159. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  160. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  161. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  162. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  163. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  164. llms/extensions/katex/ui/index.mjs +0 -92
  165. llms/extensions/katex/ui/katex-swap.css +0 -1230
  166. llms/extensions/katex/ui/katex-swap.min.css +0 -1
  167. llms/extensions/katex/ui/katex.css +0 -1230
  168. llms/extensions/katex/ui/katex.js +0 -19080
  169. llms/extensions/katex/ui/katex.min.css +0 -1
  170. llms/extensions/katex/ui/katex.min.js +0 -1
  171. llms/extensions/katex/ui/katex.min.mjs +0 -1
  172. llms/extensions/katex/ui/katex.mjs +0 -18547
  173. llms/extensions/providers/__init__.py +0 -18
  174. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  175. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  176. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  177. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  178. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  179. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  180. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  181. llms/extensions/providers/anthropic.py +0 -229
  182. llms/extensions/providers/chutes.py +0 -155
  183. llms/extensions/providers/google.py +0 -378
  184. llms/extensions/providers/nvidia.py +0 -105
  185. llms/extensions/providers/openai.py +0 -156
  186. llms/extensions/providers/openrouter.py +0 -72
  187. llms/extensions/system_prompts/README.md +0 -22
  188. llms/extensions/system_prompts/__init__.py +0 -45
  189. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  190. llms/extensions/system_prompts/ui/index.mjs +0 -280
  191. llms/extensions/system_prompts/ui/prompts.json +0 -1067
  192. llms/extensions/tools/__init__.py +0 -5
  193. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  194. llms/extensions/tools/ui/index.mjs +0 -204
  195. llms/providers-extra.json +0 -356
  196. llms/ui/ctx.mjs +0 -365
  197. llms/ui/index.mjs +0 -129
  198. llms/ui/modules/chat/ChatBody.mjs +0 -691
  199. llms/ui/modules/chat/index.mjs +0 -828
  200. llms/ui/modules/layout.mjs +0 -243
  201. llms/ui/modules/model-selector.mjs +0 -851
  202. llms_py-3.0.0.dist-info/RECORD +0 -202
  203. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/WHEEL +0 -0
  204. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/entry_points.txt +0 -0
  205. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  206. {llms_py-3.0.0.dist-info → llms_py-3.0.0b1.dist-info}/top_level.txt +0 -0
llms/extensions/app/db.py DELETED
@@ -1,644 +0,0 @@
1
- import json
2
- import os
3
- import threading
4
- from datetime import datetime, timedelta
5
- from typing import Any, Dict
6
-
7
- from .db_manager import DbManager
8
-
9
-
10
- def with_user(data, user):
11
- if user is None:
12
- if "user" in data:
13
- del data["user"]
14
- return data
15
- else:
16
- data["user"] = user
17
- return data
18
-
19
-
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
- class AppDB:
59
- def __init__(self, ctx, db_path):
60
- if db_path is None:
61
- raise ValueError("db_path is required")
62
-
63
- self.ctx = ctx
64
- self.db_path = str(db_path)
65
-
66
- dirname = os.path.dirname(self.db_path)
67
- if dirname:
68
- os.makedirs(dirname, exist_ok=True)
69
-
70
- self.db = DbManager(ctx, self.db_path)
71
- self.columns = {
72
- "thread": {
73
- "id": "INTEGER",
74
- "user": "TEXT",
75
- "createdAt": "TIMESTAMP",
76
- "updatedAt": "TIMESTAMP",
77
- "title": "TEXT",
78
- "systemPrompt": "TEXT",
79
- "model": "TEXT",
80
- "modelInfo": "JSON",
81
- "modalities": "JSON",
82
- "messages": "JSON",
83
- "args": "JSON",
84
- "toolHistory": "JSON",
85
- "cost": "REAL",
86
- "inputTokens": "INTEGER",
87
- "outputTokens": "INTEGER",
88
- "stats": "JSON",
89
- "provider": "TEXT",
90
- "providerModel": "TEXT",
91
- "publishedAt": "TIMESTAMP",
92
- "startedAt": "TIMESTAMP",
93
- "completedAt": "TIMESTAMP",
94
- "metadata": "JSON",
95
- "error": "TEXT",
96
- "ref": "TEXT",
97
- },
98
- "request": {
99
- "id": "INTEGER",
100
- "user": "TEXT",
101
- "threadId": "INTEGER",
102
- "createdAt": "TIMESTAMP",
103
- "updatedAt": "TIMESTAMP",
104
- "title": "TEXT",
105
- "model": "TEXT",
106
- "duration": "INTEGER",
107
- "cost": "REAL",
108
- "inputPrice": "REAL",
109
- "inputTokens": "INTEGER",
110
- "inputCachedTokens": "INTEGER",
111
- "outputPrice": "REAL",
112
- "outputTokens": "INTEGER",
113
- "totalTokens": "INTEGER",
114
- "usage": "JSON",
115
- "provider": "TEXT",
116
- "providerModel": "TEXT",
117
- "providerRef": "TEXT",
118
- "finishReason": "TEXT",
119
- "startedAt": "TIMESTAMP",
120
- "completedAt": "TIMESTAMP",
121
- "error": "TEXT",
122
- "stackTrace": "TEXT",
123
- "ref": "TEXT",
124
- },
125
- }
126
- with self.create_writer_connection() as conn:
127
- self.init_db(conn)
128
-
129
- def get_connection(self):
130
- return self.create_reader_connection()
131
-
132
- def create_reader_connection(self):
133
- return self.db.create_reader_connection()
134
-
135
- def create_writer_connection(self):
136
- return self.db.create_writer_connection()
137
-
138
- # Check for missing columns and migrate if necessary
139
- def add_missing_columns(self, conn, table):
140
- cur = self.db.exec(conn, f"PRAGMA table_info({table})")
141
- columns = {row[1] for row in cur.fetchall()}
142
-
143
- for col, dtype in self.columns[table].items():
144
- if col not in columns:
145
- try:
146
- self.db.exec(conn, f"ALTER TABLE {table} ADD COLUMN {col} {dtype}")
147
- except Exception as e:
148
- self.ctx.err(f"adding {table} column {col}", e)
149
-
150
- def init_db(self, conn):
151
- # Create table with all columns
152
- # Note: default SQLite timestamp has different tz to datetime.now()
153
- overrides = {
154
- "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
155
- "createdAt": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
156
- "updatedAt": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
157
- }
158
- sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns["thread"].items()])
159
- self.db.exec(
160
- conn,
161
- f"""
162
- CREATE TABLE IF NOT EXISTS thread (
163
- {sql_columns}
164
- )
165
- """,
166
- )
167
- self.add_missing_columns(conn, "thread")
168
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_user ON thread(user)")
169
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_createdat ON thread(createdAt)")
170
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_updatedat ON thread(updatedAt)")
171
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_model ON thread(model)")
172
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_thread_cost ON thread(cost)")
173
-
174
- sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns["request"].items()])
175
- self.db.exec(
176
- conn,
177
- f"""
178
- CREATE TABLE IF NOT EXISTS request (
179
- {sql_columns}
180
- )
181
- """,
182
- )
183
- self.add_missing_columns(conn, "request")
184
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_user ON request(user)")
185
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_createdat ON request(createdAt)")
186
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_cost ON request(cost)")
187
- self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_request_threadid ON request(threadId)")
188
-
189
- def import_db(self, threads, requests):
190
- self.ctx.log("import threads and requests")
191
- with self.create_writer_connection() as conn:
192
- conn.execute("DROP TABLE IF EXISTS thread")
193
- conn.execute("DROP TABLE IF EXISTS request")
194
- self.init_db(conn)
195
- thread_id_map = {}
196
- for thread in threads:
197
- thread_id = self.import_thread(conn, thread)
198
- thread_id_map[thread["id"]] = thread_id
199
- self.ctx.log(f"imported {len(threads)} threads")
200
- for request in requests:
201
- self.import_request(conn, request, thread_id_map)
202
- self.ctx.log(f"imported {len(requests)} requests")
203
-
204
- def import_date(self, date):
205
- # "1765794035" or "2025-12-31T05:41:46.686Z" or "2026-01-02 05:00:16"
206
- str = date or datetime.now().isoformat()
207
- if isinstance(str, int):
208
- return datetime.fromtimestamp(str)
209
- if isinstance(str, float):
210
- return datetime.fromtimestamp(str)
211
- return (
212
- datetime.strptime(str, "%Y-%m-%dT%H:%M:%S.%fZ")
213
- if "T" in str
214
- else datetime.strptime(str, "%Y-%m-%d %H:%M:%S")
215
- )
216
-
217
- def import_thread(self, conn, orig):
218
- thread = orig.copy()
219
- thread["refId"] = thread["id"]
220
- del thread["id"]
221
-
222
- info = thread.get("modelInfo", thread.get("info", {}))
223
- created_at = self.import_date(thread.get("createdAt"))
224
- thread["createdAt"] = created_at
225
- if "updateAt" not in thread:
226
- thread["updateAt"] = created_at
227
- thread["modelInfo"] = info
228
- if "modalities" not in thread:
229
- if "modalities" in info:
230
- modalities = info["modalities"]
231
- if isinstance(modalities, dict):
232
- input = modalities.get("input", ["text"])
233
- output = modalities.get("output", ["text"])
234
- thread["modalities"] = list(set(input + output))
235
- else:
236
- thread["modalities"] = modalities
237
- else:
238
- thread["modalities"] = ["text"]
239
- if "provider" not in thread and "provider" in info:
240
- thread["provider"] = info["provider"]
241
- if "providerModel" not in thread and "id" in info:
242
- thread["providerModel"] = info["id"]
243
-
244
- stats = thread.get("stats", {})
245
- if "inputTokens" not in thread and "inputTokens" in stats:
246
- thread["inputTokens"] = stats["inputTokens"]
247
- if "outputTokens" not in thread and "outputTokens" in stats:
248
- thread["outputTokens"] = stats["outputTokens"]
249
- if "cost" not in thread and "cost" in stats:
250
- thread["cost"] = stats["cost"]
251
- if "completedAt" not in thread:
252
- thread["completedAt"] = created_at + timedelta(milliseconds=stats.get("duration", 0))
253
-
254
- sql_columns = []
255
- sql_params = []
256
- columns = self.columns["thread"]
257
- for col in columns:
258
- if col == "id":
259
- continue
260
- sql_columns.append(col)
261
- val = thread.get(col, None)
262
- if columns[col] == "JSON" and val is not None:
263
- val = json.dumps(val)
264
- sql_params.append(val)
265
-
266
- return conn.execute(
267
- f"INSERT INTO thread ({', '.join(sql_columns)}) VALUES ({', '.join(['?'] * len(sql_params))})",
268
- sql_params,
269
- ).lastrowid
270
-
271
- # run on startup
272
- def import_request(self, conn, orig, id_map):
273
- request = orig.copy()
274
- del request["id"]
275
- thread_id = request.get("threadId")
276
- if thread_id:
277
- request["threadId"] = id_map.get(thread_id, None)
278
-
279
- created_at = self.import_date(request.get("created"))
280
- request["createdAt"] = created_at
281
- if "updateAt" not in request:
282
- request["updateAt"] = created_at
283
- if "completedAt" not in request:
284
- request["completedAt"] = created_at + timedelta(milliseconds=request.get("duration", 0))
285
-
286
- sql_columns = []
287
- sql_params = []
288
- columns = self.columns["request"]
289
- for col in columns:
290
- if col == "id":
291
- continue
292
- sql_columns.append(col)
293
- val = request.get(col, None)
294
- if columns[col] == "JSON" and val is not None:
295
- val = json.dumps(val)
296
- sql_params.append(val)
297
-
298
- return conn.execute(
299
- f"INSERT INTO request ({', '.join(sql_columns)}) VALUES ({', '.join(['?'] * len(sql_params))})",
300
- sql_params,
301
- ).lastrowid
302
-
303
- def get_user_filter(self, user=None, params=None):
304
- if user is None:
305
- return "WHERE user IS NULL", params or {}
306
- else:
307
- args = params.copy() if params else {}
308
- args.update({"user": user})
309
- return "WHERE user = :user", args
310
-
311
- 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
318
-
319
- def get_thread_column(self, id, column, user=None):
320
- if column not in self.columns["thread"]:
321
- self.ctx.err(f"get_thread_column invalid column ({id}, {column}, {user})", None)
322
- return None
323
-
324
- try:
325
- sql_where, params = self.get_user_filter(user, {"id": id})
326
- return self.db.scalar(f"SELECT {column} FROM thread {sql_where} AND id = :id", params)
327
- except Exception as e:
328
- self.ctx.err(f"get_thread_column ({id}, {column}, {user})", e)
329
- return None
330
-
331
- def query_threads(self, query: Dict[str, Any], user=None):
332
- try:
333
- columns = self.columns["thread"]
334
- all_columns = columns.keys()
335
-
336
- take = min(int(query.get("take", "50")), 1000)
337
- skip = int(query.get("skip", "0"))
338
- sort = query.get("sort", "-id")
339
-
340
- # always filter by user
341
- sql_where, params = self.get_user_filter(user, {"take": take, "skip": skip})
342
-
343
- filter = {}
344
- for k in query:
345
- if k in all_columns:
346
- filter[k] = query[k]
347
- params[k] = query[k]
348
-
349
- if len(filter) > 0:
350
- sql_where += " AND " + " AND ".join([f"{k} = :{k}" for k in filter])
351
-
352
- if "null" in query:
353
- cols = valid_columns(all_columns, query["null"])
354
- if len(cols) > 0:
355
- sql_where += " AND " + " AND ".join([f"{k} IS NULL" for k in cols])
356
-
357
- if "not_null" in query:
358
- cols = valid_columns(all_columns, query.get("not_null"))
359
- if len(cols) > 0:
360
- sql_where += " AND " + " AND ".join([f"{k} IS NOT NULL" for k in cols])
361
-
362
- if "q" in query:
363
- sql_where += " AND " if sql_where else "WHERE "
364
- sql_where += "(title LIKE :q OR messages LIKE :q)"
365
- params["q"] = f"%{query['q']}%"
366
-
367
- 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"
368
-
369
- if query.get("as") == "column":
370
- return self.db.column(sql, params)
371
- else:
372
- return self.db.all(sql, params)
373
-
374
- except Exception as e:
375
- self.ctx.err(f"query_threads ({take}, {skip})", e)
376
- return []
377
-
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
- def prepare_thread(self, thread, id=None, user=None):
449
- now = datetime.now()
450
- if id:
451
- thread["id"] = id
452
- else:
453
- thread["createdAt"] = now
454
- thread["updatedAt"] = now
455
- if "messages" in thread:
456
- for m in thread["messages"]:
457
- self.ctx.cache_message_inline_data(m)
458
- return with_user(thread, user=user)
459
-
460
- def create_thread(self, thread: Dict[str, Any], user=None):
461
- return self.insert("thread", self.prepare_thread(thread, user=user))
462
-
463
- 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))
465
-
466
- def update_thread(self, id, thread: Dict[str, Any], user=None):
467
- return self.update("thread", self.prepare_thread(thread, id, user=user))
468
-
469
- 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))
471
-
472
- def delete_thread(self, id, user=None, callback=None):
473
- sql_where, params = self.get_user_filter(user, {"id": id})
474
- self.db.write(f"DELETE FROM thread {sql_where} AND id = :id", params, callback)
475
-
476
- def query_requests(self, query: Dict[str, Any], user=None):
477
- try:
478
- columns = self.columns["request"]
479
- all_columns = columns.keys()
480
-
481
- take = min(int(query.get("take", "50")), 10000)
482
- skip = int(query.get("skip", 0))
483
- sort = query.get("sort", "-id")
484
-
485
- # always filter by user
486
- sql_where, params = self.get_user_filter(user, {"take": take, "skip": skip})
487
-
488
- filter = {}
489
- for k in query:
490
- if k in all_columns:
491
- filter[k] = query[k]
492
- params[k] = query[k]
493
-
494
- if len(filter) > 0:
495
- sql_where += " AND " + " AND ".join([f"{k} = :{k}" for k in filter])
496
-
497
- if "null" in query:
498
- cols = valid_columns(all_columns, query["null"])
499
- if len(cols) > 0:
500
- sql_where += " AND " + " AND ".join([f"{k} IS NULL" for k in cols])
501
-
502
- if "not_null" in query:
503
- cols = valid_columns(all_columns, query.get("not_null"))
504
- if len(cols) > 0:
505
- sql_where += " AND " + " AND ".join([f"{k} IS NOT NULL" for k in cols])
506
-
507
- if "q" in query:
508
- sql_where += " AND " if sql_where else "WHERE "
509
- sql_where += "(title LIKE :q)"
510
- params["q"] = f"%{query['q']}%"
511
-
512
- if "month" in query:
513
- sql_where += " AND strftime('%Y-%m', createdAt) = :month"
514
- params["month"] = query["month"]
515
-
516
- 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"
517
-
518
- if query.get("as") == "column":
519
- return self.db.column(sql, params)
520
- else:
521
- return self.db.all(sql, params)
522
- except Exception as e:
523
- self.ctx.err(f"query_requests ({take}, {skip})", e)
524
- return []
525
-
526
- def get_request_summary(self, user=None):
527
- try:
528
- sql_where, params = self.get_user_filter(user)
529
- # Use strftime to format date as YYYY-MM-DD
530
- sql = f"""
531
- SELECT
532
- strftime('%Y-%m-%d', createdAt) as date,
533
- count(id) as requests,
534
- sum(cost) as cost,
535
- sum(inputTokens) as inputTokens,
536
- sum(outputTokens) as outputTokens
537
- FROM request
538
- {sql_where}
539
- GROUP BY date
540
- ORDER BY date
541
- """
542
- return self.db.all(sql, params)
543
- except Exception as e:
544
- self.ctx.err(f"get_request_summary ({user})", e)
545
- return []
546
-
547
- def get_daily_request_summary(self, day, user=None):
548
- try:
549
- sql_where, params = self.get_user_filter(user)
550
- # Add date filter
551
- sql_where += " AND strftime('%Y-%m-%d', createdAt) = :day"
552
- params["day"] = day
553
-
554
- # Model aggregation
555
- sql_model = f"""
556
- SELECT
557
- model,
558
- count(id) as count,
559
- sum(cost) as cost,
560
- sum(duration) as duration,
561
- sum(inputTokens + outputTokens) as tokens,
562
- sum(inputTokens) as inputTokens,
563
- sum(outputTokens) as outputTokens
564
- FROM request
565
- {sql_where}
566
- GROUP BY model
567
- """
568
- model_data = {}
569
- for row in self.db.all(sql_model, params):
570
- model_data[row["model"]] = {
571
- "cost": row["cost"] or 0,
572
- "count": row["count"],
573
- "duration": row["duration"] or 0,
574
- "tokens": row["tokens"] or 0,
575
- "inputTokens": row["inputTokens"] or 0,
576
- "outputTokens": row["outputTokens"] or 0,
577
- }
578
-
579
- # Provider aggregation
580
- sql_provider = f"""
581
- SELECT
582
- provider,
583
- count(id) as count,
584
- sum(cost) as cost,
585
- sum(duration) as duration,
586
- sum(inputTokens + outputTokens) as tokens,
587
- sum(inputTokens) as inputTokens,
588
- sum(outputTokens) as outputTokens
589
- FROM request
590
- {sql_where}
591
- AND provider IS NOT NULL
592
- GROUP BY provider
593
- """
594
- provider_data = {}
595
- for row in self.db.all(sql_provider, params):
596
- provider_data[row["provider"]] = {
597
- "cost": row["cost"] or 0,
598
- "count": row["count"],
599
- "duration": row["duration"] or 0,
600
- "tokens": row["tokens"] or 0,
601
- "inputTokens": row["inputTokens"] or 0,
602
- "outputTokens": row["outputTokens"] or 0,
603
- }
604
-
605
- return {"modelData": model_data, "providerData": provider_data}
606
- except Exception as e:
607
- self.ctx.err(f"get_daily_request_summary ({day}, {user})", e)
608
- return {"modelData": {}, "providerData": {}}
609
-
610
- def create_request(self, request: Dict[str, Any], user=None):
611
- request["createdAt"] = request["updatedAt"] = datetime.now()
612
- return self.insert("request", with_user(request, user=user))
613
-
614
- async def create_request_async(self, request: Dict[str, Any], user=None):
615
- request["createdAt"] = request["updatedAt"] = datetime.now()
616
- return await self.insert_async("request", with_user(request, user=user))
617
-
618
- def update_request(self, id, request: Dict[str, Any], user=None):
619
- request["id"] = id
620
- request["updatedAt"] = datetime.now()
621
- return self.update("request", with_user(request, user=user))
622
-
623
- async def update_request_async(self, id, request: Dict[str, Any], user=None):
624
- request["id"] = id
625
- request["updatedAt"] = datetime.now()
626
- return await self.update_async("request", with_user(request, user=user))
627
-
628
- def delete_request(self, id, user=None, callback=None):
629
- sql_where, params = self.get_user_filter(user, {"id": id})
630
- self.db.write(f"DELETE FROM request {sql_where} AND id = :id", params, callback)
631
-
632
- def close(self):
633
- self.db.close()
634
-
635
- # complete all in progress tasks
636
- with self.db.create_writer_connection() as conn:
637
- conn.execute(
638
- "UPDATE thread SET completedAt = :completedAt, error = :error WHERE completedAt IS NULL",
639
- {"completedAt": datetime.now(), "error": "Server Shutdown"},
640
- )
641
- conn.execute(
642
- "UPDATE request SET completedAt = :completedAt, error = :error WHERE completedAt IS NULL",
643
- {"completedAt": datetime.now(), "error": "Server Shutdown"},
644
- )