rhombus-node-mcp 0.1.10 → 0.1.12
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 +94 -19
- package/dist/constants.js +2 -0
- package/dist/index.js +23 -534
- package/dist/logger.js +27 -0
- package/dist/network.js +81 -0
- package/dist/resources/getResources.js +19 -0
- package/dist/resources/llms.pdf.js +19 -0
- package/dist/resources/routes.json.js +31 -0
- package/dist/tools/clips-tool.js +62 -0
- package/dist/tools/create-tool.js +80 -0
- package/dist/tools/devices/camera-tool/camera-tool.js +218 -0
- package/dist/tools/devices/camera-tool/types.js +172 -0
- package/dist/tools/devices/get-entity-tool.js +117 -0
- package/dist/tools/events-tool.js +101 -0
- package/dist/tools/faces-tool.js +140 -0
- package/dist/tools/get-org-information.js +18 -0
- package/dist/tools/getTools.js +22 -0
- package/dist/tools/location-tool.js +34 -0
- package/dist/tools/policy-alerts-tool.js +57 -0
- package/dist/tools/reboot-cameras.js +63 -0
- package/dist/tools/time-tool.js +57 -0
- package/dist/types/deviceType.js +14 -0
- package/dist/types.js +16 -13
- package/dist/util.js +111 -0
- package/dist/utils/confirmation.js +45 -0
- package/package.json +3 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getFilePathsInDirectory } from "../util.js";
|
|
4
|
+
async function getResources() {
|
|
5
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const filePaths = getFilePathsInDirectory(currentDir);
|
|
7
|
+
const resources = [];
|
|
8
|
+
for (const filePath of filePaths) {
|
|
9
|
+
const imported = (await import(filePath));
|
|
10
|
+
if (imported.createResource !== undefined) {
|
|
11
|
+
resources.push({
|
|
12
|
+
name: filePath,
|
|
13
|
+
create: imported.createResource,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return resources;
|
|
18
|
+
}
|
|
19
|
+
export default getResources;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "path";
|
|
4
|
+
// change from _createResource to createResource to add it back in
|
|
5
|
+
export function _createResource(server) {
|
|
6
|
+
server.resource("knowledge-base.pdf", "knowledge-base://llms.pdf", async (uri) => {
|
|
7
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const filePath = path.resolve(currentDir, "../../assets/llms.pdf");
|
|
9
|
+
return {
|
|
10
|
+
contents: [
|
|
11
|
+
{
|
|
12
|
+
uri: uri.href,
|
|
13
|
+
text: fs.readFileSync(filePath).toString(),
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "path";
|
|
4
|
+
// change from _createResource to createResource to add it back in
|
|
5
|
+
export function createResource(server) {
|
|
6
|
+
server.resource("routes.json", "file://routes.json/", {
|
|
7
|
+
mimeType: "application/json",
|
|
8
|
+
description: `
|
|
9
|
+
This resource provides a comprehensive catalog of application paths within the Rhombus Console, including both common and parameterized routes.
|
|
10
|
+
Each path is accompanied by a brief description derived from KBA support articles, offering context on its function within the product.
|
|
11
|
+
The resource also lists relevant external support links.
|
|
12
|
+
The paths are categorized into "common_paths", "parameterized_paths", and "external_paths" for easy navigation and understanding.
|
|
13
|
+
|
|
14
|
+
It is in the form of a JSON file.
|
|
15
|
+
|
|
16
|
+
Use these paths to help the user navigate to the correct that they may want to go to. However, try not directly reference the path itself, rather describe it, and make sure to show the user a button to help navigate with.
|
|
17
|
+
When providing a path, make sure to be very exact. You're only allowed to substitute in path segments that begin with : or are surrounded by brackets []. For example, /locations/:locationUuid, you need to get a location's UUID and replace :locationUuid with the actual UUID`,
|
|
18
|
+
}, async (uri) => {
|
|
19
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const filePath = path.resolve(currentDir, "../../assets/routes.json");
|
|
21
|
+
return {
|
|
22
|
+
contents: [
|
|
23
|
+
{
|
|
24
|
+
uri: uri.href,
|
|
25
|
+
text: fs.readFileSync(filePath).toString(),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { postApi } from "../network.js";
|
|
3
|
+
import { createToolArgs } from "../util.js";
|
|
4
|
+
const ClipsArgs = z.object({
|
|
5
|
+
deviceUuidFilters: z
|
|
6
|
+
.array(z.string())
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("A list of UUIDs representing specific devices to filter clips by. Only clips emitted by these devices will be returned."),
|
|
9
|
+
locationUuidFilters: z
|
|
10
|
+
.array(z.string())
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("A list of UUIDs representing specific locations to filter clips by. Only clips associated with these locations will be returned."),
|
|
13
|
+
searchFilter: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("A simple string to search for within the names of the clips."),
|
|
17
|
+
timestampMsAfter: z
|
|
18
|
+
.number()
|
|
19
|
+
.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."),
|
|
20
|
+
timestampMsBefore: z
|
|
21
|
+
.number()
|
|
22
|
+
.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."),
|
|
23
|
+
});
|
|
24
|
+
async function getSavedClips(args, requestModifiers) {
|
|
25
|
+
return await postApi("/event/getClipsWithProgress", args, requestModifiers);
|
|
26
|
+
}
|
|
27
|
+
export function createTool(server) {
|
|
28
|
+
server.tool("clips-tool", `
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
This tool allows you to filter clips by:
|
|
32
|
+
* Specific devices using their UUIDs.
|
|
33
|
+
* Specific locations using their UUIDs.
|
|
34
|
+
* A simple string search on clip names.
|
|
35
|
+
* A time range, specifying a start (timestampMsAfter) and/or end (timestampMsBefore) timestamp in milliseconds since epoch.
|
|
36
|
+
|
|
37
|
+
The tool returns a JSON object with the following structure and important fields:
|
|
38
|
+
* **errorMsg (string | null):** An error message if the request failed.
|
|
39
|
+
* **objecterror (boolean | null):** Indicates if an object-level error occurred.
|
|
40
|
+
* **pageToken (string | null):** A token to be supplied on the next search request to get the next page of results. If this token is null, there is no more data available.
|
|
41
|
+
* **savedClips (array of objects | null):** An array where each object represents a saved video clip. Each clip object contains the following important fields:
|
|
42
|
+
* **uuid (string):** The unique identifier for the video clip.
|
|
43
|
+
* **title (string):** The name given to the video clip.
|
|
44
|
+
* **description (string | null):** An optional description for the clip.
|
|
45
|
+
* **timestampMs (int64):** The start time of the video clip in milliseconds since epoch.
|
|
46
|
+
* **createdAtMs (int64):** The creation timestamp of the clip in milliseconds since epoch.
|
|
47
|
+
* **deviceUuid (string):** The UUID of the primary device (e.g., camera) that recorded the clip.
|
|
48
|
+
* **deviceUuids (array of strings or null):** A list of UUIDs for all devices associated with the clip.
|
|
49
|
+
* **durationSec (int32):** The length of the video clip in seconds.
|
|
50
|
+
* **status (string):** The current processing status of the clip, with possible values such as INITIATING, UPLOADING, RENDERING, FAILED, COMPLETE, OFFLINE, or UNKNOWN.
|
|
51
|
+
* **userUuid (string | null):** The UUID of the user associated with the clip, if applicable.
|
|
52
|
+
* **sourceAlertUuid (string | null):** The UUID of the alert that triggered the creation of this clip, if any.
|
|
53
|
+
`, createToolArgs({
|
|
54
|
+
...ClipsArgs.shape,
|
|
55
|
+
}), async ({ requestModifiers, ...args }) => {
|
|
56
|
+
let ret;
|
|
57
|
+
ret = await getSavedClips(args, requestModifiers);
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify(ret) }],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createToolArgs } from "../util.js";
|
|
3
|
+
import { CreateVideoWallOptions } from "../types.js";
|
|
4
|
+
import { postApi } from "../network.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { addConfirmationParams, requireConfirmation } from "../utils/confirmation.js";
|
|
7
|
+
async function createVideoWall(options, headers) {
|
|
8
|
+
const body = {
|
|
9
|
+
videoWall: {
|
|
10
|
+
displayName: options?.displayName,
|
|
11
|
+
deviceList: options?.deviceList,
|
|
12
|
+
othersCanEdit: true,
|
|
13
|
+
orgUuid: options?.orgUuid,
|
|
14
|
+
shared: true,
|
|
15
|
+
settings: {
|
|
16
|
+
gridSize: { width: options?.settings.columnCount, height: options?.settings.columnCount },
|
|
17
|
+
gridLayout: "1 2\n3 4",
|
|
18
|
+
intervalSeconds: options?.settings.intervalSeconds || 5,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const response = await postApi("/camera/createVideoWall", body, headers);
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
async function handleCreateVideoWallRequest(videoWallCreateOptions, headers) {
|
|
26
|
+
let text = "Unable to create video wall!";
|
|
27
|
+
logger.error("🔨 Creating video wall");
|
|
28
|
+
if (!videoWallCreateOptions?.displayName) {
|
|
29
|
+
text = JSON.stringify({
|
|
30
|
+
needUserInput: true,
|
|
31
|
+
commandForUser: "What should the name of the video wall be?",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else if ((videoWallCreateOptions?.deviceList || []).length === 0) {
|
|
35
|
+
text = JSON.stringify({
|
|
36
|
+
needUserInput: true,
|
|
37
|
+
commandForUser: "Which cameras would you like on this video wall?",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
logger.error("Creating video wall with options: ", JSON.stringify(videoWallCreateOptions));
|
|
42
|
+
text = JSON.stringify(await createVideoWall(videoWallCreateOptions, headers));
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function createTool(server) {
|
|
54
|
+
server.tool("create-tool", "Tool for creating many entity types such as video walls.", addConfirmationParams(createToolArgs({
|
|
55
|
+
entityType: z
|
|
56
|
+
.enum(["video-wall"])
|
|
57
|
+
.describe("The entity type to create. Example: video wall."),
|
|
58
|
+
videoWallCreateOptions: CreateVideoWallOptions,
|
|
59
|
+
})), async ({ entityType, videoWallCreateOptions, requestModifiers, confirmationId }) => {
|
|
60
|
+
const confirmation = requireConfirmation(confirmationId);
|
|
61
|
+
if (confirmation === true) {
|
|
62
|
+
switch (entityType) {
|
|
63
|
+
case "video-wall":
|
|
64
|
+
return await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers);
|
|
65
|
+
default:
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: "",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return confirmation;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getLogger } from "../../../logger.js";
|
|
3
|
+
import { appendQueryParams, AUTH_HEADERS, postApi, STATIC_HEADERS } from "../../../network.js";
|
|
4
|
+
import { createToolArgs, createToolTextContent, removeNullFields, } from "../../../util.js";
|
|
5
|
+
import { addConfirmationParams, isConfirmed, requireConfirmation, } from "../../../utils/confirmation.js";
|
|
6
|
+
import { ExternalUpdateableFacetedUserConfigSchema, } from "./types.js";
|
|
7
|
+
const logger = getLogger("camera-tool");
|
|
8
|
+
async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers) {
|
|
9
|
+
const body = {
|
|
10
|
+
cameraUuid: cameraUuid,
|
|
11
|
+
downscaleFactor: 10,
|
|
12
|
+
jpgQuality: 70,
|
|
13
|
+
permyriadCropHeight: 10000,
|
|
14
|
+
permyriadCropWidth: 5625,
|
|
15
|
+
permyriadCropX: 2188,
|
|
16
|
+
permyriadCropY: 0,
|
|
17
|
+
timestampMs: timestampMs,
|
|
18
|
+
};
|
|
19
|
+
logger.debug(`Getting frameUri from UUID: ${cameraUuid} at timestampMs: ${timestampMs}`);
|
|
20
|
+
const base64Image = await postApi("/video/getExactFrameUri", body, requestModifiers).then(async (res) => {
|
|
21
|
+
logger.debug(`Received frameUri ${res.frameUri}`);
|
|
22
|
+
// construct request headers
|
|
23
|
+
let requestHeaders = {
|
|
24
|
+
...(requestModifiers?.headers ?? AUTH_HEADERS),
|
|
25
|
+
...STATIC_HEADERS,
|
|
26
|
+
};
|
|
27
|
+
// add query params
|
|
28
|
+
if (requestModifiers?.query) {
|
|
29
|
+
res.frameUri = appendQueryParams(res.frameUri, requestModifiers.query);
|
|
30
|
+
}
|
|
31
|
+
logger.trace(`Fetching with headers\n${JSON.stringify(requestHeaders)}`);
|
|
32
|
+
return await fetch(res.frameUri, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers: requestHeaders,
|
|
35
|
+
}).then(async (res) => {
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
logger.error(`Failed to fetch image: ${await res.text()}`);
|
|
38
|
+
logger.error(res);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
42
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
43
|
+
const base64 = buffer.toString("base64");
|
|
44
|
+
return base64;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
if (!base64Image) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
status: "failed to fetch image",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
status: "successfully fetched image",
|
|
56
|
+
imageType: "base64",
|
|
57
|
+
imageData: base64Image,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export async function getCameraSettings(cameraUuid, requestModifiers) {
|
|
61
|
+
const res = await postApi("/camera/getFacetedConfig", {
|
|
62
|
+
deviceUuid: cameraUuid,
|
|
63
|
+
}, requestModifiers);
|
|
64
|
+
if (res.error) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
status: "failed to fetch camera settings",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
config: res.config,
|
|
73
|
+
status: "fetched camera settings",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function updateCameraSettings(cameraUuid, update, requestModifiers) {
|
|
77
|
+
// remove any "null" values
|
|
78
|
+
const res = await postApi("/camera/updateFacetedConfig", {
|
|
79
|
+
configUpdate: {
|
|
80
|
+
deviceUuid: cameraUuid,
|
|
81
|
+
...removeNullFields(update),
|
|
82
|
+
},
|
|
83
|
+
}, requestModifiers);
|
|
84
|
+
if (res.error) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
status: "failed to update camera settings",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
config: res.config,
|
|
93
|
+
status: "updated camera settings",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function createTool(server) {
|
|
97
|
+
server.tool("camera-tool", `
|
|
98
|
+
This tool can perform some action pertaining to the video stream of a camera. There are three types of requests
|
|
99
|
+
that can be passed into "requestType":
|
|
100
|
+
- image
|
|
101
|
+
- get-settings
|
|
102
|
+
- update-settings
|
|
103
|
+
|
|
104
|
+
What follows is a description of the behavior of this tool given the requestType
|
|
105
|
+
|
|
106
|
+
If the requestType is "image":
|
|
107
|
+
|
|
108
|
+
This tool captures and returns a real-time snapshot from a designated security camera.
|
|
109
|
+
The image reflects the current scene in the camera\'s field of view and serves as a contextual
|
|
110
|
+
input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
|
|
111
|
+
or situational assessment. When invoked, the tool provides the following: \n
|
|
112
|
+
• Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects. \n
|
|
113
|
+
• Enriched Data Potential: The image can be paired with AI models or downstream analytics to extract insights such as: \n• Number and type of objects in frame (e.g., humans, cars, packages) \n• Unusual behaviors (e.g., loitering, unauthorized access) \n• Environmental conditions (e.g., lighting, obstruction, cleanliness) \nUse Cases: \n• Verify what triggered a motion alert or analytic rule. \n• Provide visual context for access events or alarms. \n• Support live incident triage or retrospective investigations. \n• Feed contextual imagery to agents making security or operational decisions. \nInvocation Notes: \nTo use this tool correctly, the agent should provide the specific camera identifier or location name. If possible, include the intent (e.g., "verify unauthorized access", "identify vehicle", "check for obstructions") to enhance downstream processing or summarization.
|
|
114
|
+
|
|
115
|
+
If the requestType is "get-settings":
|
|
116
|
+
|
|
117
|
+
THIS TOOL UPDATES AND SETS DATA.
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
Use Cases: Retrieve the current resolution of Camera A.
|
|
121
|
+
|
|
122
|
+
If the requestType is "update-settings":
|
|
123
|
+
|
|
124
|
+
THIS TOOL UPDATES AND SETS DATA.
|
|
125
|
+
|
|
126
|
+
You can call call this tool with requestType "get-settings" and/or with "image" first to get a better idea of what needs to be updated.
|
|
127
|
+
This tool updates the configuration for a camera or associated device using the "configUpdate" parameter, which must be a JSON object containing the specific fields and their new values. For example, you can modify streaming parameters.
|
|
128
|
+
Thus, "configUpdate' is a necessary parameter if updating settings.
|
|
129
|
+
Please make sure you only update the necessary fields, since any unnecessary changes may cause the camera to behave improperly.
|
|
130
|
+
It may be a good idea to call "image" on this tool again after updating settings to make sure the new settings were effective in fulfilling
|
|
131
|
+
the user's request.
|
|
132
|
+
Use Cases: Adjust streaming parameters for Camera E.
|
|
133
|
+
`, addConfirmationParams(createToolArgs({
|
|
134
|
+
requestType: z.enum(["image", "get-settings", "update-settings"]),
|
|
135
|
+
timestampMs: z.optional(z.number()).describe(`
|
|
136
|
+
the timestamp in milliseconds. You can default to the current time if the user didn't specify a time, or you can call time-tool to parse the user's time description
|
|
137
|
+
`),
|
|
138
|
+
cameraUuid: z.optional(z.string()).describe("the camera uuid requested"),
|
|
139
|
+
configUpdate: ExternalUpdateableFacetedUserConfigSchema.optional().describe('the config update that would be applied to the camera if the requestType is "update-settings"'),
|
|
140
|
+
})), async ({ cameraUuid, timestampMs, requestType, configUpdate, requestModifiers, confirmationId, }) => {
|
|
141
|
+
if (!cameraUuid) {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
needUserInput: true,
|
|
148
|
+
commandForUser: "Which camera are you talking about?",
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
let response;
|
|
155
|
+
switch (requestType) {
|
|
156
|
+
case "image":
|
|
157
|
+
if (!timestampMs) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: JSON.stringify({
|
|
163
|
+
needUserInput: true,
|
|
164
|
+
commandForUser: "At what time?",
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers);
|
|
171
|
+
if (!response.success || !response.imageData) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "image",
|
|
180
|
+
data: response.imageData,
|
|
181
|
+
mimeType: "image/jpeg",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
case "get-settings":
|
|
186
|
+
response = await getCameraSettings(cameraUuid, requestModifiers);
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
189
|
+
};
|
|
190
|
+
case "update-settings":
|
|
191
|
+
const confirmation = requireConfirmation(confirmationId);
|
|
192
|
+
if (!isConfirmed(confirmation)) {
|
|
193
|
+
return confirmation;
|
|
194
|
+
}
|
|
195
|
+
if (!configUpdate) {
|
|
196
|
+
return createToolTextContent("Missing configUpdate");
|
|
197
|
+
}
|
|
198
|
+
response = await updateCameraSettings(cameraUuid, configUpdate, requestModifiers);
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
201
|
+
};
|
|
202
|
+
default:
|
|
203
|
+
response = {
|
|
204
|
+
error: true,
|
|
205
|
+
status: "missing unknown type from tool call",
|
|
206
|
+
};
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: JSON.stringify({ response }),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const VideoFacetSettings = z
|
|
3
|
+
.object({
|
|
4
|
+
// blocked_debounce_time_ms: z.number().int().nullable(),
|
|
5
|
+
// blocked_threshold: z.number().int().nullable(),
|
|
6
|
+
// dewarpMode: z.enum([
|
|
7
|
+
// 'NO_TRANSFORM',
|
|
8
|
+
// 'NORMAL',
|
|
9
|
+
// 'PANORAMA',
|
|
10
|
+
// 'SUB_REGION',
|
|
11
|
+
// 'VERTICAL_PANORAMA',
|
|
12
|
+
// 'TRANSVERSE_MERCATOR',
|
|
13
|
+
// 'MERCATOR',
|
|
14
|
+
// 'EQUIRECTANGULAR',
|
|
15
|
+
// ]).nullable(),
|
|
16
|
+
// disabled_schedule: z.array(
|
|
17
|
+
// z.object({
|
|
18
|
+
// minuteOfWeekStart: z.number().int().nullable(),
|
|
19
|
+
// minuteOfWeekStop: z.number().int().nullable(),
|
|
20
|
+
// }).nullable()
|
|
21
|
+
// ).nullable(),
|
|
22
|
+
// disabled_schedule_inverted: z.boolean().nullable(),
|
|
23
|
+
// disabled_schedule_uuid: z.string().nullable(),
|
|
24
|
+
// exposure_level: z.number().nullable(),
|
|
25
|
+
// fisheye_display_mode: z.enum(['RAW', 'IMMERSIVE', 'TILES', 'RAW_PANO']).nullable(),
|
|
26
|
+
// floorplan_homography: z.array(z.array(z.number().nullable()).nullable()).nullable(),
|
|
27
|
+
hdr_enabled: z.boolean().nullable(),
|
|
28
|
+
img_brightness: z
|
|
29
|
+
.number()
|
|
30
|
+
.nullable()
|
|
31
|
+
.describe("Ranges from [-255, 255]. The image is described as a range from [0, 255], and the brightness slider will ADD this value to whatever the original value is. Use this accordingly."),
|
|
32
|
+
img_contrast: z
|
|
33
|
+
.number()
|
|
34
|
+
.nullable()
|
|
35
|
+
.describe("Ranges from [0, 128]. In general, 64 works for most situations, but adjust accordingly."),
|
|
36
|
+
img_saturation: z
|
|
37
|
+
.number()
|
|
38
|
+
.nullable()
|
|
39
|
+
.describe("Ranges from [0, 255]. In general, 64 works for most situations, but adjust accordingly."),
|
|
40
|
+
img_sharpness: z
|
|
41
|
+
.number()
|
|
42
|
+
.nullable()
|
|
43
|
+
.describe("Ranges from [0, 11]. In general, 6 works for most situations, but adjust accordingly."),
|
|
44
|
+
// metering_config: z.object({
|
|
45
|
+
// rotation: z.number().nullable(),
|
|
46
|
+
// table: z.string().nullable(),
|
|
47
|
+
// }).nullable(),
|
|
48
|
+
// motor_config: z.object({
|
|
49
|
+
// af_enabled: z.boolean().nullable(),
|
|
50
|
+
// af_region: z.object({
|
|
51
|
+
// x: z.number().int().min(0).max(10000).nullable(),
|
|
52
|
+
// y: z.number().int().min(0).max(10000).nullable(),
|
|
53
|
+
// width: z.number().int().min(0).max(10000).nullable(),
|
|
54
|
+
// height: z.number().int().min(0).max(10000).nullable(),
|
|
55
|
+
// }).nullable(),
|
|
56
|
+
// focus: z.number().nullable(),
|
|
57
|
+
// piris: z.number().nullable(),
|
|
58
|
+
// zoom: z.number().nullable(),
|
|
59
|
+
// }).nullable(),
|
|
60
|
+
// mounting_direction: z.enum(['DOWN', 'UP', 'SIDEWAYS', 'UNKNOWN']).nullable(),
|
|
61
|
+
night_exposure_level: z.number().nullable(),
|
|
62
|
+
night_img_brightness: z.number().nullable(),
|
|
63
|
+
night_img_contrast: z.number().nullable(),
|
|
64
|
+
night_img_saturation: z.number().nullable(),
|
|
65
|
+
night_img_sharpness: z.number().nullable(),
|
|
66
|
+
// night_metering_config: CameraMeteringConfigTypeSchema.nullable(),
|
|
67
|
+
night_sensor_gain_max: z.number().nullable(),
|
|
68
|
+
night_shutter_time_max: z.number().nullable(),
|
|
69
|
+
night_shutter_time_min: z.number().nullable(),
|
|
70
|
+
// object_search: z.boolean().nullable(),
|
|
71
|
+
// privacy_windows: z.array(
|
|
72
|
+
// z.object({
|
|
73
|
+
// x: z.number().int().min(0).max(10000).nullable(),
|
|
74
|
+
// y: z.number().int().min(0).max(10000).nullable(),
|
|
75
|
+
// width: z.number().int().min(0).max(10000).nullable(),
|
|
76
|
+
// height: z.number().int().min(0).max(10000).nullable(),
|
|
77
|
+
// }).nullable()
|
|
78
|
+
// ).nullable(),
|
|
79
|
+
// ptz_config: z.object({
|
|
80
|
+
// offset_x_percent: z.number().nullable(),
|
|
81
|
+
// offset_y_percent: z.number().nullable(),
|
|
82
|
+
// rotation: z.number().nullable(),
|
|
83
|
+
// size_percent: z.number().nullable(),
|
|
84
|
+
// }).nullable(),
|
|
85
|
+
// region_for_occupancy: z.object({
|
|
86
|
+
// inverted: z.boolean().nullable(),
|
|
87
|
+
// polygons: z.array(
|
|
88
|
+
// z.object({
|
|
89
|
+
// coordinates: z.array(
|
|
90
|
+
// z.object({
|
|
91
|
+
// x: z.number().nullable(),
|
|
92
|
+
// y: z.number().nullable(),
|
|
93
|
+
// }).nullable()
|
|
94
|
+
// ).nullable(),
|
|
95
|
+
// }).nullable()
|
|
96
|
+
// ).nullable(),
|
|
97
|
+
// }).nullable(),
|
|
98
|
+
// region_of_interest: z.object({
|
|
99
|
+
// inverted: z.boolean().nullable(),
|
|
100
|
+
// polygons: z.array(
|
|
101
|
+
// z.object({
|
|
102
|
+
// coordinates: z.array(
|
|
103
|
+
// z.object({
|
|
104
|
+
// x: z.number().nullable(),
|
|
105
|
+
// y: z.number().nullable(),
|
|
106
|
+
// }).nullable()
|
|
107
|
+
// ).nullable(),
|
|
108
|
+
// }).nullable()
|
|
109
|
+
// ).nullable(),
|
|
110
|
+
// }).nullable(),
|
|
111
|
+
// region_of_interest_groups: z.array(
|
|
112
|
+
// z.object({
|
|
113
|
+
// inclusive: z.boolean().nullable(),
|
|
114
|
+
// regionsOfInterest: z.array(
|
|
115
|
+
// z.object({
|
|
116
|
+
// activities: z.array(z.string()).nullable(), // Assuming ActivityEnumSchema is just string
|
|
117
|
+
// name: z.string().nullable(),
|
|
118
|
+
// polygon: z.object({
|
|
119
|
+
// coordinates: z.array(
|
|
120
|
+
// z.object({
|
|
121
|
+
// x: z.number().nullable(),
|
|
122
|
+
// y: z.number().nullable(),
|
|
123
|
+
// }).nullable()
|
|
124
|
+
// ).nullable(),
|
|
125
|
+
// }).nullable(),
|
|
126
|
+
// uuid: z.string().nullable(),
|
|
127
|
+
// }).nullable()
|
|
128
|
+
// ).nullable(),
|
|
129
|
+
// type: z.enum(['ACTIVITY', 'REPORTING', 'CROSSING']).nullable(),
|
|
130
|
+
// }).nullable()
|
|
131
|
+
// ).nullable(),
|
|
132
|
+
resolution: z
|
|
133
|
+
.object({
|
|
134
|
+
width: z.number().int().positive().nullable(),
|
|
135
|
+
height: z.number().int().positive().nullable(),
|
|
136
|
+
})
|
|
137
|
+
.nullable(),
|
|
138
|
+
rotation: z.number().nullable(),
|
|
139
|
+
segment_max_bytes: z.number().nullable(),
|
|
140
|
+
sensor_gain_max: z.number().nullable(),
|
|
141
|
+
shutter_time_max: z.number().nullable(),
|
|
142
|
+
shutter_time_min: z.number().nullable(),
|
|
143
|
+
snapshot_height: z.number().nullable(),
|
|
144
|
+
snapshot_interval_secs: z.number().nullable(),
|
|
145
|
+
// tile_views: z.array(
|
|
146
|
+
// z.object({
|
|
147
|
+
// aspectRatio: z.object({
|
|
148
|
+
// width: z.number().nullable(),
|
|
149
|
+
// height: z.number().nullable(),
|
|
150
|
+
// }).nullable(),
|
|
151
|
+
// pitchDegrees: z.number().nullable(),
|
|
152
|
+
// rollDegrees: z.number().nullable(),
|
|
153
|
+
// verticalFieldOfViewDegrees: z.number().nullable(),
|
|
154
|
+
// yawDegrees: z.number().nullable(),
|
|
155
|
+
// }).nullable()
|
|
156
|
+
// ).nullable(),
|
|
157
|
+
// updatedSetMethodMap: z.record(z.boolean().nullable()).nullable(),
|
|
158
|
+
video_persist_disabled: z.boolean().nullable(),
|
|
159
|
+
// wdr_enabled: z.boolean().nullable(), TODO: find a way to tell the LLM which devices support this
|
|
160
|
+
wdr_strength: z.number().nullable().describe("Ranges from [0, 128]."),
|
|
161
|
+
zero_motion_video_bitrate_percent: z.number().nullable(),
|
|
162
|
+
})
|
|
163
|
+
.nullable()
|
|
164
|
+
.describe("This describes the kind of settings that you can manipulate for a camera. If the user asks to change settings, only change what is necessary and don't pass in any fields that you do not need.");
|
|
165
|
+
export const ExternalUpdateableFacetedUserConfigSchema = z.object({
|
|
166
|
+
/** Update Video Settings */
|
|
167
|
+
videoFacetSettings: z
|
|
168
|
+
.object({
|
|
169
|
+
v0: VideoFacetSettings,
|
|
170
|
+
})
|
|
171
|
+
.describe("For each of the object keys: v0, v1, v2, v3, they correspond to a specific facet of the camera, since a camera may have multiple facets, up to 4. Hence, why they are zero-indexed."),
|
|
172
|
+
});
|