@blueharford/scrypted-spatial-awareness 0.6.25 → 0.6.26

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.
package/dist/plugin.zip CHANGED
Binary file
@@ -35407,25 +35407,23 @@ class SpatialReasoningEngine {
35407
35407
  llmSearched = false;
35408
35408
  llmProvider = null;
35409
35409
  llmProviderType = 'unknown';
35410
- /** Find or initialize LLM device - looks for ChatCompletion interface from @scrypted/llm plugin */
35411
- async findLlmDevice() {
35412
- if (this.llmDevice)
35413
- return this.llmDevice;
35410
+ // Load balancing for multiple LLMs
35411
+ llmDevices = [];
35412
+ llmIndex = 0;
35413
+ /** Find ALL LLM devices for load balancing */
35414
+ async findAllLlmDevices() {
35414
35415
  if (this.llmSearched)
35415
- return null; // Already searched and found nothing
35416
+ return;
35416
35417
  this.llmSearched = true;
35417
35418
  try {
35418
- // Look for devices with ChatCompletion interface (the correct interface for @scrypted/llm)
35419
35419
  for (const id of Object.keys(systemManager.getSystemState())) {
35420
35420
  const device = systemManager.getDeviceById(id);
35421
35421
  if (!device)
35422
35422
  continue;
35423
- // Check if this device has ChatCompletion interface
35424
- // The @scrypted/llm plugin exposes ChatCompletion, not ObjectDetection
35425
35423
  if (device.interfaces?.includes('ChatCompletion')) {
35426
35424
  const deviceName = device.name?.toLowerCase() || '';
35427
35425
  const pluginId = device.pluginId?.toLowerCase() || '';
35428
- // Identify the provider type for logging and image format selection
35426
+ // Identify the provider type for image format selection
35429
35427
  let providerType = 'Unknown';
35430
35428
  let providerTypeEnum = 'unknown';
35431
35429
  if (deviceName.includes('openai') || deviceName.includes('gpt')) {
@@ -35438,38 +35436,96 @@ class SpatialReasoningEngine {
35438
35436
  }
35439
35437
  else if (deviceName.includes('ollama')) {
35440
35438
  providerType = 'Ollama';
35441
- providerTypeEnum = 'openai'; // Ollama uses OpenAI-compatible format
35439
+ providerTypeEnum = 'openai';
35442
35440
  }
35443
35441
  else if (deviceName.includes('gemini') || deviceName.includes('google')) {
35444
35442
  providerType = 'Google';
35445
- providerTypeEnum = 'openai'; // Google uses OpenAI-compatible format
35443
+ providerTypeEnum = 'openai';
35446
35444
  }
35447
35445
  else if (deviceName.includes('llama')) {
35448
35446
  providerType = 'llama.cpp';
35449
- providerTypeEnum = 'openai'; // llama.cpp uses OpenAI-compatible format
35447
+ providerTypeEnum = 'openai';
35450
35448
  }
35451
35449
  else if (pluginId.includes('@scrypted/llm') || pluginId.includes('llm')) {
35452
35450
  providerType = 'Scrypted LLM';
35453
35451
  providerTypeEnum = 'unknown';
35454
35452
  }
35455
- this.llmDevice = device;
35456
- this.llmProvider = `${providerType} (${device.name})`;
35457
- this.llmProviderType = providerTypeEnum;
35458
- this.console.log(`[LLM] Connected to ${providerType}: ${device.name}`);
35459
- this.console.log(`[LLM] Plugin: ${pluginId || 'N/A'}`);
35460
- this.console.log(`[LLM] Image format: ${providerTypeEnum}`);
35461
- this.console.log(`[LLM] Interfaces: ${device.interfaces?.join(', ')}`);
35462
- return this.llmDevice;
35453
+ this.llmDevices.push({
35454
+ device: device,
35455
+ id,
35456
+ name: device.name || id,
35457
+ providerType: providerTypeEnum,
35458
+ lastUsed: 0,
35459
+ errorCount: 0,
35460
+ });
35461
+ this.console.log(`[LLM] Found ${providerType}: ${device.name}`);
35463
35462
  }
35464
35463
  }
35465
- // If we get here, no LLM plugin found
35466
- this.console.warn('[LLM] No ChatCompletion device found. Install @scrypted/llm for enhanced descriptions.');
35467
- this.console.warn('[LLM] Falling back to rule-based descriptions using topology data.');
35464
+ if (this.llmDevices.length === 0) {
35465
+ this.console.warn('[LLM] No ChatCompletion devices found. Install @scrypted/llm for enhanced descriptions.');
35466
+ }
35467
+ else {
35468
+ this.console.log(`[LLM] Load balancing across ${this.llmDevices.length} LLM device(s)`);
35469
+ }
35468
35470
  }
35469
35471
  catch (e) {
35470
- this.console.error('[LLM] Error searching for LLM device:', e);
35472
+ this.console.error('[LLM] Error searching for LLM devices:', e);
35473
+ }
35474
+ }
35475
+ /** Get the next available LLM using round-robin with least-recently-used preference */
35476
+ async findLlmDevice() {
35477
+ await this.findAllLlmDevices();
35478
+ if (this.llmDevices.length === 0)
35479
+ return null;
35480
+ // If only one LLM, just use it
35481
+ if (this.llmDevices.length === 1) {
35482
+ const llm = this.llmDevices[0];
35483
+ this.llmDevice = llm.device;
35484
+ this.llmProvider = llm.name;
35485
+ this.llmProviderType = llm.providerType;
35486
+ return llm.device;
35487
+ }
35488
+ // Find the LLM with the oldest lastUsed time (least recently used)
35489
+ // Also prefer LLMs with fewer errors
35490
+ let bestIndex = 0;
35491
+ let bestScore = Infinity;
35492
+ for (let i = 0; i < this.llmDevices.length; i++) {
35493
+ const llm = this.llmDevices[i];
35494
+ // Score = lastUsed time + (errorCount * 60 seconds penalty)
35495
+ const score = llm.lastUsed + (llm.errorCount * 60000);
35496
+ if (score < bestScore) {
35497
+ bestScore = score;
35498
+ bestIndex = i;
35499
+ }
35500
+ }
35501
+ const selected = this.llmDevices[bestIndex];
35502
+ this.llmDevice = selected.device;
35503
+ this.llmProvider = selected.name;
35504
+ this.llmProviderType = selected.providerType;
35505
+ this.console.log(`[LLM] Selected: ${selected.name} (last used ${Math.round((Date.now() - selected.lastUsed) / 1000)}s ago, errors: ${selected.errorCount})`);
35506
+ return selected.device;
35507
+ }
35508
+ /** Mark an LLM as used (for load balancing) */
35509
+ markLlmUsed(device) {
35510
+ const llm = this.llmDevices.find(l => l.device === device);
35511
+ if (llm) {
35512
+ llm.lastUsed = Date.now();
35513
+ }
35514
+ }
35515
+ /** Mark an LLM as having an error (for load balancing - will be deprioritized) */
35516
+ markLlmError(device) {
35517
+ const llm = this.llmDevices.find(l => l.device === device);
35518
+ if (llm) {
35519
+ llm.errorCount++;
35520
+ this.console.log(`[LLM] ${llm.name} error count: ${llm.errorCount}`);
35521
+ }
35522
+ }
35523
+ /** Reset error count for an LLM after successful call */
35524
+ markLlmSuccess(device) {
35525
+ const llm = this.llmDevices.find(l => l.device === device);
35526
+ if (llm && llm.errorCount > 0) {
35527
+ llm.errorCount = Math.max(0, llm.errorCount - 1); // Gradually reduce error count
35471
35528
  }
35472
- return null;
35473
35529
  }
35474
35530
  /** Get the current LLM provider name */
35475
35531
  getLlmProvider() {
@@ -35804,6 +35860,8 @@ class SpatialReasoningEngine {
35804
35860
  // Fallback to text-only if image conversion failed
35805
35861
  messageContent = prompt;
35806
35862
  }
35863
+ // Mark LLM as used for load balancing
35864
+ this.markLlmUsed(llm);
35807
35865
  // Call LLM using ChatCompletion interface
35808
35866
  const result = await llm.getChatCompletion({
35809
35867
  messages: [
@@ -35818,12 +35876,14 @@ class SpatialReasoningEngine {
35818
35876
  // Extract description from ChatCompletion result
35819
35877
  const content = result?.choices?.[0]?.message?.content;
35820
35878
  if (content && typeof content === 'string') {
35879
+ this.markLlmSuccess(llm);
35821
35880
  return content.trim();
35822
35881
  }
35823
35882
  return null;
35824
35883
  }
35825
35884
  catch (e) {
35826
35885
  this.console.warn('LLM description generation failed:', e);
35886
+ this.markLlmError(llm);
35827
35887
  return null;
35828
35888
  }
35829
35889
  }
@@ -35892,6 +35952,8 @@ Examples of good descriptions:
35892
35952
  - "Landscaper with leaf blower heading to work truck"
35893
35953
 
35894
35954
  Generate ONLY the description, nothing else:`;
35955
+ // Mark LLM as used for load balancing
35956
+ this.markLlmUsed(llm);
35895
35957
  // Try multimodal format first, fall back to text-only if it fails
35896
35958
  let result;
35897
35959
  let usedVision = false;
@@ -35942,6 +36004,7 @@ Generate ONLY the description, nothing else:`;
35942
36004
  const content = result?.choices?.[0]?.message?.content;
35943
36005
  if (content && typeof content === 'string') {
35944
36006
  this.console.log(`[LLM] Got ${eventType} description (vision=${usedVision}): ${content.trim().substring(0, 50)}...`);
36007
+ this.markLlmSuccess(llm);
35945
36008
  return content.trim();
35946
36009
  }
35947
36010
  this.console.warn(`[LLM] No content in response for ${eventType}`);
@@ -35949,6 +36012,7 @@ Generate ONLY the description, nothing else:`;
35949
36012
  }
35950
36013
  catch (e) {
35951
36014
  this.console.warn(`[LLM] ${eventType} description generation failed:`, e);
36015
+ this.markLlmError(llm);
35952
36016
  return null;
35953
36017
  }
35954
36018
  }
@@ -36386,12 +36450,12 @@ class TopologyDiscoveryEngine {
36386
36450
  isEnabled() {
36387
36451
  return this.config.discoveryIntervalHours > 0;
36388
36452
  }
36389
- /** Find LLM device with ChatCompletion interface */
36390
- async findLlmDevice() {
36391
- if (this.llmDevice)
36392
- return this.llmDevice;
36453
+ // Load balancing for multiple LLMs
36454
+ llmDevices = [];
36455
+ /** Find ALL LLM devices for load balancing */
36456
+ async findAllLlmDevices() {
36393
36457
  if (this.llmSearched)
36394
- return null;
36458
+ return;
36395
36459
  this.llmSearched = true;
36396
36460
  try {
36397
36461
  for (const id of Object.keys(systemManager.getSystemState())) {
@@ -36400,33 +36464,77 @@ class TopologyDiscoveryEngine {
36400
36464
  continue;
36401
36465
  if (device.interfaces?.includes('ChatCompletion')) {
36402
36466
  const deviceName = device.name?.toLowerCase() || '';
36403
- // Detect provider type for image format selection
36467
+ let providerType = 'unknown';
36404
36468
  if (deviceName.includes('openai') || deviceName.includes('gpt')) {
36405
- this.llmProviderType = 'openai';
36469
+ providerType = 'openai';
36406
36470
  }
36407
36471
  else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
36408
- this.llmProviderType = 'anthropic';
36472
+ providerType = 'anthropic';
36409
36473
  }
36410
36474
  else if (deviceName.includes('ollama') || deviceName.includes('gemini') ||
36411
36475
  deviceName.includes('google') || deviceName.includes('llama')) {
36412
- // These providers use OpenAI-compatible format
36413
- this.llmProviderType = 'openai';
36414
- }
36415
- else {
36416
- this.llmProviderType = 'unknown';
36476
+ providerType = 'openai';
36417
36477
  }
36418
- this.llmDevice = device;
36419
- this.console.log(`[Discovery] Connected to LLM: ${device.name}`);
36420
- this.console.log(`[Discovery] Image format: ${this.llmProviderType}`);
36421
- return this.llmDevice;
36478
+ this.llmDevices.push({
36479
+ device: device,
36480
+ id,
36481
+ name: device.name || id,
36482
+ providerType,
36483
+ lastUsed: 0,
36484
+ errorCount: 0,
36485
+ });
36486
+ this.console.log(`[Discovery] Found LLM: ${device.name}`);
36422
36487
  }
36423
36488
  }
36424
- this.console.warn('[Discovery] No ChatCompletion device found. Vision-based discovery unavailable.');
36489
+ if (this.llmDevices.length === 0) {
36490
+ this.console.warn('[Discovery] No ChatCompletion devices found. Vision-based discovery unavailable.');
36491
+ }
36492
+ else {
36493
+ this.console.log(`[Discovery] Load balancing across ${this.llmDevices.length} LLM device(s)`);
36494
+ }
36425
36495
  }
36426
36496
  catch (e) {
36427
- this.console.error('[Discovery] Error finding LLM device:', e);
36497
+ this.console.error('[Discovery] Error finding LLM devices:', e);
36498
+ }
36499
+ }
36500
+ /** Find LLM device with ChatCompletion interface - uses load balancing */
36501
+ async findLlmDevice() {
36502
+ await this.findAllLlmDevices();
36503
+ if (this.llmDevices.length === 0)
36504
+ return null;
36505
+ // If only one LLM, just use it
36506
+ if (this.llmDevices.length === 1) {
36507
+ const llm = this.llmDevices[0];
36508
+ this.llmDevice = llm.device;
36509
+ this.llmProviderType = llm.providerType;
36510
+ return llm.device;
36511
+ }
36512
+ // Find the LLM with oldest lastUsed time (least recently used)
36513
+ let bestIndex = 0;
36514
+ let bestScore = Infinity;
36515
+ for (let i = 0; i < this.llmDevices.length; i++) {
36516
+ const llm = this.llmDevices[i];
36517
+ const score = llm.lastUsed + (llm.errorCount * 60000);
36518
+ if (score < bestScore) {
36519
+ bestScore = score;
36520
+ bestIndex = i;
36521
+ }
36522
+ }
36523
+ const selected = this.llmDevices[bestIndex];
36524
+ this.llmDevice = selected.device;
36525
+ this.llmProviderType = selected.providerType;
36526
+ // Mark as used
36527
+ selected.lastUsed = Date.now();
36528
+ this.console.log(`[Discovery] Selected LLM: ${selected.name}`);
36529
+ return selected.device;
36530
+ }
36531
+ /** Mark an LLM as having an error */
36532
+ markLlmError(device) {
36533
+ const llm = this.llmDevices.find(l => l.device === device);
36534
+ if (llm) {
36535
+ llm.errorCount++;
36536
+ this.console.log(`[Discovery] ${llm.name} error count: ${llm.errorCount}`);
36428
36537
  }
36429
- return null;
36430
36538
  }
36431
36539
  /** Get camera snapshot as ImageData */
36432
36540
  async getCameraSnapshot(cameraId) {
@@ -36602,6 +36710,10 @@ Use the mount height to help estimate distances - objects at ground level will a
36602
36710
  }
36603
36711
  // All formats failed
36604
36712
  if (lastError) {
36713
+ // Track error for load balancing
36714
+ if (llm) {
36715
+ this.markLlmError(llm);
36716
+ }
36605
36717
  const errorStr = String(lastError);
36606
36718
  if ((0, spatial_reasoning_1.isVisionFormatError)(lastError)) {
36607
36719
  analysis.error = 'Vision/image analysis failed with all formats. Ensure you have a vision-capable model (e.g., gpt-4o, gpt-4-turbo, claude-3-sonnet) configured and the @scrypted/llm plugin supports vision.';