llms-py 2.0.5__tar.gz → 2.0.6__tar.gz

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 (39) hide show
  1. {llms_py-2.0.5/llms_py.egg-info → llms_py-2.0.6}/PKG-INFO +1 -1
  2. {llms_py-2.0.5 → llms_py-2.0.6}/llms.py +1 -1
  3. {llms_py-2.0.5 → llms_py-2.0.6/llms_py.egg-info}/PKG-INFO +1 -1
  4. {llms_py-2.0.5 → llms_py-2.0.6}/pyproject.toml +1 -1
  5. {llms_py-2.0.5 → llms_py-2.0.6}/setup.py +1 -1
  6. {llms_py-2.0.5 → llms_py-2.0.6}/ui/Main.mjs +177 -0
  7. {llms_py-2.0.5 → llms_py-2.0.6}/ui/app.css +8 -0
  8. {llms_py-2.0.5 → llms_py-2.0.6}/LICENSE +0 -0
  9. {llms_py-2.0.5 → llms_py-2.0.6}/MANIFEST.in +0 -0
  10. {llms_py-2.0.5 → llms_py-2.0.6}/README.md +0 -0
  11. {llms_py-2.0.5 → llms_py-2.0.6}/index.html +0 -0
  12. {llms_py-2.0.5 → llms_py-2.0.6}/llms.json +0 -0
  13. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/SOURCES.txt +0 -0
  14. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/dependency_links.txt +0 -0
  15. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/entry_points.txt +0 -0
  16. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/not-zip-safe +0 -0
  17. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/requires.txt +0 -0
  18. {llms_py-2.0.5 → llms_py-2.0.6}/llms_py.egg-info/top_level.txt +0 -0
  19. {llms_py-2.0.5 → llms_py-2.0.6}/requirements.txt +0 -0
  20. {llms_py-2.0.5 → llms_py-2.0.6}/setup.cfg +0 -0
  21. {llms_py-2.0.5 → llms_py-2.0.6}/ui/App.mjs +0 -0
  22. {llms_py-2.0.5 → llms_py-2.0.6}/ui/ChatPrompt.mjs +0 -0
  23. {llms_py-2.0.5 → llms_py-2.0.6}/ui/Recents.mjs +0 -0
  24. {llms_py-2.0.5 → llms_py-2.0.6}/ui/Sidebar.mjs +0 -0
  25. {llms_py-2.0.5 → llms_py-2.0.6}/ui/fav.svg +0 -0
  26. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/highlight.min.mjs +0 -0
  27. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/idb.min.mjs +0 -0
  28. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/marked.min.mjs +0 -0
  29. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/servicestack-client.min.mjs +0 -0
  30. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/servicestack-vue.min.mjs +0 -0
  31. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/vue-router.min.mjs +0 -0
  32. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/vue.min.mjs +0 -0
  33. {llms_py-2.0.5 → llms_py-2.0.6}/ui/lib/vue.mjs +0 -0
  34. {llms_py-2.0.5 → llms_py-2.0.6}/ui/markdown.mjs +0 -0
  35. {llms_py-2.0.5 → llms_py-2.0.6}/ui/tailwind.input.css +0 -0
  36. {llms_py-2.0.5 → llms_py-2.0.6}/ui/threadStore.mjs +0 -0
  37. {llms_py-2.0.5 → llms_py-2.0.6}/ui/typography.css +0 -0
  38. {llms_py-2.0.5 → llms_py-2.0.6}/ui/utils.mjs +0 -0
  39. {llms_py-2.0.5 → llms_py-2.0.6}/ui.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.5
3
+ Version: 2.0.6
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
@@ -21,7 +21,7 @@ from aiohttp import web
21
21
  from pathlib import Path
22
22
  from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
23
23
 
24
- VERSION = "2.0.5"
24
+ VERSION = "2.0.6"
25
25
  _ROOT = None
26
26
  g_config_path = None
27
27
  g_ui_path = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.5
3
+ Version: 2.0.6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "llms-py"
7
- version = "2.0.5"
7
+ version = "2.0.6"
8
8
  description = "A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -16,7 +16,7 @@ with open(os.path.join(this_directory, "requirements.txt"), encoding="utf-8") as
16
16
 
17
17
  setup(
18
18
  name="llms-py",
19
- version="2.0.5",
19
+ version="2.0.6",
20
20
  author="ServiceStack",
21
21
  author_email="team@servicestack.net",
22
22
  description="A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers",
@@ -197,6 +197,51 @@ export default {
197
197
  <div class="max-w-2xl mx-auto">
198
198
  <ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
199
199
  </div>
200
+
201
+ <!-- Export/Import buttons -->
202
+ <div class="mt-2 flex space-x-3 justify-center">
203
+ <button type="button"
204
+ @click="exportThreads"
205
+ :disabled="isExporting"
206
+ :title="'Export ' + threads?.threads?.value?.length + ' conversations'"
207
+ class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
208
+ >
209
+ <svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
210
+ <path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"></path>
211
+ </svg>
212
+ <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
213
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
214
+ <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>
215
+ </svg>
216
+ {{ isExporting ? 'Exporting...' : 'Export' }}
217
+ </button>
218
+
219
+ <button type="button"
220
+ @click="triggerImport"
221
+ :disabled="isImporting"
222
+ title="Import conversations from JSON file"
223
+ class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
224
+ >
225
+ <svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
226
+ <path fill="currentColor" d="m14 12l-4-4v3H2v2h8v3m10 2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3h2V6h12v12H6v-3H4v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/>
227
+ </svg>
228
+ <svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
229
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
230
+ <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>
231
+ </svg>
232
+ {{ isImporting ? 'Importing...' : 'Import' }}
233
+ </button>
234
+
235
+ <!-- Hidden file input for import -->
236
+ <input
237
+ ref="fileInput"
238
+ type="file"
239
+ accept=".json"
240
+ @change="handleFileImport"
241
+ class="hidden"
242
+ />
243
+ </div>
244
+
200
245
  </div>
201
246
 
202
247
  <!-- Messages -->
@@ -351,6 +396,9 @@ export default {
351
396
  const currentSystemPrompt = ref('')
352
397
  const showSystemPrompt = ref(false)
353
398
  const messagesContainer = ref(null)
399
+ const isExporting = ref(false)
400
+ const isImporting = ref(false)
401
+ const fileInput = ref(null)
354
402
 
355
403
  // Auto-scroll to bottom when new messages arrive
356
404
  const scrollToBottom = async () => {
@@ -412,6 +460,129 @@ export default {
412
460
  }))
413
461
  })
414
462
 
463
+ async function exportThreads() {
464
+ if (isExporting.value) return
465
+
466
+ isExporting.value = true
467
+ try {
468
+ // Load all threads from IndexedDB
469
+ await threads.loadThreads()
470
+ const allThreads = threads.threads.value
471
+
472
+ // Create export data with metadata
473
+ const exportData = {
474
+ exportedAt: new Date().toISOString(),
475
+ version: '1.0',
476
+ source: 'llms.py',
477
+ threadCount: allThreads.length,
478
+ threads: allThreads
479
+ }
480
+
481
+ // Create and download JSON file
482
+ const jsonString = JSON.stringify(exportData, null, 2)
483
+ const blob = new Blob([jsonString], { type: 'application/json' })
484
+ const url = URL.createObjectURL(blob)
485
+
486
+ const link = document.createElement('a')
487
+ link.href = url
488
+ link.download = `llms-threads-export-${new Date().toISOString().split('T')[0]}.json`
489
+ document.body.appendChild(link)
490
+ link.click()
491
+ document.body.removeChild(link)
492
+ URL.revokeObjectURL(url)
493
+
494
+ } catch (error) {
495
+ console.error('Failed to export threads:', error)
496
+ alert('Failed to export threads: ' + error.message)
497
+ } finally {
498
+ isExporting.value = false
499
+ }
500
+ }
501
+
502
+ function triggerImport() {
503
+ if (isImporting.value) return
504
+ fileInput.value?.click()
505
+ }
506
+
507
+ async function handleFileImport(event) {
508
+ const file = event.target.files?.[0]
509
+ if (!file) return
510
+
511
+ isImporting.value = true
512
+ try {
513
+ const text = await file.text()
514
+ const importData = JSON.parse(text)
515
+
516
+ // Validate import data structure
517
+ if (!importData.threads || !Array.isArray(importData.threads)) {
518
+ throw new Error('Invalid import file: missing or invalid threads array')
519
+ }
520
+
521
+ // Import threads one by one
522
+ let importedCount = 0
523
+ let updatedCount = 0
524
+
525
+ for (const threadData of importData.threads) {
526
+ if (!threadData.id) {
527
+ console.warn('Skipping thread without ID:', threadData)
528
+ continue
529
+ }
530
+
531
+ try {
532
+ // Check if thread already exists
533
+ const existingThread = await threads.getThread(threadData.id)
534
+
535
+ if (existingThread) {
536
+ // Update existing thread
537
+ await threads.updateThread(threadData.id, {
538
+ title: threadData.title,
539
+ model: threadData.model,
540
+ systemPrompt: threadData.systemPrompt,
541
+ messages: threadData.messages || [],
542
+ createdAt: threadData.createdAt,
543
+ // Keep the existing updatedAt or use imported one
544
+ updatedAt: threadData.updatedAt || existingThread.updatedAt
545
+ })
546
+ updatedCount++
547
+ } else {
548
+ // Add new thread directly to IndexedDB
549
+ await threads.initDB()
550
+ const db = await threads.initDB()
551
+ const tx = db.transaction(['threads'], 'readwrite')
552
+ await tx.objectStore('threads').add({
553
+ id: threadData.id,
554
+ title: threadData.title || 'Imported Chat',
555
+ model: threadData.model || '',
556
+ systemPrompt: threadData.systemPrompt || '',
557
+ messages: threadData.messages || [],
558
+ createdAt: threadData.createdAt || new Date().toISOString(),
559
+ updatedAt: threadData.updatedAt || new Date().toISOString()
560
+ })
561
+ await tx.complete
562
+ importedCount++
563
+ }
564
+ } catch (error) {
565
+ console.error('Failed to import thread:', threadData.id, error)
566
+ }
567
+ }
568
+
569
+ // Reload threads to reflect changes
570
+ await threads.loadThreads()
571
+
572
+ alert(`Import completed!\nNew threads: ${importedCount}\nUpdated threads: ${updatedCount}`)
573
+
574
+ } catch (error) {
575
+ console.error('Failed to import threads:', error)
576
+ alert('Failed to import threads: ' + error.message)
577
+ } finally {
578
+ isImporting.value = false
579
+ // Clear the file input
580
+ if (fileInput.value) {
581
+ fileInput.value.value = ''
582
+ }
583
+ }
584
+ }
585
+
415
586
  function configUpdated() {
416
587
  console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
417
588
  if (selectedModel.value && !models.includes(selectedModel.value)) {
@@ -466,6 +637,12 @@ export default {
466
637
  toggleReasoning,
467
638
  formatReasoning,
468
639
  configUpdated,
640
+ exportThreads,
641
+ isExporting,
642
+ triggerImport,
643
+ handleFileImport,
644
+ isImporting,
645
+ fileInput,
469
646
  }
470
647
  }
471
648
  }
@@ -457,6 +457,9 @@
457
457
  .mb-4 {
458
458
  margin-bottom: calc(var(--spacing) * 4);
459
459
  }
460
+ .mb-6 {
461
+ margin-bottom: calc(var(--spacing) * 6);
462
+ }
460
463
  .-ml-px {
461
464
  margin-left: -1px;
462
465
  }
@@ -2245,6 +2248,11 @@
2245
2248
  color: var(--color-slate-500);
2246
2249
  }
2247
2250
  }
2251
+ .disabled\:opacity-50 {
2252
+ &:disabled {
2253
+ opacity: 50%;
2254
+ }
2255
+ }
2248
2256
  .disabled\:shadow-none {
2249
2257
  &:disabled {
2250
2258
  --tw-shadow: 0 0 #0000;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes