llms-py 3.0.0b4__py3-none-any.whl → 3.0.0b5__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/__pycache__/main.cpython-314.pyc +0 -0
- llms/main.py +134 -20
- llms/providers-extra.json +32 -0
- llms/providers.json +1 -1
- llms/ui/App.mjs +3 -3
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +195 -0
- llms/ui/ctx.mjs +18 -9
- llms/ui/index.mjs +10 -1
- llms/ui/lib/servicestack-vue.mjs +3 -3
- llms/ui/modules/analytics.mjs +28 -15
- llms/ui/modules/chat/ChatBody.mjs +130 -8
- llms/ui/modules/chat/index.mjs +10 -0
- llms/ui/modules/tools.mjs +202 -0
- llms/ui/tailwind.input.css +20 -0
- llms/ui/utils.mjs +6 -0
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/METADATA +1 -1
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/RECORD +22 -21
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b4.dist-info → llms_py-3.0.0b5.dist-info}/top_level.txt +0 -0
llms/ui/modules/analytics.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref,
|
|
1
|
+
import { ref, watch, nextTick, computed, inject, onMounted, onUnmounted } from 'vue'
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
3
|
import { leftPart } from '@servicestack/client'
|
|
4
4
|
import { Chart, registerables } from "chart.js"
|
|
@@ -106,7 +106,7 @@ const MonthSelector = {
|
|
|
106
106
|
|
|
107
107
|
export const Analytics = {
|
|
108
108
|
template: `
|
|
109
|
-
<div class="flex flex-col
|
|
109
|
+
<div class="flex flex-col w-full">
|
|
110
110
|
<!-- Header -->
|
|
111
111
|
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 sm:px-4 py-3">
|
|
112
112
|
<div
|
|
@@ -150,9 +150,9 @@ export const Analytics = {
|
|
|
150
150
|
</div>
|
|
151
151
|
|
|
152
152
|
<!-- Content -->
|
|
153
|
-
<div class="flex-1
|
|
153
|
+
<div class="flex-1 bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
|
|
154
154
|
|
|
155
|
-
<div :class="activeTab === 'activity' ? '
|
|
155
|
+
<div :class="activeTab === 'activity' ? '' : 'max-w-6xl mx-auto'">
|
|
156
156
|
<!-- Stats Summary (hidden for Activity tab) -->
|
|
157
157
|
<div v-if="activeTab !== 'activity'" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
158
158
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
@@ -283,7 +283,7 @@ export const Analytics = {
|
|
|
283
283
|
</div>
|
|
284
284
|
|
|
285
285
|
<!-- Activity Tab - Full Page Layout -->
|
|
286
|
-
<div v-if="activeTab === 'activity'" class="
|
|
286
|
+
<div v-if="activeTab === 'activity'" class="flex flex-col bg-white dark:bg-gray-800">
|
|
287
287
|
<!-- Filters Bar -->
|
|
288
288
|
<div class="flex-shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 sm:px-6 py-4">
|
|
289
289
|
<div class="flex flex-wrap gap-2 sm:gap-4 items-end">
|
|
@@ -321,7 +321,7 @@ export const Analytics = {
|
|
|
321
321
|
</div>
|
|
322
322
|
|
|
323
323
|
<!-- Requests List with Infinite Scroll -->
|
|
324
|
-
<div class="flex-1
|
|
324
|
+
<div class="flex-1">
|
|
325
325
|
<div v-if="isActivityLoading && activityRequests.length === 0" class="flex items-center justify-center h-full">
|
|
326
326
|
<div class="text-center">
|
|
327
327
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
@@ -393,6 +393,7 @@ export const Analytics = {
|
|
|
393
393
|
No more requests to load
|
|
394
394
|
</div>
|
|
395
395
|
</div>
|
|
396
|
+
<div ref="scrollSentinel" class="h-4 w-full"></div>
|
|
396
397
|
</div>
|
|
397
398
|
</div>
|
|
398
399
|
</div>
|
|
@@ -527,7 +528,8 @@ export const Analytics = {
|
|
|
527
528
|
const selectedProvider = ref('')
|
|
528
529
|
const sortBy = ref('created')
|
|
529
530
|
const filterOptions = ref({ models: [], providers: [] })
|
|
530
|
-
const
|
|
531
|
+
const scrollSentinel = ref(null)
|
|
532
|
+
let observer = null
|
|
531
533
|
|
|
532
534
|
const hasActiveFilters = computed(() => selectedModel.value || selectedProvider.value)
|
|
533
535
|
|
|
@@ -1312,14 +1314,17 @@ export const Analytics = {
|
|
|
1312
1314
|
}
|
|
1313
1315
|
}
|
|
1314
1316
|
|
|
1315
|
-
const
|
|
1316
|
-
if (
|
|
1317
|
+
const setupObserver = () => {
|
|
1318
|
+
if (observer) observer.disconnect()
|
|
1317
1319
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
+
observer = new IntersectionObserver((entries) => {
|
|
1321
|
+
if (entries[0].isIntersecting && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
|
|
1322
|
+
loadActivityRequests(false)
|
|
1323
|
+
}
|
|
1324
|
+
}, { rootMargin: '200px' })
|
|
1320
1325
|
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
1326
|
+
if (scrollSentinel.value) {
|
|
1327
|
+
observer.observe(scrollSentinel.value)
|
|
1323
1328
|
}
|
|
1324
1329
|
}
|
|
1325
1330
|
|
|
@@ -1435,6 +1440,8 @@ export const Analytics = {
|
|
|
1435
1440
|
} else if (newTab === 'activity') {
|
|
1436
1441
|
await loadActivityFilterOptions()
|
|
1437
1442
|
await loadActivityRequests(true)
|
|
1443
|
+
await nextTick()
|
|
1444
|
+
setupObserver()
|
|
1438
1445
|
}
|
|
1439
1446
|
})
|
|
1440
1447
|
|
|
@@ -1467,9 +1474,15 @@ export const Analytics = {
|
|
|
1467
1474
|
if (activeTab.value === 'activity') {
|
|
1468
1475
|
await loadActivityFilterOptions()
|
|
1469
1476
|
await loadActivityRequests(true)
|
|
1477
|
+
await nextTick()
|
|
1478
|
+
setupObserver()
|
|
1470
1479
|
}
|
|
1471
1480
|
})
|
|
1472
1481
|
|
|
1482
|
+
onUnmounted(() => {
|
|
1483
|
+
if (observer) observer.disconnect()
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1473
1486
|
return {
|
|
1474
1487
|
activeTab,
|
|
1475
1488
|
costChartType,
|
|
@@ -1504,8 +1517,8 @@ export const Analytics = {
|
|
|
1504
1517
|
sortBy,
|
|
1505
1518
|
filterOptions,
|
|
1506
1519
|
hasActiveFilters,
|
|
1507
|
-
|
|
1508
|
-
|
|
1520
|
+
hasActiveFilters,
|
|
1521
|
+
scrollSentinel,
|
|
1509
1522
|
clearActivityFilters,
|
|
1510
1523
|
formatActivityDate,
|
|
1511
1524
|
threadExists,
|
|
@@ -80,6 +80,7 @@ export default {
|
|
|
80
80
|
<div
|
|
81
81
|
v-for="message in currentThread.messages"
|
|
82
82
|
:key="message.id"
|
|
83
|
+
v-show="!(message.role === 'tool' && isToolLinked(message))"
|
|
83
84
|
class="flex items-start space-x-3 group"
|
|
84
85
|
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
|
85
86
|
>
|
|
@@ -88,9 +89,15 @@ export default {
|
|
|
88
89
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
89
90
|
:class="message.role === 'user'
|
|
90
91
|
? 'bg-blue-100 dark:bg-blue-900 text-gray-900 dark:text-gray-100 border border-blue-200 dark:border-blue-700'
|
|
91
|
-
:
|
|
92
|
+
: message.role === 'tool'
|
|
93
|
+
? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800'
|
|
94
|
+
: 'bg-gray-600 dark:bg-gray-500 text-white'"
|
|
92
95
|
>
|
|
93
|
-
|
|
96
|
+
<span v-if="message.role === 'user'">U</span>
|
|
97
|
+
<svg v-else-if="message.role === 'tool'" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
98
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
|
99
|
+
</svg>
|
|
100
|
+
<span v-else>AI</span>
|
|
94
101
|
</div>
|
|
95
102
|
|
|
96
103
|
<!-- Delete button (shown on hover) -->
|
|
@@ -132,14 +139,83 @@ export default {
|
|
|
132
139
|
></div>
|
|
133
140
|
|
|
134
141
|
<!-- Collapsible reasoning section -->
|
|
135
|
-
<div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
|
|
142
|
+
<div v-if="message.role === 'assistant' && message.reasoning" class="mt-2 mb-2">
|
|
136
143
|
<button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center space-x-1">
|
|
137
144
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.id) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
|
|
138
145
|
<span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
139
146
|
</button>
|
|
140
|
-
<div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
|
|
147
|
+
<div v-if="isReasoningExpanded(message.id)" class="reasoning mt-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
|
|
141
148
|
<div v-if="typeof message.reasoning === 'string'" v-html="$fmt.markdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
|
|
142
|
-
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto
|
|
149
|
+
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(message.reasoning) }}</pre>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<!-- Tool Calls & Outputs -->
|
|
154
|
+
<div v-if="message.tool_calls && message.tool_calls.length > 0" class="mb-3 space-y-4">
|
|
155
|
+
<div v-for="(tool, i) in message.tool_calls" :key="i" class="rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden">
|
|
156
|
+
<!-- Tool Call Header -->
|
|
157
|
+
<div class="px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50/30 dark:bg-gray-800 space-x-4">
|
|
158
|
+
<div class="flex items-center gap-2">
|
|
159
|
+
<svg class="size-3.5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
|
|
160
|
+
<span class="font-mono text-xs font-bold text-gray-700 dark:text-gray-300">{{ tool.function.name }}</span>
|
|
161
|
+
</div>
|
|
162
|
+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Tool Call</span>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Arguments -->
|
|
166
|
+
<div v-if="tool.function.arguments && tool.function.arguments != '{}'" class="not-prose px-3 py-2">
|
|
167
|
+
<HtmlFormat v-if="hasJsonStructure(tool.function.arguments)" :value="tryParseJson(tool.function.arguments)" :classes="customHtmlClasses" />
|
|
168
|
+
<pre v-else class="tool-arguments">{{ tool.function.arguments }}</pre>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Tool Output (Nested) -->
|
|
172
|
+
<div v-if="getToolOutput(tool.id)" class="border-t border-gray-200 dark:border-gray-700">
|
|
173
|
+
<div class="px-3 py-1.5 flex justify-between items-center border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-800">
|
|
174
|
+
<div class="flex items-center gap-2 ">
|
|
175
|
+
<svg class="size-3.5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
176
|
+
<span class="text-[10px] uppercase tracking-wider text-gray-400 font-medium">Output</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div v-if="hasJsonStructure(getToolOutput(tool.id).content)" class="flex items-center gap-2 text-[10px] uppercase tracking-wider font-medium select-none">
|
|
179
|
+
<span @click="setPrefs({ toolFormat: 'text' })"
|
|
180
|
+
class="cursor-pointer transition-colors"
|
|
181
|
+
:class="prefs.toolFormat !== 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
|
|
182
|
+
text
|
|
183
|
+
</span>
|
|
184
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
185
|
+
<span @click="setPrefs({ toolFormat: 'preview' })"
|
|
186
|
+
class="cursor-pointer transition-colors"
|
|
187
|
+
:class="prefs.toolFormat == 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
|
|
188
|
+
preview
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="not-prose px-3 py-2">
|
|
193
|
+
<pre v-if="prefs.toolFormat !== 'preview' || !hasJsonStructure(getToolOutput(tool.id).content)" class="tool-output">{{ getToolOutput(tool.id).content }}</pre>
|
|
194
|
+
<div v-else class="text-xs">
|
|
195
|
+
<HtmlFormat v-if="tryParseJson(getToolOutput(tool.id).content)" :value="tryParseJson(getToolOutput(tool.id).content)" :classes="customHtmlClasses" />
|
|
196
|
+
<div v-else class="text-gray-500 italic p-2">Invalid JSON content</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- Tool Output (Orphaned) -->
|
|
204
|
+
<div v-if="message.role === 'tool' && !isToolLinked(message)" class="text-sm">
|
|
205
|
+
<div class="flex items-center gap-2 mb-1 opacity-70">
|
|
206
|
+
<div class="flex items-center text-xs font-mono font-medium text-gray-500 uppercase tracking-wider">
|
|
207
|
+
<svg class="size-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
208
|
+
Tool Output
|
|
209
|
+
</div>
|
|
210
|
+
<div v-if="message.name" class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 rounded text-gray-700 dark:text-gray-300">
|
|
211
|
+
{{ message.name }}
|
|
212
|
+
</div>
|
|
213
|
+
<div v-if="message.tool_call_id" class="text-[10px] font-mono text-gray-400">
|
|
214
|
+
{{ message.tool_call_id.slice(0,8) }}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="not-prose bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-800 p-2 overflow-x-auto">
|
|
218
|
+
<pre class="tool-output">{{ message.content }}</pre>
|
|
143
219
|
</div>
|
|
144
220
|
</div>
|
|
145
221
|
|
|
@@ -153,7 +229,7 @@ export default {
|
|
|
153
229
|
</div>
|
|
154
230
|
|
|
155
231
|
<!-- User Message with separate attachments -->
|
|
156
|
-
<div v-if="message.role !== 'assistant'">
|
|
232
|
+
<div v-else-if="message.role !== 'assistant' && message.role !== 'tool'">
|
|
157
233
|
<div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
|
|
158
234
|
|
|
159
235
|
<!-- Attachments Grid -->
|
|
@@ -311,9 +387,9 @@ export default {
|
|
|
311
387
|
const router = useRouter()
|
|
312
388
|
const route = useRoute()
|
|
313
389
|
|
|
314
|
-
const prefs = ctx.getPrefs()
|
|
390
|
+
const prefs = ref(ctx.getPrefs())
|
|
315
391
|
|
|
316
|
-
const selectedModel = ref(prefs.model || config.defaults.text.model || '')
|
|
392
|
+
const selectedModel = ref(prefs.value.model || config.defaults.text.model || '')
|
|
317
393
|
const selectedModelObj = computed(() => {
|
|
318
394
|
if (!selectedModel.value || !models) return null
|
|
319
395
|
return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
|
|
@@ -748,7 +824,48 @@ export default {
|
|
|
748
824
|
})
|
|
749
825
|
onUnmounted(() => sub?.unsubscribe())
|
|
750
826
|
|
|
827
|
+
const getToolOutput = (toolCallId) => {
|
|
828
|
+
return currentThread.value?.messages?.find(m => m.role === 'tool' && m.tool_call_id === toolCallId)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const isToolLinked = (message) => {
|
|
832
|
+
if (message.role !== 'tool') return false
|
|
833
|
+
return currentThread.value?.messages?.some(m => m.role === 'assistant' && m.tool_calls?.some(tc => tc.id === message.tool_call_id))
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const tryParseJson = (str) => {
|
|
837
|
+
try {
|
|
838
|
+
return JSON.parse(str)
|
|
839
|
+
} catch (e) {
|
|
840
|
+
return null
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const hasJsonStructure = (str) => {
|
|
844
|
+
return tryParseJson(str) != null
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* @param {object|array} type
|
|
848
|
+
* @param {'div'|'table'|'thead'|'th'|'tr'|'td'} tag
|
|
849
|
+
* @param {number} depth
|
|
850
|
+
* @param {string} cls
|
|
851
|
+
* @param {number} index
|
|
852
|
+
*/
|
|
853
|
+
const customHtmlClasses = (type, tag, depth, cls, index) => {
|
|
854
|
+
cls = cls.replace('shadow ring-1 ring-black/5 md:rounded-lg', '')
|
|
855
|
+
if (tag == 'th') {
|
|
856
|
+
cls += ' lowercase'
|
|
857
|
+
}
|
|
858
|
+
return cls
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function setPrefs(o) {
|
|
862
|
+
Object.assign(prefs.value, o)
|
|
863
|
+
ctx.setPrefs(prefs.value)
|
|
864
|
+
}
|
|
865
|
+
|
|
751
866
|
return {
|
|
867
|
+
prefs,
|
|
868
|
+
setPrefs,
|
|
752
869
|
config,
|
|
753
870
|
models,
|
|
754
871
|
threads,
|
|
@@ -781,6 +898,11 @@ export default {
|
|
|
781
898
|
openLightbox,
|
|
782
899
|
closeLightbox,
|
|
783
900
|
resolveUrl,
|
|
901
|
+
getToolOutput,
|
|
902
|
+
isToolLinked,
|
|
903
|
+
tryParseJson,
|
|
904
|
+
hasJsonStructure,
|
|
905
|
+
customHtmlClasses,
|
|
784
906
|
}
|
|
785
907
|
}
|
|
786
908
|
}
|
llms/ui/modules/chat/index.mjs
CHANGED
|
@@ -666,6 +666,16 @@ const ChatPrompt = {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
if (!errorStatus.value) {
|
|
669
|
+
// Add tool history messages if any
|
|
670
|
+
if (response.tool_history && Array.isArray(response.tool_history)) {
|
|
671
|
+
for (const msg of response.tool_history) {
|
|
672
|
+
if (msg.role === 'assistant') {
|
|
673
|
+
msg.model = props.model.name // tag with model
|
|
674
|
+
}
|
|
675
|
+
await threads.addMessageToThread(threadId, msg)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
669
679
|
// Add assistant response (save entire message including reasoning)
|
|
670
680
|
const assistantMessage = response.choices?.[0]?.message
|
|
671
681
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { ref, inject, computed } from "vue"
|
|
2
|
+
|
|
3
|
+
const Tools = {
|
|
4
|
+
template: `
|
|
5
|
+
<div class="p-4 md:p-6 max-w-7xl mx-auto w-full">
|
|
6
|
+
<div class="mb-6">
|
|
7
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Tools</h1>
|
|
8
|
+
<p class="text-gray-600 dark:text-gray-400 mt-1">
|
|
9
|
+
{{ ($state.tools || []).length }} tools available
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
14
|
+
<div v-for="tool in ($state.tools || [])" :key="tool.function.name"
|
|
15
|
+
class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden flex flex-col">
|
|
16
|
+
|
|
17
|
+
<div class="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
|
18
|
+
<div class="font-bold text-lg text-gray-900 dark:text-gray-100 font-mono break-all">
|
|
19
|
+
{{ tool.function.name }}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="p-4 flex-1 flex flex-col">
|
|
24
|
+
<p v-if="tool.function.description" class="text-sm text-gray-600 dark:text-gray-300 mb-4 flex-1">
|
|
25
|
+
{{ tool.function.description }}
|
|
26
|
+
</p>
|
|
27
|
+
<p v-else class="text-sm text-gray-400 italic mb-4 flex-1">
|
|
28
|
+
No description provided
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<div v-if="tool.function.parameters?.properties && Object.keys(tool.function.parameters.properties).length > 0">
|
|
32
|
+
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Parameters</div>
|
|
33
|
+
<div class="space-y-3">
|
|
34
|
+
<div v-for="(prop, name) in tool.function.parameters.properties" :key="name" class="text-sm bg-gray-50 dark:bg-gray-700/30 rounded p-2">
|
|
35
|
+
<div class="flex flex-wrap items-baseline gap-2 mb-1">
|
|
36
|
+
<span class="font-mono font-medium text-blue-600 dark:text-blue-400">{{ name }}</span>
|
|
37
|
+
<span class="text-gray-500 text-xs">({{ prop.type }})</span>
|
|
38
|
+
<span v-if="tool.function.parameters.required?.includes(name)"
|
|
39
|
+
class="px-1.5 py-0.5 text-[10px] rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 font-medium">
|
|
40
|
+
REQUIRED
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div v-if="prop.description" class="text-gray-600 dark:text-gray-400 text-xs">
|
|
44
|
+
{{ prop.description }}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div v-else class="text-sm text-gray-400 italic border-t border-gray-100 dark:border-gray-700 pt-2 mt-auto">
|
|
50
|
+
No parameters
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`,
|
|
57
|
+
setup() {
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ToolSelector = {
|
|
63
|
+
template: `
|
|
64
|
+
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
|
65
|
+
<div class="flex flex-wrap items-center gap-2 text-sm">
|
|
66
|
+
|
|
67
|
+
<!-- All -->
|
|
68
|
+
<button @click="$ctx.setPrefs({ onlyTools: null })"
|
|
69
|
+
class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none"
|
|
70
|
+
:class="$prefs.onlyTools == null
|
|
71
|
+
? 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800'
|
|
72
|
+
: 'cursor-pointer bg-white dark:bg-gray-800 text-gray-600 dark:border-gray-700 dark:text-gray-400 border-gray-200 dark:hover:border-gray-600 hover:border-gray-300'">
|
|
73
|
+
All
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
<!-- None -->
|
|
77
|
+
<button @click="$ctx.setPrefs({ onlyTools:[] })"
|
|
78
|
+
class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none"
|
|
79
|
+
:class="$prefs.onlyTools?.length === 0
|
|
80
|
+
? 'bg-fuchsia-100 dark:bg-fuchsia-900/40 text-fuchsia-800 dark:text-fuchsia-300 border-fuchsia-200 dark:border-fuchsia-800'
|
|
81
|
+
: 'cursor-pointer bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
|
82
|
+
None
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<div class="border-l h-4"></div>
|
|
86
|
+
|
|
87
|
+
<!-- Tools -->
|
|
88
|
+
<button v-for="tool in availableTools" :key="tool.function.name" type="button"
|
|
89
|
+
@click="toggleTool(tool.function.name)"
|
|
90
|
+
:title="tool.function.description"
|
|
91
|
+
class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none"
|
|
92
|
+
:class="isToolActive(tool.function.name)
|
|
93
|
+
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800'
|
|
94
|
+
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
|
95
|
+
{{ tool.function.name }}
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
setup() {
|
|
101
|
+
const ctx = inject('ctx')
|
|
102
|
+
|
|
103
|
+
const availableTools = computed(() => ctx.state.tools || [])
|
|
104
|
+
|
|
105
|
+
function isToolActive(name) {
|
|
106
|
+
const only = ctx.prefs.onlyTools
|
|
107
|
+
if (only == null) return true
|
|
108
|
+
if (Array.isArray(only)) return only.includes(name)
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toggleTool(name) {
|
|
113
|
+
let onlyTools = ctx.prefs.onlyTools
|
|
114
|
+
|
|
115
|
+
// If currently 'All', clicking a tool means we enter custom mode with all OTHER tools selected (deselecting clicked)
|
|
116
|
+
if (onlyTools == null) {
|
|
117
|
+
onlyTools = availableTools.value.map(t => t.function.name).filter(t => t !== name)
|
|
118
|
+
} else {
|
|
119
|
+
// Currently Custom or None
|
|
120
|
+
if (onlyTools.includes(name)) {
|
|
121
|
+
onlyTools = onlyTools.filter(t => t !== name)
|
|
122
|
+
} else {
|
|
123
|
+
onlyTools = [...onlyTools, name]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ctx.setPrefs({ onlyTools })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
availableTools,
|
|
132
|
+
isToolActive,
|
|
133
|
+
toggleTool
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default {
|
|
139
|
+
install(ctx) {
|
|
140
|
+
|
|
141
|
+
ctx.components({
|
|
142
|
+
Tools,
|
|
143
|
+
ToolSelector,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const svg = (attrs, title) => `<svg ${attrs} xmlns="http://www.w4.org/2000/svg" viewBox="0 0 24 24">${title ? "<title>" + title + "</title>" : ''}<path fill="currentColor" d="M5.33 3.272a3.5 3.5 0 0 1 4.472 4.473L20.647 18.59l-2.122 2.122L7.68 9.867a3.5 3.5 0 0 1-4.472-4.474L5.444 7.63a1.5 1.5 0 0 0 2.121-2.121zm10.367 1.883l3.182-1.768l1.414 1.415l-1.768 3.182l-1.768.353l-2.12 2.121l-1.415-1.414l2.121-2.121zm-7.071 7.778l2.121 2.122l-4.95 4.95A1.5 1.5 0 0 1 3.58 17.99l.097-.107z"/></svg>`
|
|
147
|
+
|
|
148
|
+
ctx.setLeftIcons({
|
|
149
|
+
tools: {
|
|
150
|
+
component: {
|
|
151
|
+
template: svg('@click=$ctx.togglePath("/tools")'),
|
|
152
|
+
},
|
|
153
|
+
isActive({ path }) {
|
|
154
|
+
return path === '/tools'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
ctx.setTopIcons({
|
|
160
|
+
tools: {
|
|
161
|
+
component: {
|
|
162
|
+
template: svg([
|
|
163
|
+
`@click=$ctx.toggleTop("ToolSelector")`,
|
|
164
|
+
`:class="$prefs.onlyTools == null ? 'text-green-600 dark:text-green-300' : $prefs.onlyTools.length ? 'text-blue-600! dark:text-blue-300!' : ''"`
|
|
165
|
+
].join(' ')),
|
|
166
|
+
// , "{{$prefs.onlyTools == null ? 'Include All Tools' : $prefs.onlyTools.length ? 'Include Selected Tools' : 'All Tools Excluded'}}"
|
|
167
|
+
},
|
|
168
|
+
isActive({ top }) {
|
|
169
|
+
return top === 'ToolSelector'
|
|
170
|
+
},
|
|
171
|
+
get title() {
|
|
172
|
+
return ctx.prefs.onlyTools == null
|
|
173
|
+
? `All Tools Included`
|
|
174
|
+
: ctx.prefs.onlyTools.length
|
|
175
|
+
? `${ctx.prefs.onlyTools.length} ${ctx.utils.pluralize('Tool', ctx.prefs.onlyTools.length)} Included`
|
|
176
|
+
: 'No Tools Included'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
ctx.chatRequestFilters.push(({ request, thread }) => {
|
|
182
|
+
// Tool Preferences
|
|
183
|
+
const prefs = ctx.prefs
|
|
184
|
+
if (prefs.onlyTools != null) {
|
|
185
|
+
if (Array.isArray(prefs.onlyTools)) {
|
|
186
|
+
request.metadata.only_tools = prefs.onlyTools.length > 0
|
|
187
|
+
? prefs.onlyTools.join(',')
|
|
188
|
+
: 'none'
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
request.metadata.only_tools = 'all'
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
ctx.routes.push({ path: '/tools', component: Tools, meta: { title: 'View Tools' } })
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async load(ctx) {
|
|
199
|
+
const ext = ctx.scope('tools')
|
|
200
|
+
ctx.state.tools = await ext.getJson('/')
|
|
201
|
+
}
|
|
202
|
+
}
|
llms/ui/tailwind.input.css
CHANGED
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
::file-selector-button {
|
|
31
31
|
border-color: hsl(var(--border));
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
.reasoning .prose-xs p,
|
|
35
|
+
.reasoning .prose-xs li {
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
@theme {
|
|
@@ -144,6 +149,21 @@
|
|
|
144
149
|
font-weight: 600;
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
/* Tool specific styles to override global prose */
|
|
153
|
+
.tool-arguments,
|
|
154
|
+
.tool-output {
|
|
155
|
+
margin: 0 !important;
|
|
156
|
+
padding: 0 !important;
|
|
157
|
+
background-color: transparent !important;
|
|
158
|
+
color: inherit !important;
|
|
159
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
|
160
|
+
font-size: 0.75rem !important;
|
|
161
|
+
white-space: pre-wrap !important;
|
|
162
|
+
word-break: break-all !important;
|
|
163
|
+
border: none !important;
|
|
164
|
+
border-radius: 0 !important;
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
/* highlight.js - vs.css */
|
|
148
168
|
.hljs {
|
|
149
169
|
background: white;
|
llms/ui/utils.mjs
CHANGED
|
@@ -81,6 +81,10 @@ export function deepClone(o) {
|
|
|
81
81
|
return serializedClone(o)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export function pluralize(word, count) {
|
|
85
|
+
return count === 1 ? word : word + 's'
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
const currFmt2 = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
|
85
89
|
const currFmt6 = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 6 })
|
|
86
90
|
|
|
@@ -156,6 +160,7 @@ export function utilsFormatters() {
|
|
|
156
160
|
statsTitle,
|
|
157
161
|
relativeTime,
|
|
158
162
|
time,
|
|
163
|
+
pluralize,
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -183,6 +188,7 @@ export function utilsFunctions() {
|
|
|
183
188
|
fileToDataUri,
|
|
184
189
|
serializedClone,
|
|
185
190
|
deepClone,
|
|
191
|
+
pluralize,
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llms-py
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b5
|
|
4
4
|
Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
|
|
5
5
|
Home-page: https://github.com/ServiceStack/llms
|
|
6
6
|
Author: ServiceStack
|