llms-py 3.0.0b7__py3-none-any.whl → 3.0.0b8__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 (157) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/extensions/analytics/ui/index.mjs +51 -162
  3. llms/extensions/app/__init__.py +519 -0
  4. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  5. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  6. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  7. llms/extensions/app/db.py +641 -0
  8. llms/extensions/app/db_manager.py +195 -0
  9. llms/extensions/app/requests.json +9073 -0
  10. llms/extensions/app/threads.json +15290 -0
  11. llms/{ui/modules/threads → extensions/app/ui}/Recents.mjs +82 -55
  12. llms/{ui/modules/threads → extensions/app/ui}/index.mjs +78 -9
  13. llms/extensions/app/ui/threadStore.mjs +407 -0
  14. llms/extensions/core_tools/__init__.py +272 -32
  15. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  26. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  27. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  28. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  31. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  32. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  33. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  34. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  35. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  36. llms/extensions/core_tools/ui/index.mjs +650 -0
  37. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  38. llms/extensions/gallery/db.py +4 -4
  39. llms/extensions/gallery/ui/index.mjs +2 -1
  40. llms/extensions/katex/__init__.py +6 -0
  41. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  42. llms/extensions/katex/ui/README.md +125 -0
  43. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  44. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  45. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  47. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  48. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  50. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  52. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  53. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  56. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/index.mjs +92 -0
  119. llms/extensions/katex/ui/katex-swap.css +1230 -0
  120. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  121. llms/extensions/katex/ui/katex.css +1230 -0
  122. llms/extensions/katex/ui/katex.js +19080 -0
  123. llms/extensions/katex/ui/katex.min.css +1 -0
  124. llms/extensions/katex/ui/katex.min.js +1 -0
  125. llms/extensions/katex/ui/katex.min.mjs +1 -0
  126. llms/extensions/katex/ui/katex.mjs +18547 -0
  127. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  128. llms/extensions/providers/anthropic.py +44 -1
  129. llms/extensions/system_prompts/ui/index.mjs +2 -1
  130. llms/extensions/tools/__init__.py +5 -0
  131. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  132. llms/extensions/tools/ui/index.mjs +8 -8
  133. llms/index.html +26 -38
  134. llms/llms.json +4 -1
  135. llms/main.py +492 -103
  136. llms/ui/App.mjs +2 -3
  137. llms/ui/ai.mjs +29 -13
  138. llms/ui/app.css +250 -398
  139. llms/ui/ctx.mjs +84 -6
  140. llms/ui/index.mjs +4 -6
  141. llms/ui/lib/vue.min.mjs +10 -9
  142. llms/ui/lib/vue.mjs +1796 -1635
  143. llms/ui/markdown.mjs +4 -2
  144. llms/ui/modules/chat/ChatBody.mjs +90 -86
  145. llms/ui/modules/chat/HomeTools.mjs +0 -242
  146. llms/ui/modules/chat/index.mjs +103 -170
  147. llms/ui/modules/model-selector.mjs +2 -2
  148. llms/ui/tailwind.input.css +35 -1
  149. llms/ui/utils.mjs +12 -0
  150. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/METADATA +1 -1
  151. llms_py-3.0.0b8.dist-info/RECORD +198 -0
  152. llms/ui/modules/threads/threadStore.mjs +0 -640
  153. llms_py-3.0.0b7.dist-info/RECORD +0 -80
  154. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/WHEEL +0 -0
  155. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/entry_points.txt +0 -0
  156. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/licenses/LICENSE +0 -0
  157. {llms_py-3.0.0b7.dist-info → llms_py-3.0.0b8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,519 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import time
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from aiohttp import web
9
+
10
+ g_db = None
11
+
12
+ try:
13
+ from llms.extensions.app.db import AppDB
14
+ except ImportError as e:
15
+ print(f"Failed to import AppDB: {e}")
16
+ AppDB = None
17
+
18
+
19
+ def install(ctx):
20
+ def get_db():
21
+ global g_db
22
+ if g_db is None and AppDB:
23
+ try:
24
+ db_path = os.path.join(ctx.get_user_path(), "app", "app.sqlite")
25
+ g_db = AppDB(ctx, db_path)
26
+ ctx.register_shutdown_handler(g_db.close)
27
+
28
+ threads_json = "/home/mythz/src/ServiceStack/llms/llms/extensions/app/threads.json"
29
+ requests_json = "/home/mythz/src/ServiceStack/llms/llms/extensions/app/requests.json"
30
+ with open(threads_json) as f:
31
+ threads = json.load(f)["threads"]
32
+ threads.reverse()
33
+ with open(requests_json) as f:
34
+ requests = json.load(f)["requests"]
35
+ requests.reverse()
36
+ # g_db.import_db(threads, requests)
37
+
38
+ except Exception as e:
39
+ ctx.err("Failed to init AppDB", e)
40
+ return g_db
41
+
42
+ if not get_db():
43
+ return
44
+
45
+ def to_dto(row, json_columns):
46
+ # as=column -> [0,1,2]
47
+ if not isinstance(row, dict):
48
+ return row
49
+
50
+ to = {}
51
+ for k, v in row.items():
52
+ if k in json_columns and v is not None and isinstance(v, str):
53
+ try:
54
+ to[k] = json.loads(v)
55
+ except Exception as e:
56
+ ctx.err(f"Failed to parse JSON for {k}: {v} ({type(v)})", e)
57
+ to[k] = v
58
+ else:
59
+ to[k] = v
60
+ return to
61
+
62
+ def thread_dto(row):
63
+ return row and to_dto(row, ["messages", "modalities", "args", "modelInfo", "stats"])
64
+
65
+ def request_dto(row):
66
+ return row and to_dto(row, ["usage"])
67
+
68
+ def prompt_to_title(prompt):
69
+ return prompt[:100] + ("..." if len(prompt) > 100 else "") if prompt else None
70
+
71
+ def timestamp_messages(messages):
72
+ timestamp = int(time.time() * 1000)
73
+ for message in messages:
74
+ if "timestamp" not in message:
75
+ message["timestamp"] = timestamp
76
+ timestamp += 1 # make unique
77
+ return messages
78
+
79
+ async def query_threads(request):
80
+ rows = g_db.query_threads(request.query, user=ctx.get_username(request))
81
+ dtos = [thread_dto(row) for row in rows]
82
+ return web.json_response(dtos)
83
+
84
+ ctx.add_get("threads", query_threads)
85
+
86
+ async def create_thread(request):
87
+ thread = await request.json()
88
+ id = await g_db.create_thread_async(thread, user=ctx.get_username(request))
89
+ row = g_db.get_thread(id, user=ctx.get_username(request))
90
+ return web.json_response(thread_dto(row) if row else "")
91
+
92
+ ctx.add_post("threads", create_thread)
93
+
94
+ async def update_thread(request):
95
+ thread = await request.json()
96
+ id = request.match_info["id"]
97
+ update_count = await g_db.update_thread_async(id, thread, user=ctx.get_username(request))
98
+ if update_count == 0:
99
+ raise Exception("Thread not found")
100
+ row = g_db.get_thread(id, user=ctx.get_username(request))
101
+ return web.json_response(thread_dto(row) if row else "")
102
+
103
+ ctx.add_patch("threads/{id}", update_thread)
104
+
105
+ async def delete_thread(request):
106
+ id = request.match_info["id"]
107
+ g_db.delete_thread(id, user=ctx.get_username(request))
108
+ return web.json_response({})
109
+
110
+ ctx.add_delete("threads/{id}", delete_thread)
111
+
112
+ async def queue_chat_handler(request):
113
+ # Check authentication if enabled
114
+ is_authenticated, user_data = ctx.check_auth(request)
115
+ if not is_authenticated:
116
+ return web.json_response(ctx.error_auth_required, status=401)
117
+
118
+ if not request.body_exists:
119
+ raise Exception("messages required")
120
+
121
+ chat = await request.json()
122
+
123
+ messages = timestamp_messages(chat.get("messages", []))
124
+ if len(messages) == 0:
125
+ raise Exception("messages required")
126
+
127
+ id = request.match_info["id"]
128
+ thread = thread_dto(g_db.get_thread(id, user=ctx.get_username(request)))
129
+ if not thread:
130
+ raise Exception("Thread not found")
131
+
132
+ update_thread = {
133
+ "messages": messages,
134
+ "startedAt": datetime.now(),
135
+ "completedAt": None,
136
+ "error": None,
137
+ }
138
+
139
+ model = chat.get("model", None)
140
+ if model:
141
+ update_thread["model"] = model
142
+ metadata = chat.get("metadata", {})
143
+ if len(metadata) > 0:
144
+ update_thread["metadata"] = metadata
145
+ if chat.get("modalities") or not thread.get("modalities"):
146
+ update_thread["modalities"] = chat.get("modalities", ["text"])
147
+ system_prompt = ctx.chat_to_system_prompt(chat)
148
+ if system_prompt:
149
+ update_thread["systemPrompt"] = system_prompt
150
+
151
+ args = thread.get("args") or {}
152
+ for k, v in chat.items():
153
+ if k in ctx.request_args:
154
+ args[k] = v
155
+ update_thread["args"] = args
156
+
157
+ # allow chat to override thread title
158
+ title = chat.get("title")
159
+ if title:
160
+ update_thread["title"] = title
161
+ else:
162
+ # only update thread title if it's not already set
163
+ title = thread.get("title")
164
+ if not title:
165
+ update_thread["title"] = title = prompt_to_title(ctx.last_user_prompt(chat))
166
+
167
+ user = ctx.get_username(request)
168
+ await g_db.update_thread_async(
169
+ id,
170
+ update_thread,
171
+ user=user,
172
+ )
173
+ thread = thread_dto(g_db.get_thread(id, user=user))
174
+ if not thread:
175
+ raise Exception("Thread not found")
176
+
177
+ chat = {
178
+ "model": thread.get("model"),
179
+ "messages": thread.get("messages"),
180
+ "modalities": thread.get("modalities"),
181
+ "systemPrompt": thread.get("systemPrompt"),
182
+ "metadata": thread.get("metadata", {}),
183
+ }
184
+ for k, v in thread.get("args", {}).items():
185
+ if k in ctx.request_args:
186
+ chat[k] = v
187
+
188
+ context = {
189
+ "chat": chat,
190
+ "user": user,
191
+ "threadId": id,
192
+ "tools": chat.get("metadata").get("tools", "all"),
193
+ }
194
+
195
+ # execute chat in background thread
196
+ async def run_chat(chat_req, context_req):
197
+ try:
198
+ await ctx.chat_completion(chat_req, context=context_req)
199
+ except Exception as ex:
200
+ ctx.err("run_chat", ex)
201
+ # not necessary to update thread in db with error as it's done in chat_error filter
202
+
203
+ asyncio.create_task(run_chat(chat, context))
204
+
205
+ return web.json_response(thread_dto(thread))
206
+
207
+ ctx.add_post("threads/{id}/chat", queue_chat_handler)
208
+
209
+ async def get_thread_updates(request):
210
+ id = request.match_info["id"]
211
+ after = request.query.get("after", None)
212
+ user = ctx.get_username(request)
213
+ thread = g_db.get_thread(id, user=user)
214
+ if not thread:
215
+ raise Exception("Thread not found")
216
+ if after:
217
+ started = time.time()
218
+ thread_id = thread.get("id")
219
+ thread_updated_at = thread.get("updatedAt")
220
+
221
+ while thread_updated_at <= after:
222
+ thread_updated_at = g_db.get_thread_column(thread_id, "updatedAt", user=user)
223
+ # if thread is not updated in 30 seconds, break
224
+ if time.time() - started > 10:
225
+ break
226
+ await asyncio.sleep(1)
227
+ ctx.dbg(f"get_thread_updates: {thread_id} / {thread_updated_at} < {after} / {thread_updated_at < after}")
228
+ thread = g_db.get_thread(thread_id, user=user)
229
+ return web.json_response(thread_dto(thread))
230
+
231
+ ctx.add_get("threads/{id}/updates", get_thread_updates)
232
+
233
+ async def cancel_thread(request):
234
+ id = request.match_info["id"]
235
+ await g_db.update_thread_async(
236
+ id, {"completedAt": datetime.now(), "error": "Request was canceled"}, user=ctx.get_username(request)
237
+ )
238
+ thread = g_db.get_thread(id, user=ctx.get_username(request))
239
+ ctx.dbg(f"cancel_thread: {id} / {thread.get('error')} / {thread.get('completedAt')}")
240
+ return web.json_response(thread_dto(thread))
241
+
242
+ ctx.add_post("threads/{id}/cancel", cancel_thread)
243
+
244
+ async def query_requests(request):
245
+ rows = g_db.query_requests(request.query, user=ctx.get_username(request))
246
+ dtos = [request_dto(row) for row in rows]
247
+ return web.json_response(dtos)
248
+
249
+ ctx.add_get("requests", query_requests)
250
+
251
+ async def delete_request(request):
252
+ id = request.match_info["id"]
253
+ g_db.delete_request(id, user=ctx.get_username(request))
254
+ return web.json_response({})
255
+
256
+ ctx.add_delete("requests/{id}", delete_request)
257
+
258
+ async def requests_summary(request):
259
+ rows = g_db.get_request_summary(user=ctx.get_username(request))
260
+ stats = {
261
+ "dailyData": {},
262
+ "years": [],
263
+ "totalCost": 0,
264
+ "totalRequests": 0,
265
+ "totalInputTokens": 0,
266
+ "totalOutputTokens": 0,
267
+ }
268
+ years = set()
269
+ for row in rows:
270
+ date = row["date"]
271
+ year = int(date[:4])
272
+ years.add(year)
273
+ stats["dailyData"][date] = {
274
+ "cost": row["cost"],
275
+ "requests": row["requests"],
276
+ "inputTokens": row["inputTokens"],
277
+ "outputTokens": row["outputTokens"],
278
+ }
279
+ stats["totalCost"] += row["cost"] or 0
280
+ stats["totalRequests"] += row["requests"] or 0
281
+ stats["totalInputTokens"] += row["inputTokens"] or 0
282
+ stats["totalOutputTokens"] += row["outputTokens"] or 0
283
+
284
+ stats["years"] = sorted(years)
285
+ return web.json_response(stats)
286
+
287
+ ctx.add_get("requests/summary", requests_summary)
288
+
289
+ async def daily_requests_summary(request):
290
+ day = request.match_info["day"]
291
+ summary = g_db.get_daily_request_summary(day, user=ctx.get_username(request))
292
+ return web.json_response(summary)
293
+
294
+ ctx.add_get("requests/summary/{day}", daily_requests_summary)
295
+
296
+ async def chat_request(openai_request, context):
297
+ chat = openai_request
298
+ user = context.get("user", None)
299
+ provider = context.get("provider", None)
300
+ thread_id = context.get("threadId", None)
301
+ model_info = context.get("modelInfo", None)
302
+
303
+ metadata = chat.get("metadata", {})
304
+ model = chat.get("model", None)
305
+ messages = timestamp_messages(chat.get("messages", []))
306
+ title = context.get("title") or prompt_to_title(ctx.last_user_prompt(chat) if chat else None)
307
+ started_at = context.get("startedAt")
308
+ if not started_at:
309
+ context["startedAt"] = started_at = datetime.now()
310
+ if thread_id is None:
311
+ thread = {
312
+ "user": user,
313
+ "model": model,
314
+ "provider": provider,
315
+ "modelInfo": model_info,
316
+ "title": title,
317
+ "messages": messages,
318
+ "systemPrompt": ctx.chat_to_system_prompt(chat),
319
+ "modalities": chat.get("modalities", ["text"]),
320
+ "startedAt": started_at,
321
+ "metadata": metadata,
322
+ }
323
+ thread_id = await g_db.create_thread_async(thread, user=user)
324
+ context["threadId"] = thread_id
325
+ else:
326
+ update_thread = {
327
+ "model": model,
328
+ "provider": provider,
329
+ "modelInfo": model_info,
330
+ "startedAt": started_at,
331
+ "messages": messages,
332
+ "completedAt": None,
333
+ "error": None,
334
+ "metadata": metadata,
335
+ }
336
+ await g_db.update_thread_async(thread_id, update_thread, user=user)
337
+
338
+ completed_at = g_db.get_thread_column(thread_id, "completedAt", user=user)
339
+ if completed_at:
340
+ context["completed"] = True
341
+
342
+ ctx.register_chat_request_filter(chat_request)
343
+
344
+ async def tool_request(tool_chat, context):
345
+ ctx.dbg("tool_request")
346
+ thread_id = context.get("threadId", None)
347
+ if not thread_id:
348
+ ctx.dbg("Missing threadId")
349
+ return
350
+ user = context.get("user", None)
351
+ completed_at = g_db.get_thread_column(thread_id, "completedAt", user=user)
352
+ if completed_at:
353
+ context["completed"] = True
354
+
355
+ ctx.register_chat_tool_filter(tool_request)
356
+
357
+ async def chat_response(openai_response, context):
358
+ ctx.dbg("create_response")
359
+ o = openai_response
360
+ chat = context.get("chat")
361
+ usage = o.get("usage", None)
362
+ if not usage and not chat:
363
+ ctx.dbg("Missing chat and usage")
364
+ return
365
+
366
+ user = context.get("user", None)
367
+ thread_id = context.get("threadId", None)
368
+ provider = context.get("provider", None)
369
+ model_info = context.get("modelInfo", None)
370
+ model_cost = context.get("modelCost", model_info.get("cost", None)) or {"input": 0, "output": 0}
371
+ duration = context.get("duration", 0)
372
+
373
+ metadata = o.get("metadata", {})
374
+ choices = o.get("choices", [])
375
+ tasks = []
376
+ title = context.get("title") or prompt_to_title(ctx.last_user_prompt(chat) if chat else None)
377
+ completed_at = datetime.now()
378
+
379
+ model = model_info.get("name") or model_info.get("id")
380
+ finish_reason = choices[0].get("finish_reason", None) if len(choices) > 0 else None
381
+ input_price = model_cost.get("input", 0)
382
+ output_price = model_cost.get("output", 0)
383
+ input_tokens = usage.get("prompt_tokens", 0)
384
+ output_tokens = usage.get("completion_tokens", 0)
385
+ total_tokens = usage.get("total_tokens", input_tokens + output_tokens)
386
+ cost = o.get("cost", ((input_price * input_tokens) + (output_price * output_tokens)) / 1000000)
387
+
388
+ request = {
389
+ "user": user,
390
+ "model": model,
391
+ "duration": duration,
392
+ "cost": cost,
393
+ "inputPrice": input_price,
394
+ "inputTokens": input_tokens,
395
+ "inputCachedTokens": usage.get("inputCachedTokens", 0),
396
+ "outputPrice": output_price,
397
+ "outputTokens": output_tokens,
398
+ "finishReason": finish_reason,
399
+ "provider": provider,
400
+ "providerModel": o.get("model", None),
401
+ "providerRef": o.get("provider", None),
402
+ "threadId": thread_id,
403
+ "title": title,
404
+ "startedAt": context.get("startedAt"),
405
+ "totalTokens": total_tokens,
406
+ "usage": usage,
407
+ "completedAt": completed_at,
408
+ "toolHistory": o.get("tool_history", None),
409
+ "ref": o.get("id", None),
410
+ }
411
+ tasks.append(g_db.create_request_async(request, user=user))
412
+
413
+ if thread_id:
414
+ messages = chat.get("messages", [])
415
+ last_role = messages[-1].get("role", None) if len(messages) > 0 else None
416
+ if last_role == "user" or last_role == "tool":
417
+ user_message = messages[-1]
418
+ user_message["model"] = model
419
+ user_message["usage"] = {
420
+ "tokens": input_tokens,
421
+ "price": input_price,
422
+ "cost": (input_price * input_tokens) / 1000000,
423
+ }
424
+ else:
425
+ ctx.dbg(
426
+ f"Missing user message for thread {thread_id}, {len(messages)} messages, last role: {last_role}"
427
+ )
428
+ assistant_message = ctx.chat_response_to_message(o)
429
+ assistant_message["model"] = model
430
+ assistant_message["usage"] = {
431
+ "tokens": output_tokens,
432
+ "price": output_price,
433
+ "cost": (output_price * output_tokens) / 1000000,
434
+ "duration": duration,
435
+ }
436
+ messages.append(assistant_message)
437
+
438
+ update_thread = {
439
+ "model": model,
440
+ "providerModel": o.get("model"),
441
+ "modelInfo": model_info,
442
+ "messages": messages,
443
+ "completedAt": completed_at,
444
+ }
445
+ if "error" in metadata:
446
+ update_thread["error"] = metadata["error"]
447
+ tasks.append(g_db.update_thread_async(thread_id, update_thread, user=user))
448
+ else:
449
+ ctx.dbg("Missing thread_id")
450
+
451
+ await asyncio.gather(*tasks)
452
+
453
+ # Update thread costs from all thread requests
454
+ thread_requests = g_db.query_requests({"threadId": thread_id}, user=user)
455
+ total_costs = 0
456
+ total_input = 0
457
+ total_output = 0
458
+ for request in thread_requests:
459
+ total_costs += request.get("cost", 0) or 0
460
+ total_input += request.get("inputTokens", 0) or 0
461
+ total_output += request.get("outputTokens", 0) or 0
462
+ stats = {
463
+ "inputTokens": total_input,
464
+ "outputTokens": total_output,
465
+ "cost": total_costs,
466
+ "duration": duration,
467
+ "requests": len(thread_requests),
468
+ }
469
+ g_db.update_thread(
470
+ thread_id,
471
+ {
472
+ "inputTokens": total_input,
473
+ "outputTokens": total_output,
474
+ "cost": total_costs,
475
+ "stats": stats,
476
+ },
477
+ user=user,
478
+ )
479
+
480
+ ctx.register_chat_response_filter(chat_response)
481
+
482
+ async def chat_error(e: Exception, context: Any):
483
+ error = ctx.error_message(e)
484
+ ctx.dbg(f"Chat error: {error}")
485
+ chat = context.get("chat")
486
+ if not chat:
487
+ ctx.dbg("Missing chat")
488
+ return
489
+
490
+ title = context.get("title") or prompt_to_title(ctx.last_user_prompt(chat) if chat else None)
491
+ completed_at = datetime.now()
492
+ user = context.get("user", None)
493
+
494
+ thread_id = context.get("threadId", None)
495
+ tasks = []
496
+ if thread_id:
497
+ tasks.append(g_db.update_thread_async(thread_id, {"completedAt": completed_at, "error": error}, user=user))
498
+ else:
499
+ ctx.dbg("Missing threadId")
500
+
501
+ request = {
502
+ "user": user,
503
+ "model": chat.get("model", None),
504
+ "title": title,
505
+ "threadId": thread_id,
506
+ "startedAt": context.get("startedAt"),
507
+ "completedAt": completed_at,
508
+ "error": error,
509
+ "stackTrace": context.get("stackTrace", None),
510
+ }
511
+ tasks.append(g_db.create_request_async(request, user=user))
512
+
513
+ if len(tasks) > 0:
514
+ await asyncio.gather(*tasks)
515
+
516
+ ctx.register_chat_error_filter(chat_error)
517
+
518
+
519
+ __install__ = install