llms-py 3.0.6__py3-none-any.whl → 3.0.8__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,29 +1,233 @@
1
- import { inject, computed } from "vue"
1
+ import { inject, computed, ref, onMounted } from "vue"
2
+
3
+ let ext
4
+
5
+ const ToolResult = {
6
+ template: `
7
+ <div>
8
+ <div class="flex items-center gap-2 text-[10px] uppercase tracking-wider font-medium select-none">
9
+ <span @click="ext.setPrefs({ toolFormat: 'text' })"
10
+ class="cursor-pointer transition-colors"
11
+ :class="ext.prefs.toolFormat !== 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
12
+ text
13
+ </span>
14
+ <span class="text-gray-300 dark:text-gray-700">|</span>
15
+ <span @click="ext.setPrefs({ toolFormat: 'preview' })"
16
+ class="cursor-pointer transition-colors"
17
+ :class="ext.prefs.toolFormat == 'preview' ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'">
18
+ preview
19
+ </span>
20
+ </div>
21
+ <div class="not-prose px-3 py-2">
22
+ <pre v-if="ext.prefs.toolFormat !== 'preview'" class="tool-output">{{ origResult }}</pre>
23
+ <div v-else>
24
+ <ViewTypes v-if="Array.isArray(result)" :results="result" />
25
+ <ViewType v-else :result="result" />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ `,
30
+ props: {
31
+ result: {
32
+ required: true
33
+ }
34
+ },
35
+ setup(props) {
36
+
37
+ const origResult = computed(() => {
38
+ let ret = props.result
39
+ if (Array.isArray(props.result) && props.result.length == 1) {
40
+ ret = props.result[0]
41
+ }
42
+ if (ret.type) {
43
+ if (ret.type === "text") {
44
+ return ret.text
45
+ }
46
+ }
47
+ return props.result
48
+ })
49
+ const displayResult = computed(() => {
50
+ try {
51
+ let result = typeof props.result == 'string'
52
+ ? JSON.parse(props.result)
53
+ : props.result
54
+ if (Array.isArray(result) && result.length == 1) {
55
+ result = result[0]
56
+ }
57
+ if (result.type) {
58
+ if (result.type === "text") {
59
+ try {
60
+ return JSON.parse(result.text)
61
+ } catch (e) {
62
+ return result.text
63
+ }
64
+ }
65
+ }
66
+ return result
67
+ } catch (e) {
68
+ return props.result
69
+ }
70
+ })
71
+ return {
72
+ ext,
73
+ origResult,
74
+ displayResult,
75
+ }
76
+ }
77
+ }
2
78
 
3
79
  const Tools = {
80
+ components: {
81
+ ToolResult
82
+ },
4
83
  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>
84
+ <div class="p-4 md:p-6 max-w-7xl mx-auto w-full relative">
85
+ <div v-if="Object.keys($ctx.tools.toolPageHeaders).length">
86
+ <div v-for="(component, key) in $ctx.tools.toolPageHeaders" :key="key">
87
+ <component :is="component" />
88
+ </div>
89
+ </div>
90
+ <div ref="refTop" class="mb-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
91
+ <div>
92
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Tools</h1>
93
+ <p class="text-gray-600 dark:text-gray-400 mt-1">
94
+ {{ filteredTools.length }} tools available
95
+ </p>
96
+ </div>
97
+
98
+ <div v-if="groups.length > 0" class="flex flex-wrap items-center gap-2">
99
+ <button @click="ext.setPrefs({ selectedGroup: 'All' })"
100
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none"
101
+ :class="ext.prefs.selectedGroup === 'All'
102
+ ? 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 border-green-300 dark:border-green-800'
103
+ : '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'">
104
+ All
105
+ </button>
106
+
107
+ <div class="border-l h-4 mx-1 border-gray-300 dark:border-gray-600"></div>
108
+
109
+ <button v-for="group in groups" :key="group"
110
+ @click="ext.setPrefs({ selectedGroup: group})"
111
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none"
112
+ :class="ext.prefs.selectedGroup === group
113
+ ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800'
114
+ : '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'">
115
+ {{ group }}
116
+ </button>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Execution Form Panel -->
121
+ <div v-if="executingTool" class="mb-8 bg-white dark:bg-gray-800 rounded-lg border border-blue-200 dark:border-blue-800 shadow-sm overflow-hidden animate-in fade-in slide-in-from-top-4 duration-200">
122
+ <div class="bg-blue-50 dark:bg-blue-900/30 px-4 py-3 border-b border-blue-100 dark:border-blue-800 flex justify-between items-center">
123
+ <div class="flex items-center gap-2">
124
+ <h3 class="font-bold text-gray-900 dark:text-gray-100">Execute: <span class="font-mono text-blue-600 dark:text-blue-400">{{ executingTool.function.name }}</span></h3>
125
+ </div>
126
+ <button @click="closeExec" type="button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
127
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
128
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
129
+ </svg>
130
+ </button>
131
+ </div>
132
+
133
+ <div class="p-4 md:p-6">
134
+ <form ref="refForm" @submit.prevent="execTool" class="space-y-4">
135
+ <div v-if="Object.keys(executingTool.function.parameters?.properties || {}).length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-4">
136
+ <div v-for="(prop, name) in executingTool.function.parameters.properties" :key="name">
137
+ <label :for="'input-' + name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
138
+ {{ name }}
139
+ <span v-if="executingTool.function.parameters.required?.includes(name)" class="text-red-500">*</span>
140
+ </label>
141
+
142
+ <div v-if="prop.enum">
143
+ <select v-model="execForm[name]" :id="'input-' + name" class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
144
+ <option :value="undefined" disabled>Select...</option>
145
+ <option v-for="opt in prop.enum" :key="opt" :value="opt">{{ opt }}</option>
146
+ </select>
147
+ </div>
148
+
149
+ <div v-else-if="prop.type === 'boolean'">
150
+ <select v-model="execForm[name]" :id="'input-' + name" class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
151
+ <option :value="false">False</option>
152
+ <option :value="true">True</option>
153
+ </select>
154
+ </div>
155
+
156
+ <div v-else>
157
+ <input :type="prop.type === 'integer' || prop.type === 'number' ? 'number' : 'text'"
158
+ v-model="execForm[name]"
159
+ :id="'input-' + name"
160
+ :placeholder="prop.description"
161
+ :step="prop.type === 'integer' ? 1 : 0.01"
162
+ class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
163
+ </div>
164
+ <p v-if="prop.description" class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ prop.description }}</p>
165
+ </div>
166
+ </div>
167
+ <div v-else class="text-gray-500 dark:text-gray-400 italic">
168
+ No parameters required.
169
+ </div>
170
+
171
+ <div class="flex items-center gap-3 pt-4 border-t border-gray-100 dark:border-gray-700">
172
+ <button type="submit" :disabled="loading"
173
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
174
+ <svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
175
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
176
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
177
+ </svg>
178
+ {{ loading ? 'Executing...' : 'Run Tool' }}
179
+ </button>
180
+ </div>
181
+ </form>
182
+
183
+ <div v-if="execResult !== null || execError" class="mt-6">
184
+ <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Response:</h4>
185
+ <div v-if="execError" class="p-4 rounded-md bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 font-mono text-sm whitespace-pre-wrap">
186
+ {{ execError }}
187
+ </div>
188
+ <ToolResult v-else :result="execResult" />
189
+ </div>
190
+ </div>
11
191
  </div>
12
192
 
13
193
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
14
- <div v-for="tool in (Array.isArray($state.tools) ? $state.tools : []).filter(x => x.function)" :key="tool.function.name"
194
+ <div v-for="tool in filteredTools" :key="tool.function.name"
15
195
  class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
16
196
 
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">
197
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex justify-between items-center">
198
+ <div class="font-bold text-lg text-gray-900 dark:text-gray-100 font-mono break-all mr-2">
19
199
  {{ tool.function.name }}
20
200
  </div>
201
+ <button @click="startExec(tool)" type="button" title="Execute Tool" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 border-none">
202
+ <svg xmlns="http://www.w3.org/2000/svg" class="size-6" viewBox="0 0 20 20" fill="currentColor">
203
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
204
+ </svg>
205
+ </button>
21
206
  </div>
22
207
 
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>
208
+ <div class="tool-description p-4 flex-1 flex flex-col">
209
+ <div v-if="tool.function.description" class="text-sm text-gray-600 dark:text-gray-300 mb-4 flex-1 flex flex-col">
210
+ <div v-if="tool.function.description.length < 350">
211
+ <div v-html="$fmt.markdown(tool.function.description)"></div>
212
+ </div>
213
+ <div v-else>
214
+ <div class="relative transition-all duration-300 ease-in-out"
215
+ :class="{'max-h-[200px] overflow-hidden': !isExpanded(tool.function.name)}">
216
+ <div v-html="$fmt.markdown(tool.function.description)" :title="tool.function.description"></div>
217
+
218
+ <!-- Fade overlay when collapsed -->
219
+ <div v-if="!isExpanded(tool.function.name)"
220
+ class="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white dark:from-gray-800 to-transparent pointer-events-none">
221
+ </div>
222
+ </div>
223
+
224
+ <button @click="toggleDescription(tool.function.name)"
225
+ type="button"
226
+ class="mt-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 focus:outline-none self-start">
227
+ {{ isExpanded(tool.function.name) ? 'Show Less' : 'Show More' }}
228
+ </button>
229
+ </div>
230
+ </div>
27
231
  <p v-else class="text-sm text-gray-400 italic mb-4 flex-1">
28
232
  No description provided
29
233
  </p>
@@ -55,57 +259,253 @@ const Tools = {
55
259
  </div>
56
260
  `,
57
261
  setup() {
262
+ const ctx = inject('ctx')
263
+
264
+ // Execution State
265
+ const executingTool = ref(null)
266
+ const execForm = ref({})
267
+ const execResult = ref(null)
268
+ const execError = ref(null)
269
+ const loading = ref(false)
270
+ const refForm = ref()
271
+ const refTop = ref()
272
+
273
+ // UI State
274
+ const expandedDescriptions = ref({})
275
+
276
+ const groups = computed(() => Object.keys(ctx.state.tool.groups || {}))
277
+
278
+ const filteredTools = computed(() => {
279
+ const allTools = ctx.state.tool.definitions.filter(x => x.function)
280
+ if (ext.prefs.selectedGroup === 'All') return allTools
281
+
282
+ const groupTools = ctx.state.tool.groups[ext.prefs.selectedGroup] || []
283
+ return allTools.filter(t => groupTools.includes(t.function.name))
284
+ })
285
+
286
+ function startExec(tool) {
287
+ executingTool.value = tool
288
+ execForm.value = {}
289
+ execResult.value = null
290
+ execError.value = null
291
+
292
+ // Initialize defaults if any
293
+ if (tool.function.parameters?.properties) {
294
+ Object.entries(tool.function.parameters.properties).forEach(([key, prop]) => {
295
+ if (prop.default !== undefined) {
296
+ execForm.value[key] = prop.default
297
+ }
298
+ // Initialize booleans to false if likely
299
+ if (prop.type === 'boolean' && prop.default === undefined) {
300
+ // Optional: default to false? or maybe undefined is better to force user choice or let server handle it?
301
+ // Let's leave it undefined unless explicitly set
302
+ execForm.value[key] = false
303
+ }
304
+ })
305
+ }
306
+ // Scroll to top
307
+ // window.scrollTo({ top: 0, behavior: 'smooth' })
308
+ refForm.value?.scrollIntoView({ behavior: 'smooth' })
309
+ }
310
+
311
+ function closeExec() {
312
+ executingTool.value = null
313
+ execForm.value = {}
314
+ execResult.value = null
315
+ execError.value = null
316
+ }
317
+
318
+ async function execTool() {
319
+ if (!executingTool.value) return
58
320
 
321
+ loading.value = true
322
+ execResult.value = null
323
+ execError.value = null
324
+
325
+ try {
326
+ const ext = ctx.scope('tools')
327
+ // Filter out undefined values to avoid sending empty params that might confuse backend validation
328
+ // Or maybe send them as null? existing backend `tool_prop_value` handles things.
329
+ const payload = { ...execForm.value }
330
+
331
+ // Ensure numbers are numbers
332
+ if (executingTool.value.function.parameters?.properties) {
333
+ Object.entries(executingTool.value.function.parameters.properties).forEach(([key, prop]) => {
334
+ if ((prop.type === 'integer' || prop.type === 'number') && payload[key] !== '') {
335
+ payload[key] = Number(payload[key])
336
+ }
337
+ });
338
+ }
339
+
340
+ const res = await ext.postJson('/exec/' + executingTool.value.function.name, payload)
341
+ if (res.error) {
342
+ execError.value = res.error.message
343
+ } else {
344
+ execResult.value = res.response
345
+ }
346
+ } catch (e) {
347
+ execError.value = e.message || 'Unknown error occurred'
348
+ } finally {
349
+ loading.value = false
350
+ }
351
+ }
352
+
353
+ function toggleDescription(name) {
354
+ expandedDescriptions.value[name] = !expandedDescriptions.value[name]
355
+ }
356
+
357
+ function isExpanded(name) {
358
+ return !!expandedDescriptions.value[name]
359
+ }
360
+
361
+ onMounted(() => {
362
+ if (!ext.prefs.selectedGroup) {
363
+ ext.setPrefs({ selectedGroup: 'All' })
364
+ }
365
+ })
366
+
367
+ return {
368
+ ext,
369
+ refForm,
370
+ refTop,
371
+ groups,
372
+ filteredTools,
373
+ // Exec
374
+ executingTool,
375
+ execForm,
376
+ execResult,
377
+ execError,
378
+ loading,
379
+ startExec,
380
+ closeExec,
381
+ execTool,
382
+ // UI
383
+ toggleDescription,
384
+ isExpanded
385
+ }
59
386
  }
60
387
  }
61
388
 
62
389
  const ToolSelector = {
63
390
  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>
391
+ <div class="px-4 py-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 max-h-[80vh] overflow-y-auto">
392
+
393
+ <!-- Global Controls -->
394
+ <div class="flex items-center justify-between mb-4">
395
+ <span class="text-xs font-bold uppercase text-gray-500 tracking-wider">Include Tools</span>
396
+ <div class="flex items-center gap-2">
397
+ <button @click="$ctx.setPrefs({ onlyTools: null })"
398
+ class="px-3 py-1 rounded-md text-xs font-medium border transition-colors select-none"
399
+ :class="$prefs.onlyTools == null
400
+ ? 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 border-green-300 dark:border-green-800'
401
+ : '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'">
402
+ All Tools
403
+ </button>
404
+ <button @click="$ctx.setPrefs({ onlyTools:[] })"
405
+ class="px-3 py-1 rounded-md text-xs font-medium border transition-colors select-none"
406
+ :class="$prefs.onlyTools?.length === 0
407
+ ? 'bg-fuchsia-100 dark:bg-fuchsia-900/40 text-fuchsia-800 dark:text-fuchsia-300 border-fuchsia-200 dark:border-fuchsia-800'
408
+ : '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'">
409
+ No Tools
410
+ </button>
411
+ </div>
412
+ </div>
84
413
 
85
- <div class="border-l h-4"></div>
414
+ <!-- Groups -->
415
+ <div class="space-y-3">
416
+ <div v-for="group in toolGroups" :key="group.name"
417
+ class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
418
+
419
+ <!-- Group Header -->
420
+ <div class="flex items-center justify-between px-3 py-2 bg-gray-50/50 dark:bg-gray-800/50 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
421
+ @click="toggleCollapse(group.name)">
422
+
423
+ <div class="flex items-center gap-2 min-w-0">
424
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 text-gray-400 transition-transform duration-200" :class="{ '-rotate-90': isCollapsed(group.name) }">
425
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
426
+ </svg>
427
+ <span class="font-semibold text-sm text-gray-700 dark:text-gray-200 truncate">
428
+ {{ group.name || 'Other Tools' }}
429
+ </span>
430
+ <span class="text-xs text-gray-400 font-mono">
431
+ {{ getActiveCount(group) }}/{{ group.tools.length }}
432
+ </span>
433
+ </div>
86
434
 
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>
435
+ <div class="flex items-center gap-2" @click.stop>
436
+ <button @click="setGroupTools(group, true)" type="button"
437
+ title="Include All in Group"
438
+ class="px-2 py-0.5 rounded text-xs font-medium border transition-colors select-none"
439
+ :class="getActiveCount(group) === group.tools.length
440
+ ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-300 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/40'
441
+ : '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'">
442
+ all
443
+ </button>
444
+ <button @click="setGroupTools(group, false)" type="button"
445
+ title="Include None in Group"
446
+ class="px-2 py-0.5 rounded text-xs font-medium border transition-colors select-none"
447
+ :class="getActiveCount(group) === 0
448
+ ? 'bg-fuchsia-50 dark:bg-fuchsia-900/20 text-fuchsia-700 dark:text-fuchsia-300 border-fuchsia-200 dark:border-fuchsia-800 hover:bg-fuchsia-100 dark:hover:bg-fuchsia-900/40'
449
+ : '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'">
450
+ none
451
+ </button>
452
+ </div>
453
+ </div>
454
+
455
+ <!-- Group Body -->
456
+ <div v-show="!isCollapsed(group.name)" class="p-3 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800">
457
+ <div class="flex flex-wrap gap-2">
458
+ <button v-for="tool in group.tools" :key="tool.function.name" type="button"
459
+ @click="toggleTool(tool.function.name)"
460
+ :title="tool.function.description"
461
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none text-left truncate max-w-[200px]"
462
+ :class="isToolActive(tool.function.name)
463
+ ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800'
464
+ : 'bg-gray-50 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'">
465
+ {{ tool.function.name }}
466
+ </button>
467
+ </div>
468
+ </div>
469
+ </div>
97
470
  </div>
98
471
  </div>
99
472
  `,
100
473
  setup() {
101
474
  const ctx = inject('ctx')
475
+ const collapsedState = ref({})
476
+
477
+ const availableTools = computed(() => ctx.state.tool.definitions.filter(x => x.function))
102
478
 
103
- const availableTools = computed(() => (Array.isArray(ctx.state.tools) ? ctx.state.tools : []).filter(x => x.function))
479
+ const toolGroups = computed(() => {
480
+ const defs = availableTools.value
481
+ const groups = ctx.state.tool.groups || {}
482
+
483
+ const definedGroups = []
484
+ const usedTools = new Set()
485
+
486
+ for (const [groupName, toolNames] of Object.entries(groups)) {
487
+ if (!Array.isArray(toolNames)) continue
488
+ const tools = toolNames.map(name => defs.find(d => d.function.name === name)).filter(Boolean)
489
+ if (tools.length) {
490
+ tools.forEach(t => usedTools.add(t.function.name))
491
+ definedGroups.push({ name: groupName, tools })
492
+ }
493
+ }
494
+
495
+ const otherTools = defs.filter(d => !usedTools.has(d.function.name))
496
+ if (otherTools.length) {
497
+ definedGroups.push({ name: '', tools: otherTools })
498
+ }
499
+
500
+ return definedGroups
501
+ })
104
502
 
105
503
  function isToolActive(name) {
106
504
  const only = ctx.prefs.onlyTools
107
505
  if (only == null) return true
108
- if (Array.isArray(only)) return only.includes(name)
506
+ if (Array.isArray(only)) {
507
+ return only.includes(name)
508
+ }
109
509
  return false
110
510
  }
111
511
 
@@ -127,24 +527,88 @@ const ToolSelector = {
127
527
  ctx.setPrefs({ onlyTools })
128
528
  }
129
529
 
530
+ function toggleCollapse(groupName) {
531
+ const key = groupName || '_other_'
532
+ collapsedState.value[key] = !collapsedState.value[key]
533
+ }
534
+
535
+ function isCollapsed(groupName) {
536
+ const key = groupName || '_other_'
537
+ return !!collapsedState.value[key]
538
+ }
539
+
540
+ function setGroupTools(group, enable) {
541
+ const groupToolNames = group.tools.map(t => t.function.name)
542
+ let onlyTools = ctx.prefs.onlyTools
543
+
544
+ if (enable) {
545
+ if (onlyTools == null) return
546
+ const newSet = new Set(onlyTools)
547
+ groupToolNames.forEach(n => newSet.add(n))
548
+ onlyTools = Array.from(newSet)
549
+ if (onlyTools.length === availableTools.value.length) {
550
+ onlyTools = null
551
+ }
552
+ } else {
553
+ if (onlyTools == null) {
554
+ onlyTools = availableTools.value
555
+ .map(t => t.function.name)
556
+ .filter(n => !groupToolNames.includes(n))
557
+ } else {
558
+ onlyTools = onlyTools.filter(n => !groupToolNames.includes(n))
559
+ }
560
+ }
561
+
562
+ ctx.setPrefs({ onlyTools })
563
+ }
564
+
565
+ function getActiveCount(group) {
566
+ const onlyTools = ctx.prefs.onlyTools
567
+ if (onlyTools == null) return group.tools.length
568
+ return group.tools.filter(t => onlyTools.includes(t.function.name)).length
569
+ }
570
+
130
571
  return {
131
572
  availableTools,
573
+ toolGroups,
132
574
  isToolActive,
133
- toggleTool
575
+ toggleTool,
576
+ toggleCollapse,
577
+ isCollapsed,
578
+ setGroupTools,
579
+ getActiveCount
134
580
  }
135
581
  }
136
582
  }
137
583
 
584
+ function useTools(ctx) {
585
+ const toolPageHeaders = {}
586
+
587
+ function setToolPageHeaders(components) {
588
+ Object.assign(toolPageHeaders, components)
589
+ }
590
+
591
+ return {
592
+ toolPageHeaders,
593
+ setToolPageHeaders,
594
+ }
595
+ }
596
+
138
597
  export default {
139
598
  order: 10 - 100,
140
599
 
141
600
  install(ctx) {
601
+ ext = ctx.scope('tools')
142
602
 
143
603
  ctx.components({
144
604
  Tools,
145
605
  ToolSelector,
146
606
  })
147
607
 
608
+ ctx.setGlobals({
609
+ tools: useTools(ctx)
610
+ })
611
+
148
612
  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>`
149
613
 
150
614
  ctx.setLeftIcons({
@@ -195,10 +659,48 @@ export default {
195
659
  })
196
660
 
197
661
  ctx.routes.push({ path: '/tools', component: Tools, meta: { title: 'View Tools' } })
662
+ ctx.setState({
663
+ tool: { groups: {}, definitions: [] }
664
+ })
198
665
  },
199
666
 
200
667
  async load(ctx) {
201
- const ext = ctx.scope('tools')
202
- ctx.state.tools = (await ext.getJson('/')).response || []
668
+ const api = await ext.getJson('/')
669
+ if (api.response) {
670
+ ctx.setState({ tool: api.response })
671
+ //console.log(ctx.state.tool)
672
+ } else {
673
+ ctx.setError(api.error)
674
+ }
675
+
676
+ /* ctx.state.tool:
677
+ {
678
+ groups: {
679
+ "group_name": [
680
+ "memory_read"
681
+ ]
682
+ },
683
+ definitions: [
684
+ {
685
+ "type": "function",
686
+ "function": {
687
+ "name": "memory_read",
688
+ "description": "Read a value from persistent memory.",
689
+ "parameters": {
690
+ "type": "object",
691
+ "properties": {
692
+ "key": {
693
+ "type": "string"
694
+ }
695
+ },
696
+ "required": [
697
+ "key"
698
+ ]
699
+ }
700
+ }
701
+ ],
702
+ }
703
+ */
704
+
203
705
  }
204
706
  }