@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/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +157 -45
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/spatial-reasoning.ts +108 -26
- package/src/core/topology-discovery.ts +84 -19
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -35407,25 +35407,23 @@ class SpatialReasoningEngine {
|
|
|
35407
35407
|
llmSearched = false;
|
|
35408
35408
|
llmProvider = null;
|
|
35409
35409
|
llmProviderType = 'unknown';
|
|
35410
|
-
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
|
|
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
|
|
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
|
|
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';
|
|
35439
|
+
providerTypeEnum = 'openai';
|
|
35442
35440
|
}
|
|
35443
35441
|
else if (deviceName.includes('gemini') || deviceName.includes('google')) {
|
|
35444
35442
|
providerType = 'Google';
|
|
35445
|
-
providerTypeEnum = 'openai';
|
|
35443
|
+
providerTypeEnum = 'openai';
|
|
35446
35444
|
}
|
|
35447
35445
|
else if (deviceName.includes('llama')) {
|
|
35448
35446
|
providerType = 'llama.cpp';
|
|
35449
|
-
providerTypeEnum = 'openai';
|
|
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.
|
|
35456
|
-
|
|
35457
|
-
|
|
35458
|
-
|
|
35459
|
-
|
|
35460
|
-
|
|
35461
|
-
|
|
35462
|
-
|
|
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
|
-
|
|
35466
|
-
|
|
35467
|
-
|
|
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
|
|
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
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
36392
|
-
|
|
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
|
|
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
|
-
|
|
36467
|
+
let providerType = 'unknown';
|
|
36404
36468
|
if (deviceName.includes('openai') || deviceName.includes('gpt')) {
|
|
36405
|
-
|
|
36469
|
+
providerType = 'openai';
|
|
36406
36470
|
}
|
|
36407
36471
|
else if (deviceName.includes('anthropic') || deviceName.includes('claude')) {
|
|
36408
|
-
|
|
36472
|
+
providerType = 'anthropic';
|
|
36409
36473
|
}
|
|
36410
36474
|
else if (deviceName.includes('ollama') || deviceName.includes('gemini') ||
|
|
36411
36475
|
deviceName.includes('google') || deviceName.includes('llama')) {
|
|
36412
|
-
|
|
36413
|
-
this.llmProviderType = 'openai';
|
|
36414
|
-
}
|
|
36415
|
-
else {
|
|
36416
|
-
this.llmProviderType = 'unknown';
|
|
36476
|
+
providerType = 'openai';
|
|
36417
36477
|
}
|
|
36418
|
-
this.
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
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.
|
|
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
|
|
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.';
|