llms-py 3.0.0b3__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.
@@ -1,4 +1,4 @@
1
- import { ref, onMounted, watch, nextTick, computed, inject } from 'vue'
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 h-full w-full">
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 overflow-auto bg-gray-50 dark:bg-gray-900" :class="activeTab === 'activity' ? 'p-0' : 'p-4'">
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' ? 'h-full' : 'max-w-6xl mx-auto'">
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="h-full flex flex-col bg-white dark:bg-gray-800">
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 overflow-y-auto" @scroll="onActivityScroll" ref="activityScrollContainer">
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 activityScrollContainer = ref(null)
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 onActivityScroll = async () => {
1316
- if (!activityScrollContainer.value) return
1317
+ const setupObserver = () => {
1318
+ if (observer) observer.disconnect()
1317
1319
 
1318
- const { scrollTop, scrollHeight, clientHeight } = activityScrollContainer.value
1319
- const isNearBottom = scrollHeight - scrollTop - clientHeight < 200
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 (isNearBottom && activityHasMore.value && !isActivityLoadingMore.value && !isActivityLoading.value) {
1322
- await loadActivityRequests(false)
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
- activityScrollContainer,
1508
- onActivityScroll,
1520
+ hasActiveFilters,
1521
+ scrollSentinel,
1509
1522
  clearActivityFilters,
1510
1523
  formatActivityDate,
1511
1524
  threadExists,
@@ -69,8 +69,8 @@ export default {
69
69
  </div>
70
70
 
71
71
  <!-- Messages -->
72
- <div v-else class="space-y-6">
73
- <div v-if="currentThread.messages.length && currentThread.model" class="absolute -mt-2">
72
+ <div v-else class="space-y-2">
73
+ <div v-if="currentThread?.messages.length && currentThread?.model" class="flex items-center justify-center select-none">
74
74
  <span @click="$chat.setSelectedModel({ name: currentThread.model})"
75
75
  class="flex items-center cursor-pointer px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors border hover:border-gray-300 dark:hover:border-gray-700">
76
76
  <ProviderIcon class="size-4 mr-1" :provider="$chat.getProviderForModel(currentThread.model)" />
@@ -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
- : 'bg-gray-600 dark:bg-gray-500 text-white'"
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
- {{ message.role === 'user' ? 'U' : 'AI' }}
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 text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
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 -->
@@ -178,7 +254,8 @@ export default {
178
254
  </div>
179
255
  </div>
180
256
 
181
- <div class="mt-2 text-xs opacity-70">
257
+ <div class="mt-2 text-xs opacity-70">
258
+ <span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> &#8226; </span>
182
259
  <span>{{ $fmt.time(message.timestamp) }}</span>
183
260
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
184
261
  &#8226;
@@ -310,9 +387,9 @@ export default {
310
387
  const router = useRouter()
311
388
  const route = useRoute()
312
389
 
313
- const prefs = ctx.getPrefs()
390
+ const prefs = ref(ctx.getPrefs())
314
391
 
315
- const selectedModel = ref(prefs.model || config.defaults.text.model || '')
392
+ const selectedModel = ref(prefs.value.model || config.defaults.text.model || '')
316
393
  const selectedModelObj = computed(() => {
317
394
  if (!selectedModel.value || !models) return null
318
395
  return models.find(m => m.name === selectedModel.value) || models.find(m => m.id === selectedModel.value)
@@ -747,7 +824,48 @@ export default {
747
824
  })
748
825
  onUnmounted(() => sub?.unsubscribe())
749
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
+
750
866
  return {
867
+ prefs,
868
+ setPrefs,
751
869
  config,
752
870
  models,
753
871
  threads,
@@ -780,6 +898,11 @@ export default {
780
898
  openLightbox,
781
899
  closeLightbox,
782
900
  resolveUrl,
901
+ getToolOutput,
902
+ isToolLinked,
903
+ tryParseJson,
904
+ hasJsonStructure,
905
+ customHtmlClasses,
783
906
  }
784
907
  }
785
908
  }
@@ -560,7 +560,8 @@ const ChatPrompt = {
560
560
  if (!isDuplicate) {
561
561
  await threads.addMessageToThread(threadId, {
562
562
  role: 'user',
563
- content: content
563
+ content: content,
564
+ model: props.model.name,
564
565
  })
565
566
  // Reload thread after adding message
566
567
  thread = await threads.getThread(threadId)
@@ -665,6 +666,16 @@ const ChatPrompt = {
665
666
  }
666
667
 
667
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
+
668
679
  // Add assistant response (save entire message including reasoning)
669
680
  const assistantMessage = response.choices?.[0]?.message
670
681
 
@@ -681,6 +692,7 @@ const ChatPrompt = {
681
692
  }
682
693
  await threads.logRequest(threadId, props.model, request, response)
683
694
  }
695
+ assistantMessage.model = props.model.name
684
696
  await threads.addMessageToThread(threadId, assistantMessage, usage)
685
697
 
686
698
  nextTick(addCopyButtons)
@@ -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
+ }
@@ -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.0b3
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