rhombus-node-mcp 0.1.21 → 0.1.23
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 +8 -0
- package/dist/api/access-control-tool-api.js +256 -0
- package/dist/api/alarm-monitoring-tool-api.js +65 -0
- package/dist/api/camera-tool-api.js +35 -15
- package/dist/api/camera-uptime-tool-api.js +112 -0
- package/dist/api/clips-tool-api.js +110 -21
- package/dist/api/create-tool-api.js +143 -16
- package/dist/api/door-tool-api.js +66 -0
- package/dist/api/events-tool-api.js +151 -28
- package/dist/api/faces-tool-api.js +118 -91
- package/dist/api/get-entity-tool-api.js +2 -0
- package/dist/api/guest-management-tool-api.js +75 -0
- package/dist/api/location-tool-api.js +48 -1
- package/dist/api/lpr-tool-api.js +26 -0
- package/dist/api/policy-alerts-tool-api.js +54 -0
- package/dist/api/report-tool-api.js +345 -23
- package/dist/api/rules-tool-api.js +78 -0
- package/dist/api/search-tool-api.js +93 -0
- package/dist/api/time-tool-api.js +3 -1
- package/dist/api/update-tool-api.js +204 -0
- package/dist/api/user-access-trail-tool-api.js +45 -0
- package/dist/api/user-audit-tool-api.js +47 -0
- package/dist/api/user-tool-api.js +77 -0
- package/dist/createServer.js +3 -1
- package/dist/filtering-utils.js +298 -0
- package/dist/index.js +0 -9
- package/dist/logger.js +6 -5
- package/dist/network.js +18 -4
- package/dist/tools/access-control-tool.js +97 -0
- package/dist/tools/alarm-monitoring-tool.js +50 -0
- package/dist/tools/analytics-tool.js +383 -0
- package/dist/tools/camera-tool.js +27 -9
- package/dist/tools/camera-uptime-tool.js +50 -0
- package/dist/tools/clips-tool.js +46 -16
- package/dist/tools/door-tool.js +66 -0
- package/dist/tools/events-tool.js +103 -81
- package/dist/tools/faces-tool.js +175 -32
- package/dist/tools/get-entity-tool.js +6 -1
- package/dist/tools/guest-management-tool.js +50 -0
- package/dist/tools/location-tool.js +23 -4
- package/dist/tools/lpr-tool.js +26 -20
- package/dist/tools/policy-alerts-tool.js +34 -4
- package/dist/tools/report-tool.js +183 -46
- package/dist/tools/rules-tool.js +76 -0
- package/dist/tools/search-tool.js +69 -0
- package/dist/tools/update-tool.js +63 -2
- package/dist/tools/user-access-trail-tool.js +64 -0
- package/dist/tools/user-audit-tool.js +53 -0
- package/dist/tools/user-tool.js +55 -0
- package/dist/tools/video-walls-tool.js +41 -0
- package/dist/transports/streamable-http.js +25 -15
- package/dist/types/access-control-tool-types.js +120 -0
- package/dist/types/alarm-monitoring-tool-types.js +60 -0
- package/dist/types/analytics-tool-types.js +190 -0
- package/dist/types/camera-tool-types.js +3 -2
- package/dist/types/camera-uptime-tool-types.js +52 -0
- package/dist/types/clips-tool-types.js +60 -11
- package/dist/types/door-tool-types.js +64 -0
- package/dist/types/events-tools-types.js +71 -3
- package/dist/types/faces-tools-types.js +36 -0
- package/dist/types/guest-management-tool-types.js +57 -0
- package/dist/types/location-tool-types.js +10 -1
- package/dist/types/lpr-tool-types.js +14 -0
- package/dist/types/policy-alerts-tool-types.js +38 -3
- package/dist/types/report-tool-types.js +284 -42
- package/dist/types/rules-tool-types.js +58 -0
- package/dist/types/schema.js +3123 -1
- package/dist/types/search-tool-types.js +82 -0
- package/dist/types/update-tool-types.js +4 -1
- package/dist/types/user-access-trail-tool-types.js +54 -0
- package/dist/types/user-audit-tool-types.js +48 -0
- package/dist/types/user-tool-types.js +61 -0
- package/dist/types/video-walls-tool-types.js +59 -0
- package/dist/types/zod-schemas.js +3237 -986
- package/dist/types.js +0 -21
- package/dist/util.js +34 -57
- package/package.json +7 -5
- package/dist/disabled-tools/semantic-search-tool.js +0 -90
- package/dist/tools/create-tool.js +0 -30
- package/dist/types/create-tool-types.js +0 -8
- package/dist/types/schema-components.js +0 -7211
- package/dist/types/semantic-search-tool-types.js +0 -5
package/dist/tools/lpr-tool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getSavedVehicles, getVehicleEvents, getVehicleLabels } from "../api/lpr-tool-api.js";
|
|
1
|
+
import { getSavedVehicles, getVehicleEvents, getVehicleLabels, searchLicensePlates, saveVehicle } from "../api/lpr-tool-api.js";
|
|
2
2
|
import { LprToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/lpr-tool-types.js";
|
|
3
3
|
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
4
|
const TOOL_NAME = "lpr-tool";
|
|
@@ -14,7 +14,7 @@ is recognized by a rhombus security camera, it will attach the label to the even
|
|
|
14
14
|
|
|
15
15
|
You should use the location-tool if trying to pair vehicle events to a particular location. Never use location UUIDs in reports, use names.
|
|
16
16
|
|
|
17
|
-
As such, if the user is asking anything about a label or labels it would be best practice to first call ${LprToolRequestType.GET_VEHICLE_LABELS} and then ${LprToolRequestType.GET_VEHICLE_EVENTS}
|
|
17
|
+
As such, if the user is asking anything about a label or labels it would be best practice to first call ${LprToolRequestType.GET_VEHICLE_LABELS} and then ${LprToolRequestType.GET_VEHICLE_EVENTS}
|
|
18
18
|
or ${LprToolRequestType.GET_VEHICLE_EVENTS}.
|
|
19
19
|
|
|
20
20
|
This tool has 3 modes of operation, determined by the "requestType" parameter:
|
|
@@ -38,37 +38,43 @@ const TOOL_HANDLER = async (args, _extra) => {
|
|
|
38
38
|
}));
|
|
39
39
|
}
|
|
40
40
|
const vehicleEvents = await getVehicleEvents(vehicleEventsArgs, args.timeZone, requestModifiers, sessionId);
|
|
41
|
-
return createToolStructuredContent({
|
|
42
|
-
vehicleEvents,
|
|
43
|
-
});
|
|
41
|
+
return createToolStructuredContent({ vehicleEvents });
|
|
44
42
|
}
|
|
45
43
|
case LprToolRequestType.GET_SAVED_VEHICLES: {
|
|
46
44
|
const savedVehicles = await getSavedVehicles(args.timeZone, requestModifiers, sessionId);
|
|
47
|
-
return createToolStructuredContent({
|
|
48
|
-
savedVehicles,
|
|
49
|
-
});
|
|
45
|
+
return createToolStructuredContent({ savedVehicles });
|
|
50
46
|
}
|
|
51
47
|
case LprToolRequestType.GET_VEHICLE_LABELS: {
|
|
52
48
|
const vehicleLabels = await getVehicleLabels(requestModifiers, sessionId);
|
|
53
|
-
return createToolStructuredContent({
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
return createToolStructuredContent({ vehicleLabels });
|
|
50
|
+
}
|
|
51
|
+
case LprToolRequestType.SEARCH_LICENSE_PLATES: {
|
|
52
|
+
if (!args.licensePlateQuery) {
|
|
53
|
+
return createToolStructuredContent({
|
|
54
|
+
error: "licensePlateQuery is required for search-license-plates.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const results = await searchLicensePlates(args.licensePlateQuery, args.timeZone, requestModifiers, sessionId);
|
|
58
|
+
return createToolStructuredContent({ licensePlateSearchResults: results });
|
|
59
|
+
}
|
|
60
|
+
case LprToolRequestType.SAVE_VEHICLE: {
|
|
61
|
+
if (!args.vehicleName || !args.vehicleLicensePlate) {
|
|
62
|
+
return createToolStructuredContent({
|
|
63
|
+
error: "vehicleName and vehicleLicensePlate are required for save-vehicle.",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const result = await saveVehicle(args.vehicleName, args.vehicleLicensePlate, args.vehicleDescription ?? undefined, requestModifiers, sessionId);
|
|
67
|
+
return createToolStructuredContent({ saveVehicleResult: result });
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
catch (error) {
|
|
60
72
|
if (error instanceof Error) {
|
|
61
|
-
return createToolStructuredContent({
|
|
62
|
-
error: error.message,
|
|
63
|
-
});
|
|
73
|
+
return createToolStructuredContent({ error: error.message });
|
|
64
74
|
}
|
|
65
|
-
return createToolStructuredContent({
|
|
66
|
-
error: "Unknown error",
|
|
67
|
-
});
|
|
75
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
68
76
|
}
|
|
69
|
-
return createToolStructuredContent({
|
|
70
|
-
error: "Invalid request type",
|
|
71
|
-
});
|
|
77
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
72
78
|
};
|
|
73
79
|
export function createTool(server) {
|
|
74
80
|
server.registerTool(TOOL_NAME, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getExpiringPolicyAlerts, getPolicyAlerts } from "../api/policy-alerts-tool-api.js";
|
|
1
|
+
import { dismissPolicyAlert, getExpiringPolicyAlerts, getPolicyAlertDetails, getPolicyAlertGroups, getPolicyAlerts, getUnhealthyDeviceAlerts, } from "../api/policy-alerts-tool-api.js";
|
|
2
2
|
import { ApiPayloadSchema, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/policy-alerts-tool-types.js";
|
|
3
|
+
import { extractFromToolExtra } from "../util.js";
|
|
3
4
|
const TOOL_NAME = "policy-alerts-tool";
|
|
4
5
|
const TOOL_DESCRIPTION = `
|
|
5
6
|
Retrieves Rhombus policy alerts. Policy alerts in the Rhombus system are generated based on user-defined alert
|
|
@@ -20,16 +21,45 @@ policy alerts within the Rhombus system.
|
|
|
20
21
|
This tool allows you to filter existing alerts by existing/expiring, a specific time range (before or after a timestamp in ISO 8601 format),
|
|
21
22
|
by a list of device UUIDs, or by a list of location UUIDs.
|
|
22
23
|
You can also specify the maximum number of results to return.
|
|
23
|
-
The output is provided in JSON format
|
|
24
|
+
The output is provided in JSON format.
|
|
25
|
+
|
|
26
|
+
**Pagination**: Results are paginated and have a maximum page size. If the response includes a \`lastEvaluatedKey\` (for queryType "expiringSoon") or both \`lastTimestampISO\` and \`lastUuid\` (for queryTypes "existing" and "alert-groups"), more results are available. Pass these values back in the next call using the corresponding input parameters (lastEvaluatedKey, or lastTimestampISO and lastUuid) to retrieve the next page. Repeat until the response no longer includes these fields.
|
|
27
|
+
|
|
28
|
+
IMPORTANT: The "unhealthy-devices" queryType returns historical alert notifications that were triggered for device health issues.
|
|
29
|
+
It does NOT return live/real-time device connection status. If no device health alert policies are configured, or alerts were
|
|
30
|
+
dismissed, this may return empty even when devices are offline.
|
|
31
|
+
|
|
32
|
+
**To check which devices are currently online/offline, use the get-entity-tool instead.** The get-entity-tool returns the
|
|
33
|
+
current state of all devices including their live connection status (the "connected" field). Request all entity types
|
|
34
|
+
(CAMERA, DOORBELL_CAMERA, BADGE_READER, etc.) and check the "connected" field on each device to determine which are offline.`;
|
|
24
35
|
const TOOL_HANDLER = async (args, extra) => {
|
|
25
36
|
const payload = ApiPayloadSchema.parse(args);
|
|
37
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(extra);
|
|
26
38
|
let ret;
|
|
27
39
|
switch (args.queryType) {
|
|
28
40
|
case "existing":
|
|
29
|
-
ret = await getPolicyAlerts(payload,
|
|
41
|
+
ret = await getPolicyAlerts(payload, requestModifiers, sessionId);
|
|
30
42
|
break;
|
|
31
43
|
case "expiringSoon":
|
|
32
|
-
await getExpiringPolicyAlerts(payload,
|
|
44
|
+
ret = await getExpiringPolicyAlerts(payload, requestModifiers, sessionId);
|
|
45
|
+
break;
|
|
46
|
+
case "details":
|
|
47
|
+
if (!args.alertUuid) {
|
|
48
|
+
throw new Error("alertUuid is required for 'details' queryType");
|
|
49
|
+
}
|
|
50
|
+
ret = await getPolicyAlertDetails(args.alertUuid, requestModifiers, sessionId);
|
|
51
|
+
break;
|
|
52
|
+
case "dismiss":
|
|
53
|
+
if (!args.alertUuid) {
|
|
54
|
+
throw new Error("alertUuid is required for 'dismiss' queryType");
|
|
55
|
+
}
|
|
56
|
+
ret = await dismissPolicyAlert(args.alertUuid, requestModifiers, sessionId);
|
|
57
|
+
break;
|
|
58
|
+
case "unhealthy-devices":
|
|
59
|
+
ret = await getUnhealthyDeviceAlerts(requestModifiers, sessionId);
|
|
60
|
+
break;
|
|
61
|
+
case "alert-groups":
|
|
62
|
+
ret = await getPolicyAlertGroups(payload, requestModifiers, sessionId);
|
|
33
63
|
break;
|
|
34
64
|
default:
|
|
35
65
|
throw new Error(`Unsupported queryType: ${args.queryType}`);
|
|
@@ -1,59 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { findPromptConfigurations, getAuditFeed, getCustomEventsReport, getCustomLLMReport, getDiagnosticFeed, getLineCrossingEnabledCameras, getOccupancyCountReport, getOccupancyEnabledCameras, getPeopleCountEvents, getSummaryCountReport, getThresholdCrossingCountReport, getThresholdCrossingEvents, getUniqueFaceCount, } from "../api/report-tool-api.js";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { OUTPUT_SCHEMA, RequestType, TOOL_ARGS, } from "../types/report-tool-types.js";
|
|
4
|
+
import { extractFromToolExtra } from "../util.js";
|
|
3
5
|
const TOOL_NAME = "report-tool";
|
|
4
6
|
const TOOL_DESCRIPTION = `
|
|
5
|
-
This tool
|
|
6
|
-
It provides aggregated counts over specified time intervals and scopes.
|
|
7
|
-
This tool should be used when users need high level summary reports, analytics, or aggregated counts of system events and activities.
|
|
8
|
-
If types contains PEOPLE please understand that this is not a unique person count, it is a count of people detection events.
|
|
9
|
-
It's useful for getting a high level count of people, but not for getting a unique person count.
|
|
7
|
+
**Scope:** This tool returns **aggregated counts and time-series summaries** over specified intervals and scopes. Use **events-tool** when you need raw, event-level data (individual events with timestamps). Use this tool for high-level reports, analytics, and trends—especially over periods of a day or more.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
before running GET_SUMMARY_COUNT_REPORT since the uuid pulled from summaryCountRequest should always be present on
|
|
13
|
-
the list returned from getOccupancyEnabledCameras.
|
|
9
|
+
---
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
- GET_LINE_CROSSING_ENABLED_CAMERAS: Retrieves cameras at a location that have line crossing enabled, along with their configurations.
|
|
17
|
-
This should be called first to identify which cameras can be used for threshold crossing reports.
|
|
18
|
-
- GET_THRESHOLD_CROSSING_COUNT_REPORT: Generates reports showing ingress and egress counts for line crossings over time.
|
|
19
|
-
Automatically calculates key metrics including:
|
|
20
|
-
• Average entries per hour
|
|
21
|
-
• Average exits per hour
|
|
22
|
-
• Hour with most entries (with timestamp and count)
|
|
23
|
-
• Hour with most exits (with timestamp and count)
|
|
24
|
-
• Busiest hour overall (total activity with breakdown)
|
|
25
|
-
Supports human and vehicle crossing detection with configurable time buckets (quarter hour, hour, day, week).
|
|
11
|
+
**People / occupancy counting strategy**
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
13
|
+
When asked to count people on a camera or at a location, follow this strategy:
|
|
14
|
+
1. **Always call GET_OCCUPANCY_ENABLED_CAMERAS first** to discover which cameras have occupancy counting enabled.
|
|
15
|
+
2. If the target camera IS in the list, call **GET_OCCUPANCY_COUNT_REPORT** for that device. The response will automatically include a \`faceCountEnrichment\` field with the number of unique individuals identified by face recognition in the same time range. Present both data sources: occupancy estimate and unique face count.
|
|
16
|
+
3. If the target camera is NOT in the list, **tell the user** that camera does not have occupancy counting enabled, and list the cameras that do. You can still call GET_SUMMARY_COUNT_REPORT with PEOPLE type — its response will also include \`faceCountEnrichment\` with unique face data as a fallback. If the PEOPLE count returns zero, the response will also include the list of occupancy-enabled cameras and a hint.
|
|
17
|
+
4. When both occupancy data and face recognition data are available, **synthesize both** in your answer (e.g., "Occupancy estimates ~15 people. Face recognition identified 9 unique individuals during this period.").
|
|
18
|
+
|
|
19
|
+
**PEOPLE type (in GET_SUMMARY_COUNT_REPORT):** Not a unique person count; it counts people-detection events. Requires people detection to be enabled on the camera. Use for high-level activity trends, not for deduplicated head counts.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
**Summary and occupancy**
|
|
24
|
+
- **GET_SUMMARY_COUNT_REPORT:** Aggregated counts (people, faces, motion, vehicles, etc.) over time at device, location, or org scope. Interval: minutely, hourly, daily, weekly, monthly, yearly. When called with PEOPLE type at DEVICE scope, the response is automatically enriched with face recognition data.
|
|
25
|
+
- **GET_OCCUPANCY_ENABLED_CAMERAS:** List of cameras with occupancy reporting enabled. **Always call this first** before any people/occupancy counting request to verify camera support.
|
|
26
|
+
- **GET_OCCUPANCY_COUNT_REPORT:** Occupancy count time series for a specific device over a time range. Response is automatically enriched with face recognition data. If the device does not support occupancy, the response will include a hint and the list of cameras that do.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
**Line crossing**
|
|
31
|
+
- **GET_LINE_CROSSING_ENABLED_CAMERAS:** Cameras at a location with line crossing enabled, plus their configs. Call first to see which cameras support threshold crossing reports.
|
|
32
|
+
- **GET_THRESHOLD_CROSSING_COUNT_REPORT:** Ingress/egress counts for line crossings over time. Supports human and vehicle detection; bucket size: quarter hour, hour, day, week. Response includes computed metrics: average entries/exits per hour, hour with most entries/exits, busiest hour (with breakdown).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
**Custom LLM events**
|
|
37
|
+
- **FIND_PROMPT_CONFIGURATIONS:** All custom event prompt configurations (e.g. "black dog sightings", "delivery truck arrivals", "parking availability %"). Each has prompt text, UUID, and promptType (COUNT, PERCENT, BOOLEAN). Call first to discover available custom events.
|
|
38
|
+
- **GET_CUSTOM_LLM_REPORT:** **This is the PRIMARY way to get custom event reports.** Aggregated time-series for one custom event by prompt UUID. Automatically selects the correct API based on promptType: COUNT (numeric counts), PERCENT (percentages), BOOLEAN (true/false). Intervals: minutely, quarter-hourly, hourly, daily, weekly, monthly. **Always use this for custom event reports, trends, and analytics.** Use FIND_PROMPT_CONFIGURATIONS first to get the promptUuid and promptType.
|
|
39
|
+
- **GET_CUSTOM_EVENTS_REPORT:** Raw individual event values only (not aggregated). Use only when you need per-event granularity, not for reports or trends.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
**Audit and diagnostics**
|
|
44
|
+
- **GET_AUDIT_FEED:** Audit log of all user/admin actions in the org over a time range. Returns who did what and when (principalName, targetName, action, displayText).
|
|
45
|
+
- **GET_DIAGNOSTIC_FEED:** Device diagnostic events over a time range.
|
|
46
|
+
- **GET_THRESHOLD_CROSSING_EVENTS:** Individual line-crossing events (not aggregated counts).
|
|
47
|
+
- **GET_PEOPLE_COUNT_EVENTS:** Most recent people count readings for specified devices.
|
|
38
48
|
`;
|
|
39
49
|
const TOOL_HANDLER = async (args, extra) => {
|
|
40
50
|
const { requestType } = args;
|
|
51
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(extra);
|
|
41
52
|
if (requestType === RequestType.GET_SUMMARY_COUNT_REPORT) {
|
|
42
53
|
const { summaryCountRequest } = args;
|
|
43
54
|
if (!summaryCountRequest) {
|
|
44
55
|
throw new Error("summaryCountRequest is required");
|
|
45
56
|
}
|
|
46
|
-
const { interval, scope, types, rangeStart, rangeEnd, uuid } = summaryCountRequest;
|
|
47
|
-
const
|
|
57
|
+
const { interval, scope, types, rangeStart, rangeEnd, uuid, timeZone } = summaryCountRequest;
|
|
58
|
+
const startTimeMs = new Date(rangeStart).getTime();
|
|
59
|
+
const endTimeMs = new Date(rangeEnd).getTime();
|
|
60
|
+
const report = await getSummaryCountReport(interval, scope, types, uuid ?? undefined, endTimeMs, startTimeMs, requestModifiers, sessionId, timeZone);
|
|
61
|
+
const enrichedReport = { ...report };
|
|
62
|
+
const isPeopleQuery = types.includes("PEOPLE") && scope === "DEVICE" && uuid;
|
|
63
|
+
if (isPeopleQuery) {
|
|
64
|
+
try {
|
|
65
|
+
const faceCount = await getUniqueFaceCount(uuid, startTimeMs, endTimeMs, requestModifiers, sessionId);
|
|
66
|
+
enrichedReport.faceCountEnrichment = faceCount;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
logger.error("Failed to fetch face count enrichment for summary report", err);
|
|
70
|
+
}
|
|
71
|
+
const allZero = !report?.timeSeriesDataPoints?.length ||
|
|
72
|
+
report.timeSeriesDataPoints.every((dp) => !dp.eventCountMap ||
|
|
73
|
+
Object.values(dp.eventCountMap).every((v) => v === 0 || v === null || v === undefined));
|
|
74
|
+
if (allZero) {
|
|
75
|
+
try {
|
|
76
|
+
const camerasReport = await getOccupancyEnabledCameras(requestModifiers, sessionId);
|
|
77
|
+
enrichedReport.occupancyEnabledCameras = camerasReport?.cameras?.map((c) => ({
|
|
78
|
+
uuid: c.uuid,
|
|
79
|
+
name: c.name,
|
|
80
|
+
locationUuid: c.locationUuid,
|
|
81
|
+
}));
|
|
82
|
+
enrichedReport.hint =
|
|
83
|
+
"People detection returned zero results for this camera. This usually means people counting is not enabled on this device. " +
|
|
84
|
+
"The occupancyEnabledCameras field lists cameras that support occupancy counting. " +
|
|
85
|
+
"Face recognition data is available in faceCountEnrichment above.";
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
logger.error("Failed to fetch occupancy-enabled cameras for hint", err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
48
92
|
return {
|
|
49
93
|
content: [
|
|
50
94
|
{
|
|
51
95
|
type: "text",
|
|
52
|
-
text: JSON.stringify(
|
|
96
|
+
text: JSON.stringify(enrichedReport),
|
|
53
97
|
},
|
|
54
98
|
],
|
|
55
99
|
structuredContent: {
|
|
56
|
-
summaryCountReport:
|
|
100
|
+
summaryCountReport: enrichedReport,
|
|
57
101
|
},
|
|
58
102
|
};
|
|
59
103
|
}
|
|
@@ -63,21 +107,59 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
63
107
|
throw new Error("occupancyCountRequest is required");
|
|
64
108
|
}
|
|
65
109
|
const { deviceUuid, rangeStart, rangeEnd, interval } = occupancyCountRequest;
|
|
66
|
-
const
|
|
110
|
+
const startTimeMs = new Date(rangeStart).getTime();
|
|
111
|
+
const endTimeMs = new Date(rangeEnd).getTime();
|
|
112
|
+
const report = await getOccupancyCountReport(deviceUuid, startTimeMs, endTimeMs, interval, requestModifiers, sessionId);
|
|
113
|
+
const enrichedReport = { ...report };
|
|
114
|
+
try {
|
|
115
|
+
const faceCount = await getUniqueFaceCount(deviceUuid, startTimeMs, endTimeMs, requestModifiers, sessionId);
|
|
116
|
+
enrichedReport.faceCountEnrichment = faceCount;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.error("Failed to fetch face count enrichment for occupancy report", err);
|
|
120
|
+
}
|
|
121
|
+
const allZero = !report?.timeSeriesDataPoints?.length ||
|
|
122
|
+
report.timeSeriesDataPoints.every((dp) => !dp.eventCountMap ||
|
|
123
|
+
Object.values(dp.eventCountMap).every((v) => v === 0 || v === null || v === undefined));
|
|
124
|
+
if (allZero) {
|
|
125
|
+
try {
|
|
126
|
+
const camerasReport = await getOccupancyEnabledCameras(requestModifiers, sessionId);
|
|
127
|
+
const enabledUuids = new Set(camerasReport?.cameras?.map((c) => c.uuid).filter(Boolean) ?? []);
|
|
128
|
+
if (!enabledUuids.has(deviceUuid)) {
|
|
129
|
+
enrichedReport.occupancyEnabledCameras = camerasReport?.cameras?.map((c) => ({
|
|
130
|
+
uuid: c.uuid,
|
|
131
|
+
name: c.name,
|
|
132
|
+
locationUuid: c.locationUuid,
|
|
133
|
+
}));
|
|
134
|
+
enrichedReport.hint =
|
|
135
|
+
"This camera does not have occupancy counting enabled (no occupancy polygon defined). " +
|
|
136
|
+
"The occupancyEnabledCameras field lists cameras that do support occupancy counting. " +
|
|
137
|
+
"Face recognition data is available in faceCountEnrichment above.";
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
enrichedReport.hint =
|
|
141
|
+
"This camera has occupancy counting enabled but returned zero counts for the requested time range. " +
|
|
142
|
+
"Face recognition data is available in faceCountEnrichment above.";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
logger.error("Failed to fetch occupancy-enabled cameras for hint", err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
67
149
|
return {
|
|
68
150
|
content: [
|
|
69
151
|
{
|
|
70
152
|
type: "text",
|
|
71
|
-
text: JSON.stringify(
|
|
153
|
+
text: JSON.stringify(enrichedReport),
|
|
72
154
|
},
|
|
73
155
|
],
|
|
74
156
|
structuredContent: {
|
|
75
|
-
occupancyCountReport:
|
|
157
|
+
occupancyCountReport: enrichedReport,
|
|
76
158
|
},
|
|
77
159
|
};
|
|
78
160
|
}
|
|
79
161
|
if (requestType === RequestType.GET_OCCUPANCY_ENABLED_CAMERAS) {
|
|
80
|
-
const report = await getOccupancyEnabledCameras(
|
|
162
|
+
const report = await getOccupancyEnabledCameras(requestModifiers, sessionId);
|
|
81
163
|
return {
|
|
82
164
|
content: [
|
|
83
165
|
{
|
|
@@ -96,7 +178,7 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
96
178
|
throw new Error("lineCrossingEnabledCamerasRequest is required");
|
|
97
179
|
}
|
|
98
180
|
const { locationUuid } = lineCrossingEnabledCamerasRequest;
|
|
99
|
-
const report = await getLineCrossingEnabledCameras(locationUuid,
|
|
181
|
+
const report = await getLineCrossingEnabledCameras(locationUuid, requestModifiers, sessionId);
|
|
100
182
|
return {
|
|
101
183
|
content: [
|
|
102
184
|
{
|
|
@@ -114,8 +196,8 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
114
196
|
if (!thresholdCrossingCountRequest) {
|
|
115
197
|
throw new Error("thresholdCrossingCountRequest is required");
|
|
116
198
|
}
|
|
117
|
-
const { deviceUuid, rangeStart, rangeEnd, bucketSize, crossingObject, dedupe } = thresholdCrossingCountRequest;
|
|
118
|
-
const report = await getThresholdCrossingCountReport(deviceUuid, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), bucketSize, crossingObject, dedupe,
|
|
199
|
+
const { deviceUuid, rangeStart, rangeEnd, bucketSize, crossingObject, dedupe, } = thresholdCrossingCountRequest;
|
|
200
|
+
const report = await getThresholdCrossingCountReport(deviceUuid, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), bucketSize, crossingObject, dedupe, requestModifiers, sessionId);
|
|
119
201
|
return {
|
|
120
202
|
content: [
|
|
121
203
|
{
|
|
@@ -129,7 +211,7 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
129
211
|
};
|
|
130
212
|
}
|
|
131
213
|
if (requestType === RequestType.FIND_PROMPT_CONFIGURATIONS) {
|
|
132
|
-
const report = await findPromptConfigurations(
|
|
214
|
+
const report = await findPromptConfigurations(requestModifiers, sessionId);
|
|
133
215
|
return {
|
|
134
216
|
content: [
|
|
135
217
|
{
|
|
@@ -148,9 +230,9 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
148
230
|
throw new Error("customLLMReportRequest is required");
|
|
149
231
|
}
|
|
150
232
|
const { promptUuid, promptType, rangeStart, rangeEnd, interval } = customLLMReportRequest;
|
|
151
|
-
const report = await getCustomLLMReport(promptUuid, promptType, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), interval,
|
|
233
|
+
const report = await getCustomLLMReport(promptUuid, promptType, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), interval, requestModifiers, sessionId);
|
|
152
234
|
// Log the report data to help debug
|
|
153
|
-
|
|
235
|
+
logger.log("📊 Custom LLM Report Tool Response:", JSON.stringify({
|
|
154
236
|
promptType,
|
|
155
237
|
hasError: report?.error,
|
|
156
238
|
dataPointsCount: report?.timeSeriesDataPoints?.length,
|
|
@@ -168,6 +250,61 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
168
250
|
},
|
|
169
251
|
};
|
|
170
252
|
}
|
|
253
|
+
if (requestType === RequestType.GET_AUDIT_FEED) {
|
|
254
|
+
const { auditFeedRequest } = args;
|
|
255
|
+
if (!auditFeedRequest) {
|
|
256
|
+
throw new Error("auditFeedRequest is required");
|
|
257
|
+
}
|
|
258
|
+
const report = await getAuditFeed(new Date(auditFeedRequest.startTime).getTime(), new Date(auditFeedRequest.endTime).getTime(), requestModifiers, sessionId);
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: "text", text: JSON.stringify(report) }],
|
|
261
|
+
structuredContent: { auditFeedReport: report },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (requestType === RequestType.GET_DIAGNOSTIC_FEED) {
|
|
265
|
+
const { diagnosticFeedRequest } = args;
|
|
266
|
+
if (!diagnosticFeedRequest) {
|
|
267
|
+
throw new Error("diagnosticFeedRequest is required");
|
|
268
|
+
}
|
|
269
|
+
const report = await getDiagnosticFeed(new Date(diagnosticFeedRequest.startTime).getTime(), new Date(diagnosticFeedRequest.endTime).getTime(), requestModifiers, sessionId);
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: "text", text: JSON.stringify(report) }],
|
|
272
|
+
structuredContent: { diagnosticFeedReport: report },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (requestType === RequestType.GET_THRESHOLD_CROSSING_EVENTS) {
|
|
276
|
+
const { thresholdCrossingEventsRequest } = args;
|
|
277
|
+
if (!thresholdCrossingEventsRequest) {
|
|
278
|
+
throw new Error("thresholdCrossingEventsRequest is required");
|
|
279
|
+
}
|
|
280
|
+
const report = await getThresholdCrossingEvents(thresholdCrossingEventsRequest.deviceUuid, new Date(thresholdCrossingEventsRequest.startTime).getTime(), new Date(thresholdCrossingEventsRequest.endTime).getTime(), requestModifiers, sessionId);
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: JSON.stringify(report) }],
|
|
283
|
+
structuredContent: { thresholdCrossingEventsReport: report },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (requestType === RequestType.GET_CUSTOM_EVENTS_REPORT) {
|
|
287
|
+
const { customEventsReportRequest } = args;
|
|
288
|
+
if (!customEventsReportRequest) {
|
|
289
|
+
throw new Error("customEventsReportRequest is required");
|
|
290
|
+
}
|
|
291
|
+
const report = await getCustomEventsReport(customEventsReportRequest.promptUuid, new Date(customEventsReportRequest.startTime).getTime(), new Date(customEventsReportRequest.endTime).getTime(), customEventsReportRequest.interval, requestModifiers, sessionId);
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: "text", text: JSON.stringify(report) }],
|
|
294
|
+
structuredContent: { customEventsReport: report },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (requestType === RequestType.GET_PEOPLE_COUNT_EVENTS) {
|
|
298
|
+
const { peopleCountEventsRequest } = args;
|
|
299
|
+
if (!peopleCountEventsRequest) {
|
|
300
|
+
throw new Error("peopleCountEventsRequest is required");
|
|
301
|
+
}
|
|
302
|
+
const report = await getPeopleCountEvents(peopleCountEventsRequest.deviceUuids, requestModifiers, sessionId);
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: JSON.stringify(report) }],
|
|
305
|
+
structuredContent: { peopleCountEventsReport: report },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
171
308
|
return {
|
|
172
309
|
content: [
|
|
173
310
|
{
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { listRules, createRule, updateRule, deleteRule, getRuleRecords, } from "../api/rules-tool-api.js";
|
|
2
|
+
import { RulesToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/rules-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "rules-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool manages Rhombus automation rules for triggering actions based on events.
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${RulesToolRequestType.LIST}: List all automation rules in the organization.
|
|
10
|
+
- ${RulesToolRequestType.CREATE}: Create a new automation rule. Requires ruleName and ruleConfig (JSON string with the rule definition).
|
|
11
|
+
- ${RulesToolRequestType.UPDATE}: Update an existing rule. Requires ruleUuid and ruleConfig (JSON string with updated fields).
|
|
12
|
+
- ${RulesToolRequestType.DELETE}: Delete a rule. Requires ruleUuid.
|
|
13
|
+
- ${RulesToolRequestType.GET_RECORDS}: Get the event trigger history for a specific rule. Requires ruleUuid.
|
|
14
|
+
|
|
15
|
+
Rules can trigger notifications, recordings, and other actions based on events from cameras, sensors, doors, etc.
|
|
16
|
+
`;
|
|
17
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
18
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
19
|
+
try {
|
|
20
|
+
switch (args.requestType) {
|
|
21
|
+
case RulesToolRequestType.LIST: {
|
|
22
|
+
const rules = await listRules(requestModifiers, sessionId);
|
|
23
|
+
return createToolStructuredContent({ rules });
|
|
24
|
+
}
|
|
25
|
+
case RulesToolRequestType.CREATE: {
|
|
26
|
+
if (!args.ruleConfig) {
|
|
27
|
+
return createToolTextContent(JSON.stringify({ error: "ruleConfig is required for create." }));
|
|
28
|
+
}
|
|
29
|
+
const config = JSON.parse(args.ruleConfig);
|
|
30
|
+
if (args.ruleName)
|
|
31
|
+
config.name = args.ruleName;
|
|
32
|
+
const rule = await createRule(config, requestModifiers, sessionId);
|
|
33
|
+
return createToolStructuredContent({ rule });
|
|
34
|
+
}
|
|
35
|
+
case RulesToolRequestType.UPDATE: {
|
|
36
|
+
if (!args.ruleUuid || !args.ruleConfig) {
|
|
37
|
+
return createToolTextContent(JSON.stringify({ error: "ruleUuid and ruleConfig are required for update." }));
|
|
38
|
+
}
|
|
39
|
+
const updateConfig = JSON.parse(args.ruleConfig);
|
|
40
|
+
updateConfig.ruleUuid = args.ruleUuid;
|
|
41
|
+
const rule = await updateRule(updateConfig, requestModifiers, sessionId);
|
|
42
|
+
return createToolStructuredContent({
|
|
43
|
+
rule: { uuid: args.ruleUuid, ...rule },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
case RulesToolRequestType.DELETE: {
|
|
47
|
+
if (!args.ruleUuid) {
|
|
48
|
+
return createToolTextContent(JSON.stringify({ error: "ruleUuid is required for delete." }));
|
|
49
|
+
}
|
|
50
|
+
const rule = await deleteRule(args.ruleUuid, requestModifiers, sessionId);
|
|
51
|
+
return createToolStructuredContent({ rule });
|
|
52
|
+
}
|
|
53
|
+
case RulesToolRequestType.GET_RECORDS: {
|
|
54
|
+
if (!args.ruleUuid) {
|
|
55
|
+
return createToolTextContent(JSON.stringify({ error: "ruleUuid is required for get-records." }));
|
|
56
|
+
}
|
|
57
|
+
const ruleRecords = await getRuleRecords(args.ruleUuid, requestModifiers, sessionId);
|
|
58
|
+
return createToolStructuredContent({ ruleRecords });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
return createToolStructuredContent({ error: error.message });
|
|
65
|
+
}
|
|
66
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
67
|
+
}
|
|
68
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
69
|
+
};
|
|
70
|
+
export function createTool(server) {
|
|
71
|
+
server.registerTool(TOOL_NAME, {
|
|
72
|
+
description: TOOL_DESCRIPTION,
|
|
73
|
+
inputSchema: TOOL_ARGS,
|
|
74
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
75
|
+
}, TOOL_HANDLER);
|
|
76
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { searchLicensePlates, searchObjectsByColor, searchObjectsByText, searchMotionGrid, } from "../api/search-tool-api.js";
|
|
2
|
+
import { SearchToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/search-tool-types.js";
|
|
3
|
+
import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
|
|
4
|
+
const TOOL_NAME = "search-tool";
|
|
5
|
+
const TOOL_DESCRIPTION = `
|
|
6
|
+
This tool performs visual and data searches across Rhombus camera footage and events.
|
|
7
|
+
|
|
8
|
+
It has the following modes of operation, determined by the "requestType" parameter:
|
|
9
|
+
- ${SearchToolRequestType.LICENSE_PLATE}: Search for license plate sightings by plate number. Requires query (the plate number or partial match).
|
|
10
|
+
- ${SearchToolRequestType.OBJECT_BY_COLOR}: Search for objects by color in camera footage. Requires query (color name) and cameraUuid.
|
|
11
|
+
- ${SearchToolRequestType.OBJECT_BY_TEXT}: Semantic search for objects in footage using a text description. Requires query (text description).
|
|
12
|
+
- ${SearchToolRequestType.MOTION_SEARCH}: Search for motion events in a camera's field of view. Requires cameraUuid, startTime, and endTime.
|
|
13
|
+
|
|
14
|
+
All search types support optional startTime and endTime to narrow the search window.
|
|
15
|
+
Use the get-entity-tool with entityType CAMERA to get camera UUIDs.
|
|
16
|
+
`;
|
|
17
|
+
const TOOL_HANDLER = async (args, _extra) => {
|
|
18
|
+
const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
|
|
19
|
+
try {
|
|
20
|
+
const startTimeMs = args.startTime ? new Date(args.startTime).getTime() : undefined;
|
|
21
|
+
const endTimeMs = args.endTime ? new Date(args.endTime).getTime() : undefined;
|
|
22
|
+
switch (args.requestType) {
|
|
23
|
+
case SearchToolRequestType.LICENSE_PLATE: {
|
|
24
|
+
if (!args.query) {
|
|
25
|
+
return createToolTextContent(JSON.stringify({ error: "query is required for license-plate search." }));
|
|
26
|
+
}
|
|
27
|
+
const licensePlateResults = await searchLicensePlates(args.query, startTimeMs, endTimeMs, args.deviceUuids, args.locationUuids, requestModifiers, sessionId);
|
|
28
|
+
return createToolStructuredContent({ licensePlateResults });
|
|
29
|
+
}
|
|
30
|
+
case SearchToolRequestType.OBJECT_BY_COLOR: {
|
|
31
|
+
if (!args.query || !args.cameraUuid) {
|
|
32
|
+
return createToolTextContent(JSON.stringify({ error: "query and cameraUuid are required for object-by-color search." }));
|
|
33
|
+
}
|
|
34
|
+
const objectColorResults = await searchObjectsByColor(args.query, args.cameraUuid, startTimeMs, endTimeMs, requestModifiers, sessionId);
|
|
35
|
+
return createToolStructuredContent({ objectColorResults });
|
|
36
|
+
}
|
|
37
|
+
case SearchToolRequestType.OBJECT_BY_TEXT: {
|
|
38
|
+
if (!args.query) {
|
|
39
|
+
return createToolTextContent(JSON.stringify({ error: "query is required for object-by-text search." }));
|
|
40
|
+
}
|
|
41
|
+
const textSearchDeviceUuids = args.deviceUuids
|
|
42
|
+
?? (args.cameraUuid ? [args.cameraUuid] : null);
|
|
43
|
+
const objectTextResults = await searchObjectsByText(args.query, startTimeMs, endTimeMs, textSearchDeviceUuids, requestModifiers, sessionId);
|
|
44
|
+
return createToolStructuredContent({ objectTextResults });
|
|
45
|
+
}
|
|
46
|
+
case SearchToolRequestType.MOTION_SEARCH: {
|
|
47
|
+
if (!args.cameraUuid || !startTimeMs || !endTimeMs) {
|
|
48
|
+
return createToolTextContent(JSON.stringify({ error: "cameraUuid, startTime, and endTime are required for motion-search." }));
|
|
49
|
+
}
|
|
50
|
+
const motionResults = await searchMotionGrid(args.cameraUuid, startTimeMs, endTimeMs, requestModifiers, sessionId);
|
|
51
|
+
return createToolStructuredContent({ motionResults });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
return createToolStructuredContent({ error: error.message });
|
|
58
|
+
}
|
|
59
|
+
return createToolStructuredContent({ error: "Unknown error" });
|
|
60
|
+
}
|
|
61
|
+
return createToolStructuredContent({ error: "Invalid request type" });
|
|
62
|
+
};
|
|
63
|
+
export function createTool(server) {
|
|
64
|
+
server.registerTool(TOOL_NAME, {
|
|
65
|
+
description: TOOL_DESCRIPTION,
|
|
66
|
+
inputSchema: TOOL_ARGS,
|
|
67
|
+
outputSchema: OUTPUT_SCHEMA.shape,
|
|
68
|
+
}, TOOL_HANDLER);
|
|
69
|
+
}
|