rhombus-node-mcp 0.1.15 → 0.1.17
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/README.md +9 -1
- package/dist/api/camera-tool-api.js +161 -0
- package/dist/api/clips-tool-api.js +36 -0
- package/dist/api/create-camera-policy-tool-api.js +9 -0
- package/dist/api/create-tool-api.js +53 -0
- package/dist/api/entity-lookup-tool-api.js +65 -0
- package/dist/api/events-tool-api.js +320 -0
- package/dist/api/faces-tool-api.js +110 -0
- package/dist/api/get-entity-tool-api.js +176 -0
- package/dist/api/get-org-information-tool-api.js +9 -0
- package/dist/api/location-tool-api.js +9 -0
- package/dist/api/lpr-tool-api.js +68 -0
- package/dist/api/policy-alerts-tool-api.js +42 -0
- package/dist/api/reboot-cameras-tool-api.js +34 -0
- package/dist/api/report-tool-api.js +426 -0
- package/dist/api/time-tool-api.js +90 -0
- package/dist/api/update-tool-api.js +148 -0
- package/dist/createServer.js +7 -1
- package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
- package/dist/disabled-tools/semantic-search-tool.js +90 -0
- package/dist/index.js +3 -5
- package/dist/logger.js +4 -3
- package/dist/network.js +42 -15
- package/dist/resources/routes.json.js +1 -1
- package/dist/services/embedding-service.js +153 -0
- package/dist/services/faiss-search-service.js +261 -0
- package/dist/tools/camera-tool.js +100 -0
- package/dist/tools/clips-tool.js +23 -37
- package/dist/tools/count-tool.js +25 -0
- package/dist/tools/create-camera-policy-tool.js +214 -0
- package/dist/tools/create-tool.js +25 -74
- package/dist/tools/entity-lookup-tool.js +36 -0
- package/dist/tools/events-tool.js +188 -92
- package/dist/tools/faces-tool.js +59 -132
- package/dist/tools/get-entity-tool.js +78 -0
- package/dist/tools/get-org-information-tool.js +18 -0
- package/dist/tools/location-tool.js +24 -31
- package/dist/tools/lpr-tool.js +79 -0
- package/dist/tools/policy-alerts-tool.js +34 -42
- package/dist/tools/reboot-cameras-tool.js +34 -0
- package/dist/tools/report-tool.js +190 -0
- package/dist/tools/time-conversion-tool.js +43 -0
- package/dist/tools/time-tool.js +17 -57
- package/dist/tools/update-tool.js +262 -0
- package/dist/transports/streamable-http.js +160 -57
- package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +52 -0
- package/dist/types/clips-tool-types.js +41 -0
- package/dist/types/create-camera-policy-tool-types.js +44 -0
- package/dist/types/create-tool-types.js +8 -0
- package/dist/types/deviceType.js +1 -0
- package/dist/types/endpoint-to-keys-tool-types.js +7 -0
- package/dist/types/entity-lookup-tool-types.js +70 -0
- package/dist/types/events-tools-types.js +257 -0
- package/dist/types/faces-tools-types.js +143 -0
- package/dist/types/get-entity-tool-types.js +25 -0
- package/dist/types/get-org-information-tool-types.js +3 -0
- package/dist/types/location-tool-types.js +11 -0
- package/dist/types/lpr-tool-types.js +97 -0
- package/dist/types/policy-alerts-tool-types.js +74 -0
- package/dist/types/reboot-cameras-tool-types.js +8 -0
- package/dist/types/report-tool-types.js +268 -0
- package/dist/types/schema-components.js +7093 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/semantic-search-tool-types.js +5 -0
- package/dist/types/time-conversion-tool-types.js +8 -0
- package/dist/types/time-tool-types.js +11 -0
- package/dist/types/update-tool-types.js +186 -0
- package/dist/types/zod-schemas.js +21315 -0
- package/dist/types.js +17 -7
- package/dist/util.js +94 -2
- package/dist/utils/confirmation.js +1 -1
- package/dist/utils/reduce-output.js +35 -0
- package/dist/utils/remove-nulls.js +28 -0
- package/dist/utils/temp.js +8 -0
- package/dist/utils/timestampInput.js +12 -0
- package/package.json +23 -3
- package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
- package/dist/tools/devices/get-entity-tool.js +0 -118
- package/dist/tools/get-org-information.js +0 -17
- package/dist/tools/reboot-cameras.js +0 -62
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import faiss from 'faiss-node';
|
|
5
|
+
import { EmbeddingService } from './embedding-service.js';
|
|
6
|
+
// Constants following coding style guidelines
|
|
7
|
+
const FAISS_INDEX_TYPE = 'IndexFlatIP'; // Inner Product for cosine similarity
|
|
8
|
+
const SEARCH_RESULTS_LIMIT = 50;
|
|
9
|
+
const SIMILARITY_THRESHOLD = 0.3;
|
|
10
|
+
const EMBEDDING_DIMENSIONS = 1536; // text-embedding-3-small dimensions
|
|
11
|
+
// Get project root directory - resolves relative to the current module file
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
15
|
+
const EMBEDDINGS_DATA_PATH = path.join(PROJECT_ROOT, 'data/embeddings/embeddings.json');
|
|
16
|
+
const FAISS_INDEX_PATH = path.join(PROJECT_ROOT, 'data/embeddings/faiss.index');
|
|
17
|
+
export class FaissSearchService {
|
|
18
|
+
index = null;
|
|
19
|
+
embeddings = [];
|
|
20
|
+
embeddingService;
|
|
21
|
+
constructor() {
|
|
22
|
+
this.embeddingService = new EmbeddingService();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load embeddings from file and build FAISS index
|
|
26
|
+
* @param embeddingsPath - Path to embeddings.json file (optional, defaults to project data directory)
|
|
27
|
+
*/
|
|
28
|
+
async loadEmbeddings(embeddingsPath) {
|
|
29
|
+
const resolvedEmbeddingsPath = embeddingsPath || EMBEDDINGS_DATA_PATH;
|
|
30
|
+
console.log(`📚 Loading embeddings from ${resolvedEmbeddingsPath}...`);
|
|
31
|
+
try {
|
|
32
|
+
const fileContent = await fs.readFile(resolvedEmbeddingsPath, 'utf-8');
|
|
33
|
+
this.embeddings = JSON.parse(fileContent);
|
|
34
|
+
if (!Array.isArray(this.embeddings)) {
|
|
35
|
+
throw new Error('Embeddings file does not contain a valid array');
|
|
36
|
+
}
|
|
37
|
+
console.log(`✅ Loaded ${this.embeddings.length} embeddings`);
|
|
38
|
+
// Validate embedding dimensions
|
|
39
|
+
if (this.embeddings.length > 0) {
|
|
40
|
+
const firstEmbedding = this.embeddings[0];
|
|
41
|
+
if (!firstEmbedding.embedding || firstEmbedding.embedding.length !== EMBEDDING_DIMENSIONS) {
|
|
42
|
+
throw new Error(`Invalid embedding dimensions. Expected ${EMBEDDING_DIMENSIONS}, got ${firstEmbedding.embedding?.length || 0}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error(`Failed to load embeddings: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build FAISS index from loaded embeddings
|
|
52
|
+
*/
|
|
53
|
+
async buildIndex() {
|
|
54
|
+
if (this.embeddings.length === 0) {
|
|
55
|
+
throw new Error('No embeddings loaded. Call loadEmbeddings() first.');
|
|
56
|
+
}
|
|
57
|
+
console.log(`🏗️ Building FAISS index with ${this.embeddings.length} vectors...`);
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
try {
|
|
60
|
+
// Create FAISS index for inner product (cosine similarity with normalized vectors)
|
|
61
|
+
this.index = new faiss.IndexFlatIP(EMBEDDING_DIMENSIONS);
|
|
62
|
+
// Prepare embeddings matrix
|
|
63
|
+
const embeddingMatrix = new Float32Array(this.embeddings.length * EMBEDDING_DIMENSIONS);
|
|
64
|
+
for (let i = 0; i < this.embeddings.length; i++) {
|
|
65
|
+
const embedding = this.embeddings[i].embedding;
|
|
66
|
+
// Normalize the embedding for cosine similarity
|
|
67
|
+
const normalizedEmbedding = this.normalizeVector(embedding);
|
|
68
|
+
// Copy normalized embedding to matrix
|
|
69
|
+
for (let j = 0; j < EMBEDDING_DIMENSIONS; j++) {
|
|
70
|
+
embeddingMatrix[i * EMBEDDING_DIMENSIONS + j] = normalizedEmbedding[j];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Add vectors to index
|
|
74
|
+
this.index.add(Array.from(embeddingMatrix));
|
|
75
|
+
const buildTime = Date.now() - startTime;
|
|
76
|
+
console.log(`✅ FAISS index built successfully in ${buildTime}ms`);
|
|
77
|
+
console.log(` 📊 Index type: ${FAISS_INDEX_TYPE}`);
|
|
78
|
+
console.log(` 📏 Dimensions: ${EMBEDDING_DIMENSIONS}`);
|
|
79
|
+
console.log(` 🔢 Total vectors: ${this.index.ntotal()}`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`Failed to build FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Save FAISS index to file
|
|
87
|
+
* @param indexPath - Path to save the index file
|
|
88
|
+
*/
|
|
89
|
+
async saveIndex(indexPath) {
|
|
90
|
+
if (!this.index) {
|
|
91
|
+
throw new Error('No index built. Call buildIndex() first.');
|
|
92
|
+
}
|
|
93
|
+
console.log(`💾 Saving FAISS index to ${indexPath}...`);
|
|
94
|
+
try {
|
|
95
|
+
// Ensure directory exists
|
|
96
|
+
const indexDir = path.dirname(indexPath);
|
|
97
|
+
await fs.mkdir(indexDir, { recursive: true });
|
|
98
|
+
// Save the index
|
|
99
|
+
this.index.write(indexPath);
|
|
100
|
+
console.log(`✅ Index saved successfully`);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new Error(`Failed to save FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Load FAISS index from file
|
|
108
|
+
* @param indexPath - Path to the index file (optional, defaults to project data directory)
|
|
109
|
+
*/
|
|
110
|
+
async loadIndex(indexPath) {
|
|
111
|
+
const resolvedIndexPath = indexPath || FAISS_INDEX_PATH;
|
|
112
|
+
console.log(`📖 Loading FAISS index from ${resolvedIndexPath}...`);
|
|
113
|
+
try {
|
|
114
|
+
this.index = faiss.IndexFlatIP.read(resolvedIndexPath);
|
|
115
|
+
console.log(`✅ Index loaded successfully`);
|
|
116
|
+
console.log(` 📏 Dimensions: ${EMBEDDING_DIMENSIONS}`);
|
|
117
|
+
console.log(` 🔢 Total vectors: ${this.index.ntotal()}`);
|
|
118
|
+
// Note: IndexFlatIP doesn't expose dimension property directly
|
|
119
|
+
// We validate using our known dimensions constant
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new Error(`Failed to load FAISS index: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Search for similar vectors using natural language query
|
|
127
|
+
* @param params - Search parameters
|
|
128
|
+
* @returns Search results with metadata
|
|
129
|
+
*/
|
|
130
|
+
async search(params) {
|
|
131
|
+
if (!this.index) {
|
|
132
|
+
throw new Error('No index loaded. Call loadIndex() or buildIndex() first.');
|
|
133
|
+
}
|
|
134
|
+
if (this.embeddings.length === 0) {
|
|
135
|
+
throw new Error('No embeddings metadata loaded. Call loadEmbeddings() first.');
|
|
136
|
+
}
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
try {
|
|
139
|
+
// Generate embedding for the query
|
|
140
|
+
console.log(`🔍 Searching for: "${params.query}"`);
|
|
141
|
+
const queryEmbeddingResult = await this.embeddingService.generateSingleEmbedding(params.query, `query-${Date.now()}`);
|
|
142
|
+
// Normalize query embedding for cosine similarity
|
|
143
|
+
const normalizedQueryEmbedding = this.normalizeVector(queryEmbeddingResult.embedding);
|
|
144
|
+
// Determine search parameters
|
|
145
|
+
const searchLimit = Math.min(params.limit || SEARCH_RESULTS_LIMIT, this.embeddings.length);
|
|
146
|
+
const similarityThreshold = params.minSimilarity || SIMILARITY_THRESHOLD;
|
|
147
|
+
// Perform FAISS search
|
|
148
|
+
const searchResults = this.index.search(normalizedQueryEmbedding, searchLimit);
|
|
149
|
+
// Process results
|
|
150
|
+
const results = [];
|
|
151
|
+
let filteredCount = 0;
|
|
152
|
+
for (let i = 0; i < searchResults.labels.length; i++) {
|
|
153
|
+
const vectorIndex = searchResults.labels[i];
|
|
154
|
+
const similarity = searchResults.distances[i]; // Inner product similarity
|
|
155
|
+
// Skip invalid indices
|
|
156
|
+
if (vectorIndex < 0 || vectorIndex >= this.embeddings.length) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Apply similarity threshold
|
|
160
|
+
if (similarity < similarityThreshold) {
|
|
161
|
+
filteredCount++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const embedding = this.embeddings[vectorIndex];
|
|
165
|
+
// Apply URL pattern filter if specified
|
|
166
|
+
if (params.urlPattern && !embedding.sourceUrl.includes(params.urlPattern)) {
|
|
167
|
+
filteredCount++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
results.push({
|
|
171
|
+
chunkText: embedding.chunkText,
|
|
172
|
+
sourceUrl: embedding.sourceUrl,
|
|
173
|
+
sourceTitle: embedding.sourceTitle,
|
|
174
|
+
similarity: similarity,
|
|
175
|
+
chunkId: embedding.chunkId,
|
|
176
|
+
chunkIndex: embedding.chunkIndex,
|
|
177
|
+
tokenCount: embedding.tokenCount,
|
|
178
|
+
metadata: embedding.metadata,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const searchTime = Date.now() - startTime;
|
|
182
|
+
const stats = {
|
|
183
|
+
totalVectors: this.index.ntotal(),
|
|
184
|
+
searchTime,
|
|
185
|
+
resultsReturned: results.length,
|
|
186
|
+
resultsFiltered: filteredCount,
|
|
187
|
+
};
|
|
188
|
+
console.log(`✅ Search completed in ${searchTime}ms`);
|
|
189
|
+
console.log(` 🔍 Query tokens: ${queryEmbeddingResult.tokenCount}`);
|
|
190
|
+
console.log(` 📊 Results found: ${results.length}`);
|
|
191
|
+
console.log(` 🚫 Results filtered: ${filteredCount}`);
|
|
192
|
+
return { results, stats };
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get index statistics
|
|
200
|
+
*/
|
|
201
|
+
getIndexStats() {
|
|
202
|
+
if (!this.index) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
totalVectors: this.index.ntotal(),
|
|
207
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
208
|
+
indexType: FAISS_INDEX_TYPE,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Normalize vector for cosine similarity computation
|
|
213
|
+
* @param vector - Input vector
|
|
214
|
+
* @returns Normalized vector
|
|
215
|
+
*/
|
|
216
|
+
normalizeVector(vector) {
|
|
217
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
218
|
+
if (magnitude === 0) {
|
|
219
|
+
return vector.slice(); // Return copy of zero vector
|
|
220
|
+
}
|
|
221
|
+
return vector.map(val => val / magnitude);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Validate embeddings data integrity
|
|
225
|
+
*/
|
|
226
|
+
validateEmbeddings() {
|
|
227
|
+
const errors = [];
|
|
228
|
+
if (this.embeddings.length === 0) {
|
|
229
|
+
errors.push('No embeddings loaded');
|
|
230
|
+
return { isValid: false, errors };
|
|
231
|
+
}
|
|
232
|
+
// Check each embedding
|
|
233
|
+
for (let i = 0; i < Math.min(this.embeddings.length, 10); i++) { // Sample first 10
|
|
234
|
+
const embedding = this.embeddings[i];
|
|
235
|
+
if (!embedding.chunkId) {
|
|
236
|
+
errors.push(`Embedding ${i}: Missing chunkId`);
|
|
237
|
+
}
|
|
238
|
+
if (!embedding.embedding || embedding.embedding.length !== EMBEDDING_DIMENSIONS) {
|
|
239
|
+
errors.push(`Embedding ${i}: Invalid embedding dimensions`);
|
|
240
|
+
}
|
|
241
|
+
if (!embedding.chunkText || embedding.chunkText.trim().length === 0) {
|
|
242
|
+
errors.push(`Embedding ${i}: Missing or empty chunkText`);
|
|
243
|
+
}
|
|
244
|
+
if (!embedding.sourceUrl) {
|
|
245
|
+
errors.push(`Embedding ${i}: Missing sourceUrl`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
isValid: errors.length === 0,
|
|
250
|
+
errors,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Clean up resources
|
|
255
|
+
*/
|
|
256
|
+
dispose() {
|
|
257
|
+
// FAISS resources are automatically cleaned up
|
|
258
|
+
this.index = null;
|
|
259
|
+
this.embeddings = [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getLogger } from "../logger.js";
|
|
2
|
+
import { getCameraSettings, getImageForCameraAtTime } from "../api/camera-tool-api.js";
|
|
3
|
+
import { BASE_TOOL_ARGS } from "../types/camera-tool-types.js";
|
|
4
|
+
const TOOL_NAME = "camera-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool can perform some action pertaining to the video stream of a camera. There are two types of requests
|
|
7
|
+
that can be passed into "requestType":
|
|
8
|
+
- image
|
|
9
|
+
- get-settings
|
|
10
|
+
|
|
11
|
+
What follows is a description of the behavior of this tool given the requestType "image"
|
|
12
|
+
|
|
13
|
+
This tool should be used any time someone wants to specify a subset of cameras to use for a task, based on some features that the camera sees. For example, interior cameras, cameras facing the street, cameras with a view of X, Y, Z, etc.
|
|
14
|
+
|
|
15
|
+
For instance if someone says "I want X using cameras with Y" then this tool should get a snapshot of the image to answer the question of if the camera satisfies the Y predicate.
|
|
16
|
+
|
|
17
|
+
This tool captures and returns a real-time snapshot from a designated security camera.
|
|
18
|
+
The image reflects the current scene in the camera's field of view and serves as a contextual
|
|
19
|
+
input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
|
|
20
|
+
or situational assessment. When invoked, the tool provides the following:
|
|
21
|
+
• Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
|
|
22
|
+
|
|
23
|
+
What follows is a description of the behavior of this tool given the requestType "get-settings"
|
|
24
|
+
|
|
25
|
+
This tool retrieves the current configuration for a specified camera or associated device (e.g., sensor, access controller). The returned JSON object can include detailed camera settings (e.g., resolution, bitrate) and various device-specific configurations (e.g. storage settings).
|
|
26
|
+
|
|
27
|
+
NOTE: To update camera settings, use the update-tool instead.
|
|
28
|
+
`;
|
|
29
|
+
const logger = getLogger("camera-tool");
|
|
30
|
+
const TOOL_ARGS = BASE_TOOL_ARGS;
|
|
31
|
+
const TOOL_HANDLER = async (args, extra) => {
|
|
32
|
+
const { cameraUuid, timestampISO, requestType } = args;
|
|
33
|
+
if (!cameraUuid) {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: JSON.stringify({
|
|
39
|
+
needUserInput: true,
|
|
40
|
+
commandForUser: "Which camera are you talking about?",
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let response;
|
|
47
|
+
const timestampMs = timestampISO
|
|
48
|
+
? new Date(timestampISO).getTime()
|
|
49
|
+
: new Date().getTime() - 1000 * 60 * 5;
|
|
50
|
+
switch (requestType) {
|
|
51
|
+
case "image":
|
|
52
|
+
response = await getImageForCameraAtTime(cameraUuid, timestampMs, extra._meta?.requestModifiers, extra.sessionId);
|
|
53
|
+
if (!response.success || !response.imageData) {
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
logger.debug(`Received image response:\n ${JSON.stringify(response)}`);
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "image",
|
|
63
|
+
data: response.imageData,
|
|
64
|
+
mimeType: "image/jpeg",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: JSON.stringify({
|
|
69
|
+
success: true,
|
|
70
|
+
status: "image-attached",
|
|
71
|
+
cameraUuid,
|
|
72
|
+
timestampMs,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
case "get-settings":
|
|
78
|
+
response = await getCameraSettings(cameraUuid, extra._meta?.requestModifiers, extra.sessionId);
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
81
|
+
};
|
|
82
|
+
default:
|
|
83
|
+
response = {
|
|
84
|
+
error: true,
|
|
85
|
+
status: "missing unknown type from tool call",
|
|
86
|
+
};
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: JSON.stringify({ response }),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
export function createTool(server) {
|
|
99
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
|
|
100
|
+
}
|
package/dist/tools/clips-tool.js
CHANGED
|
@@ -1,37 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
.array(z.string())
|
|
6
|
-
.optional()
|
|
7
|
-
.describe("A list of UUIDs representing specific devices to filter clips by. Only clips emitted by these devices will be returned."),
|
|
8
|
-
locationUuidFilters: z
|
|
9
|
-
.array(z.string())
|
|
10
|
-
.optional()
|
|
11
|
-
.describe("A list of UUIDs representing specific locations to filter clips by. Only clips associated with these locations will be returned."),
|
|
12
|
-
searchFilter: z
|
|
13
|
-
.string()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe("A simple string to search for within the names of the clips."),
|
|
16
|
-
timestampMsAfter: z
|
|
17
|
-
.number()
|
|
18
|
-
.describe("The start of the time range (in milliseconds since epoch) for which to retrieve clips. Only clips that occurred AFTER this timestamp will be returned."),
|
|
19
|
-
timestampMsBefore: z
|
|
20
|
-
.number()
|
|
21
|
-
.describe("The end of the time range (in milliseconds since epoch) for which to retrieve clips. Only clips that occurred BEFORE this timestamp will be returned."),
|
|
22
|
-
});
|
|
23
|
-
async function getSavedClips(args, requestModifiers) {
|
|
24
|
-
return await postApi("/event/getClipsWithProgress", args, requestModifiers);
|
|
25
|
-
}
|
|
26
|
-
export function createTool(server) {
|
|
27
|
-
server.tool("clips-tool", `
|
|
1
|
+
import { ApiPayloadSchema, TOOL_ARGS } from "../types/clips-tool-types.js";
|
|
2
|
+
import { getSavedClips, getExpiringClips } from "../api/clips-tool-api.js";
|
|
3
|
+
const TOOL_NAME = "clips-tool";
|
|
4
|
+
const TOOL_DESCRIPTION = `
|
|
28
5
|
Retrieves saved video clips from the Rhombus system. Saved clips can be viewed for up to 2 years and are typically found in the "Clips" tab of the "Saved Video" section of the Rhombus Console.
|
|
29
6
|
|
|
30
7
|
This tool allows you to filter clips by:
|
|
8
|
+
* Whether or not they are expiring soon.
|
|
31
9
|
* Specific devices using their UUIDs.
|
|
32
10
|
* Specific locations using their UUIDs.
|
|
33
11
|
* A simple string search on clip names.
|
|
34
|
-
* A time range, specifying a start (
|
|
12
|
+
* A time range, specifying a start (timestampISOAfter) and/or end (timestampISOBefore) timestamp in ISO 8601 format.
|
|
35
13
|
|
|
36
14
|
The tool returns a JSON object with the following structure and important fields:
|
|
37
15
|
* **errorMsg (string | null):** An error message if the request failed.
|
|
@@ -43,19 +21,27 @@ The tool returns a JSON object with the following structure and important fields
|
|
|
43
21
|
* **description (string | null):** An optional description for the clip.
|
|
44
22
|
* **timestampMs (int64):** The start time of the video clip in milliseconds since epoch.
|
|
45
23
|
* **createdAtMs (int64):** The creation timestamp of the clip in milliseconds since epoch.
|
|
24
|
+
* **createdAtTimestamp (string):** The creation timestamp of the clip in ISO 8601 format.
|
|
46
25
|
* **deviceUuid (string):** The UUID of the primary device (e.g., camera) that recorded the clip.
|
|
47
26
|
* **deviceUuids (array of strings or null):** A list of UUIDs for all devices associated with the clip.
|
|
48
27
|
* **durationSec (int32):** The length of the video clip in seconds.
|
|
49
28
|
* **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
|
|
50
29
|
* **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
|
|
51
30
|
* **sourceAlertUuid (string | null):** The UUID of the alert that triggered the creation of this clip, if any.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
31
|
+
`;
|
|
32
|
+
const TOOL_HANDLER = async (args, extra) => {
|
|
33
|
+
const payload = ApiPayloadSchema.parse(args);
|
|
34
|
+
let ret;
|
|
35
|
+
switch (args.queryType) {
|
|
36
|
+
case "saved":
|
|
37
|
+
ret = await getSavedClips(payload, extra._meta?.requestModifiers, extra.sessionId);
|
|
38
|
+
case "expiringSoon":
|
|
39
|
+
ret = await getExpiringClips(payload, extra._meta?.requestModifiers, extra.sessionId);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export function createTool(server) {
|
|
46
|
+
server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
|
|
61
47
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
export function createTool(server) {
|
|
4
|
+
server.tool("count-tool", `
|
|
5
|
+
This tool counts the number of items by accepting an array of UUIDs. It can count anything that has UUIDs - users, devices,
|
|
6
|
+
records, or any other entities. Simply provide an array of UUID strings and it will return the precise count.
|
|
7
|
+
`, {
|
|
8
|
+
uuids: z
|
|
9
|
+
.array(z.string().describe("UUID string of an individual item"))
|
|
10
|
+
.describe("An array of UUID strings representing the items to count. Each string should be a valid UUID."),
|
|
11
|
+
}, async ({ uuids }) => {
|
|
12
|
+
try {
|
|
13
|
+
logger.info("Counting UUIDs", uuids);
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `Count: ${uuids.length}` }],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
const errorMessage = e instanceof Error ? e.message : `Unknown error: ${e}`;
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: `Error counting UUIDs: ${errorMessage}` }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createCameraPolicy } from "../api/create-camera-policy-tool-api.js";
|
|
3
|
+
import { ApiPayloadSchema, OUTPUT_SCHEMA } from "../types/create-camera-policy-tool-types.js";
|
|
4
|
+
import { postApi } from "../network.js";
|
|
5
|
+
const TOOL_NAME = "create-camera-policy-tool";
|
|
6
|
+
const TOOL_DESCRIPTION = `
|
|
7
|
+
A tool for creating a camera policy that walks users through a multi-step process.
|
|
8
|
+
|
|
9
|
+
The step begins with the user providing a policy name, description, and organization UUID.
|
|
10
|
+
Then, the user is presented with a form to configure the schedules for the policy.
|
|
11
|
+
Finally, the user is presented with a form to assign the policy to cameras.
|
|
12
|
+
|
|
13
|
+
Uses elicitation forms for rich user interaction.
|
|
14
|
+
`;
|
|
15
|
+
const TOOL_ARGS = {
|
|
16
|
+
// Step 1: Policy creation
|
|
17
|
+
name: z.string().describe("Policy name (for creating policy)"),
|
|
18
|
+
description: z.string().describe("Policy description (for creating policy)"),
|
|
19
|
+
orgUuid: z.string().describe("Organization UUID (for creating policy)"),
|
|
20
|
+
// Step 2: Schedule configuration
|
|
21
|
+
policyUuid: z.string().describe("Policy UUID (for configuring schedules)"),
|
|
22
|
+
scheduleConfigs: z.string().describe("JSON string of schedule configurations"),
|
|
23
|
+
// Step 3: Camera assignment
|
|
24
|
+
cameraUuids: z.string().describe("Comma-separated camera UUIDs to assign policy to"),
|
|
25
|
+
policyName: z.string().describe("Policy name (for reference)"),
|
|
26
|
+
};
|
|
27
|
+
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
28
|
+
const TOOL_HANDLER = async (args, extra) => {
|
|
29
|
+
const { name, description, orgUuid, policyUuid, scheduleConfigs, cameraUuids, policyName } = args;
|
|
30
|
+
// Step 3: Camera assignment (if cameraUuids provided)
|
|
31
|
+
if (cameraUuids?.trim()) {
|
|
32
|
+
try {
|
|
33
|
+
const cameraList = cameraUuids
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((uuid) => uuid.trim())
|
|
36
|
+
.filter((uuid) => uuid);
|
|
37
|
+
if (cameraList.length > 0) {
|
|
38
|
+
const cameraPayload = {
|
|
39
|
+
cameraBulkDetails: cameraList.map(cameraUuid => ({
|
|
40
|
+
uuid: cameraUuid,
|
|
41
|
+
policyUuid: policyUuid,
|
|
42
|
+
policyUuidUpdated: true,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
await postApi({
|
|
46
|
+
route: "/camera/updateDetailsBulkV2",
|
|
47
|
+
body: cameraPayload,
|
|
48
|
+
modifiers: extra._meta?.requestModifiers,
|
|
49
|
+
sessionId: extra.sessionId,
|
|
50
|
+
});
|
|
51
|
+
const jsonResultResponse = {
|
|
52
|
+
needUserInput: false,
|
|
53
|
+
message: `Excellent! Policy created and assigned to ${cameraList.length} camera(s)!`,
|
|
54
|
+
policyUuid,
|
|
55
|
+
policyName,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `🎉 Camera policy setup completely finished!\n\n✅ Policy "${policyName}" assigned to ${cameraList.length} camera(s)\n✅ Policy is now fully active\n\nYour cameras will now generate alerts according to the policy configuration.`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
structuredContent: jsonResultResponse,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `Failed to assign policy to cameras: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Schedule configuration (if scheduleConfigs provided and non-empty)
|
|
80
|
+
if (scheduleConfigs?.trim() && scheduleConfigs.trim() !== "[]") {
|
|
81
|
+
try {
|
|
82
|
+
const configs = JSON.parse(scheduleConfigs);
|
|
83
|
+
// Only proceed if we have actual schedule configurations
|
|
84
|
+
if (configs.length > 0) {
|
|
85
|
+
const scheduledTriggers = configs.map(config => ({
|
|
86
|
+
scheduleUuid: config.scheduleUuid,
|
|
87
|
+
triggerSet: config.activities.map(activity => ({ activity })),
|
|
88
|
+
}));
|
|
89
|
+
const payload = {
|
|
90
|
+
policy: { uuid: policyUuid, scheduledTriggers },
|
|
91
|
+
};
|
|
92
|
+
const result = await postApi({
|
|
93
|
+
route: "/policy/updateCameraPolicy",
|
|
94
|
+
body: payload,
|
|
95
|
+
modifiers: extra._meta?.requestModifiers,
|
|
96
|
+
sessionId: extra.sessionId,
|
|
97
|
+
});
|
|
98
|
+
if (result.error) {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Failed to configure policy schedules: ${result.errorMsg || "Unknown error"}`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Success - now show camera assignment form
|
|
109
|
+
const jsonResultResponse = {
|
|
110
|
+
needUserInput: true,
|
|
111
|
+
message: `Excellent! Policy schedules configured.\n\nFinal step: Please select which cameras should use this policy.`,
|
|
112
|
+
requestType: "camera-assignment",
|
|
113
|
+
submitAction: "create-camera-policy-tool",
|
|
114
|
+
policyUuid,
|
|
115
|
+
policyName,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: JSON.stringify(jsonResultResponse),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
structuredContent: jsonResultResponse,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// If configs.length === 0, fall through to policy creation
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Error configuring schedules: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Step 1: Policy creation (if name provided)
|
|
141
|
+
if (name?.trim()) {
|
|
142
|
+
try {
|
|
143
|
+
const payload = ApiPayloadSchema.parse({
|
|
144
|
+
policy: {
|
|
145
|
+
name,
|
|
146
|
+
description: description?.trim() ? description : undefined,
|
|
147
|
+
orgUuid: orgUuid?.trim() ? orgUuid : undefined,
|
|
148
|
+
scheduledTriggers: [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const result = await createCameraPolicy(payload, extra._meta?.requestModifiers, extra.sessionId);
|
|
152
|
+
if (result.error) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `Failed to create camera policy: ${result.errorMsg || "Unknown error"}`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
console.error(`[createCameraPolicyTool] -- Proceeding with schedule-trigger-configuration form. Got result ${JSON.stringify(result)}`);
|
|
163
|
+
// Success - now show schedule configuration form
|
|
164
|
+
const jsonResultResponse = {
|
|
165
|
+
needUserInput: true,
|
|
166
|
+
message: `Great! Your policy "${name}" was created with UUID: ${result.policyUuid}\n\nNext, let's configure when this policy should be active and what activities should trigger alerts.`,
|
|
167
|
+
requestType: "schedule-trigger-configuration",
|
|
168
|
+
submitAction: "create-camera-policy-tool",
|
|
169
|
+
policyUuid: result.policyUuid,
|
|
170
|
+
policyName: name,
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: JSON.stringify(jsonResultResponse),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
structuredContent: jsonResultResponse,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: `Error creating camera policy: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Step 0: Show initial form (no args provided)
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify({
|
|
199
|
+
needUserInput: true,
|
|
200
|
+
message: "Please provide the following information to create your camera policy:\n\n1. **Policy Name** (required): A descriptive name for the policy\n2. **Policy Description** (optional): What this policy does\n3. **Organization UUID** (optional): Leave blank to use your current organization\n\nOnce you provide this information, I'll create the policy for you.",
|
|
201
|
+
requestType: "policy-creation-form",
|
|
202
|
+
submitAction: "create-camera-policy-tool",
|
|
203
|
+
}),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
export function createTool(server) {
|
|
209
|
+
server.registerTool(TOOL_NAME, {
|
|
210
|
+
description: TOOL_DESCRIPTION,
|
|
211
|
+
inputSchema: TOOL_ARGS,
|
|
212
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
213
|
+
}, TOOL_HANDLER);
|
|
214
|
+
}
|