rhombus-node-mcp 0.1.15 → 0.1.16

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.
Files changed (80) hide show
  1. package/README.md +9 -1
  2. package/dist/api/camera-tool-api.js +108 -0
  3. package/dist/api/clips-tool-api.js +36 -0
  4. package/dist/api/create-camera-policy-tool-api.js +9 -0
  5. package/dist/api/create-tool-api.js +53 -0
  6. package/dist/api/entity-lookup-tool-api.js +37 -0
  7. package/dist/api/events-tool-api.js +320 -0
  8. package/dist/api/faces-tool-api.js +110 -0
  9. package/dist/api/get-entity-tool-api.js +176 -0
  10. package/dist/api/get-org-information-tool-api.js +9 -0
  11. package/dist/api/location-tool-api.js +9 -0
  12. package/dist/api/lpr-tool-api.js +68 -0
  13. package/dist/api/policy-alerts-tool-api.js +42 -0
  14. package/dist/api/reboot-cameras-tool-api.js +34 -0
  15. package/dist/api/report-tool-api.js +426 -0
  16. package/dist/api/time-tool-api.js +90 -0
  17. package/dist/api/update-tool-api.js +148 -0
  18. package/dist/createServer.js +7 -1
  19. package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
  20. package/dist/disabled-tools/semantic-search-tool.js +90 -0
  21. package/dist/index.js +3 -5
  22. package/dist/logger.js +4 -3
  23. package/dist/network.js +42 -15
  24. package/dist/resources/routes.json.js +1 -1
  25. package/dist/services/embedding-service.js +153 -0
  26. package/dist/services/faiss-search-service.js +261 -0
  27. package/dist/tools/camera-tool.js +100 -0
  28. package/dist/tools/clips-tool.js +23 -37
  29. package/dist/tools/count-tool.js +25 -0
  30. package/dist/tools/create-camera-policy-tool.js +214 -0
  31. package/dist/tools/create-tool.js +25 -74
  32. package/dist/tools/entity-lookup-tool.js +35 -0
  33. package/dist/tools/events-tool.js +188 -92
  34. package/dist/tools/faces-tool.js +59 -132
  35. package/dist/tools/get-entity-tool.js +78 -0
  36. package/dist/tools/get-org-information-tool.js +18 -0
  37. package/dist/tools/location-tool.js +24 -31
  38. package/dist/tools/lpr-tool.js +71 -0
  39. package/dist/tools/policy-alerts-tool.js +32 -42
  40. package/dist/tools/reboot-cameras-tool.js +34 -0
  41. package/dist/tools/report-tool.js +190 -0
  42. package/dist/tools/time-conversion-tool.js +43 -0
  43. package/dist/tools/time-tool.js +17 -57
  44. package/dist/tools/update-tool.js +262 -0
  45. package/dist/transports/streamable-http.js +160 -57
  46. package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +13 -0
  47. package/dist/types/clips-tool-types.js +41 -0
  48. package/dist/types/create-camera-policy-tool-types.js +44 -0
  49. package/dist/types/create-tool-types.js +8 -0
  50. package/dist/types/deviceType.js +1 -0
  51. package/dist/types/endpoint-to-keys-tool-types.js +7 -0
  52. package/dist/types/entity-lookup-tool-types.js +70 -0
  53. package/dist/types/events-tools-types.js +257 -0
  54. package/dist/types/faces-tools-types.js +143 -0
  55. package/dist/types/get-entity-tool-types.js +25 -0
  56. package/dist/types/get-org-information-tool-types.js +3 -0
  57. package/dist/types/location-tool-types.js +11 -0
  58. package/dist/types/lpr-tool-types.js +97 -0
  59. package/dist/types/policy-alerts-tool-types.js +74 -0
  60. package/dist/types/reboot-cameras-tool-types.js +8 -0
  61. package/dist/types/report-tool-types.js +268 -0
  62. package/dist/types/schema-components.js +7093 -0
  63. package/dist/types/schema.js +1 -0
  64. package/dist/types/semantic-search-tool-types.js +5 -0
  65. package/dist/types/time-conversion-tool-types.js +8 -0
  66. package/dist/types/time-tool-types.js +11 -0
  67. package/dist/types/update-tool-types.js +186 -0
  68. package/dist/types/zod-schemas.js +21315 -0
  69. package/dist/types.js +17 -7
  70. package/dist/util.js +94 -2
  71. package/dist/utils/confirmation.js +1 -1
  72. package/dist/utils/reduce-output.js +35 -0
  73. package/dist/utils/remove-nulls.js +28 -0
  74. package/dist/utils/temp.js +8 -0
  75. package/dist/utils/timestampInput.js +12 -0
  76. package/package.json +23 -3
  77. package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
  78. package/dist/tools/devices/get-entity-tool.js +0 -118
  79. package/dist/tools/get-org-information.js +0 -17
  80. package/dist/tools/reboot-cameras.js +0 -62
@@ -0,0 +1,78 @@
1
+ import { getAccessControlledDoors, getAudioGateways, getBadgeReaders, getButtons, getCameraList, getDoorbellCameras, getDoorSensors, getEnvironmentalGateways, getEnvironmentalSensors, getKeypads, getMotionSensors, } from "../api/get-entity-tool-api.js";
2
+ import DeviceType from "../types/deviceType.js";
3
+ import { TOOL_ARGS } from "../types/get-entity-tool-types.js";
4
+ import { createToolTextContent, extractFromToolExtra } from "../util.js";
5
+ const TOOL_NAME = "get-entity-tool";
6
+ const TOOL_DESCRIPTION = `
7
+ Retrieves entities (or devices) of certain types.
8
+ Can request multiple entity types at once.
9
+ The return structure is a JSON string that contains the states of the requested entities.
10
+ This data is exact. Whatever entities exist will be returned here.`;
11
+ const TOOL_HANDLER = async (args, extra) => {
12
+ const { entityTypes, timeZone, filterBy, tempUnit } = args;
13
+ const { requestModifiers, sessionId } = extractFromToolExtra(extra);
14
+ const promises = [];
15
+ if (entityTypes.includes(DeviceType.CAMERA)) {
16
+ promises.push(getCameraList(requestModifiers, sessionId));
17
+ }
18
+ if (entityTypes.includes(DeviceType.DOORBELL_CAMERA)) {
19
+ promises.push(getDoorbellCameras(requestModifiers, sessionId));
20
+ }
21
+ if (entityTypes.includes(DeviceType.BADGE_READER)) {
22
+ promises.push(getBadgeReaders(requestModifiers, sessionId));
23
+ }
24
+ if (entityTypes.includes(DeviceType.ACCESS_CONTROL_DOOR)) {
25
+ promises.push(getAccessControlledDoors(requestModifiers, sessionId));
26
+ }
27
+ if (entityTypes.includes(DeviceType.AUDIO_GATEWAY)) {
28
+ promises.push(getAudioGateways(timeZone, requestModifiers, sessionId));
29
+ }
30
+ if (entityTypes.includes(DeviceType.DOOR_SENSOR)) {
31
+ promises.push(getDoorSensors(requestModifiers, sessionId));
32
+ }
33
+ if (entityTypes.includes(DeviceType.ENVIRONMENTAL_SENSOR)) {
34
+ promises.push(getEnvironmentalSensors(timeZone, tempUnit, requestModifiers, sessionId));
35
+ }
36
+ if (entityTypes.includes(DeviceType.MOTION_SENSOR)) {
37
+ promises.push(getMotionSensors(requestModifiers, sessionId));
38
+ }
39
+ if (entityTypes.includes(DeviceType.BUTTON)) {
40
+ promises.push(getButtons(timeZone, requestModifiers, sessionId));
41
+ }
42
+ if (entityTypes.includes(DeviceType.KEYPAD)) {
43
+ promises.push(getKeypads(requestModifiers, sessionId));
44
+ }
45
+ if (entityTypes.includes(DeviceType.ENVIRONMENTAL_GATEWAY)) {
46
+ promises.push(getEnvironmentalGateways(timeZone, requestModifiers, sessionId));
47
+ }
48
+ const responses = await Promise.all(promises);
49
+ // apply filters
50
+ for (let i = 0; i < responses.length; i++) {
51
+ const response = responses[i];
52
+ // look through keys and find any with an array
53
+ for (const key of Object.keys(response)) {
54
+ const value = response[key];
55
+ if (Array.isArray(value)) {
56
+ // then filter
57
+ response[key] = value.filter((item) => {
58
+ let pass = true;
59
+ if (item.locationUuid && filterBy.locationUuids && filterBy.locationUuids.length > 0) {
60
+ pass = pass && filterBy.locationUuids.includes(item.locationUuid);
61
+ }
62
+ return pass;
63
+ });
64
+ response[`${key}Count`] = response[key].length;
65
+ }
66
+ }
67
+ }
68
+ const ret = {
69
+ ...responses.reduce((prev, curr) => ({
70
+ ...prev,
71
+ ...curr,
72
+ }), {}),
73
+ };
74
+ return createToolTextContent(JSON.stringify(ret));
75
+ };
76
+ export function createTool(server) {
77
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
78
+ }
@@ -0,0 +1,18 @@
1
+ import { getOrg } from "../api/get-org-information-tool-api.js";
2
+ import { TOOL_ARGS } from "../types/get-org-information-tool-types.js";
3
+ const TOOL_NAME = "get-org-information";
4
+ const TOOL_DESCRIPTION = "Get general information about the organization including org name, camera configuration defaults, contact information, and org settings.";
5
+ const TOOL_HANDLER = async (args, extra) => {
6
+ const org = await getOrg(extra._meta?.requestModifiers, extra.sessionId);
7
+ return {
8
+ content: [
9
+ {
10
+ type: "text",
11
+ text: JSON.stringify(org),
12
+ },
13
+ ],
14
+ };
15
+ };
16
+ export function createTool(server) {
17
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
18
+ }
@@ -1,33 +1,26 @@
1
- import { z } from "zod";
2
- import { postApi } from "../network.js";
3
- async function getLocations(requestModifiers) {
4
- return await postApi("/location/getLocationsV2", {}, requestModifiers);
5
- }
1
+ import { getLocations } from "../api/location-tool-api.js";
2
+ import { TOOL_ARGS } from "../types/location-tool-types.js";
3
+ const TOOL_NAME = "location-tool";
4
+ const TOOL_DESCRIPTION = `This tool performs operations on locations.
5
+ - 'get': Retrieves all locations.`;
6
+ const TOOL_HANDLER = async (args, extra) => {
7
+ const { action } = args;
8
+ let ret;
9
+ switch (action) {
10
+ case "get":
11
+ ret = await getLocations(extra._meta?.requestModifiers, extra.sessionId);
12
+ break;
13
+ case "update":
14
+ ret = { error: true, status: "not implemented" };
15
+ break;
16
+ default:
17
+ ret = { error: true, status: `unsupported location tool call: ${action}` };
18
+ break;
19
+ }
20
+ return {
21
+ content: [{ type: "text", text: JSON.stringify(ret) }],
22
+ };
23
+ };
6
24
  export function createTool(server) {
7
- server.tool("location-tool", `This tool performs operations on locations.
8
- - 'get': Retrieves all locations.`, {
9
- action: z.enum(["get", "update"]),
10
- locationUpdate: z
11
- .object({
12
- uuid: z.string(),
13
- name: z.string(),
14
- })
15
- .optional(),
16
- }, async ({ action }, extra) => {
17
- let ret;
18
- switch (action) {
19
- case "get":
20
- ret = await getLocations(extra._meta?.requestModifiers);
21
- break;
22
- case "update":
23
- ret = { error: true, status: "not implemented" };
24
- break;
25
- default:
26
- ret = { error: true, status: `unsupported location tool call: ${action}` };
27
- break;
28
- }
29
- return {
30
- content: [{ type: "text", text: JSON.stringify(ret) }],
31
- };
32
- });
25
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
33
26
  }
@@ -0,0 +1,71 @@
1
+ import { getSavedVehicles, getVehicleEvents, getVehicleLabels } from "../api/lpr-tool-api.js";
2
+ import { LprToolRequestType, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/lpr-tool-types.js";
3
+ import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
+ const TOOL_NAME = "lpr-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ This tool interacts with the Rhombus LPR system to retrieve information about license plate recognition events and registered license plates.
7
+
8
+ The system's cameras may have LPR enabled, and when it is enabled, it will detect "license plate recognition" events when it sees a license plate
9
+ come into view. However, it is possible that the recognized license is only a partial match, so keep that in mind when using this tool.
10
+ Users will be able to save license plates into the system, and then additionally label them with a name.
11
+
12
+ This tool has 3 modes of operation, determined by the "requestType" parameter:
13
+ - ${LprToolRequestType.GET_VEHICLE_EVENTS}: Retrieves a list of vehicle events that have been detected by the system. Please keep in mind that this has the *potential*
14
+ to return a lot of data. However, 7 days should be a reasonable time range to start from if the user is not specific.
15
+ - ${LprToolRequestType.GET_SAVED_VEHICLES}: Retrieves a list of saved vehicles that have been saved in the organization.
16
+ - ${LprToolRequestType.GET_VEHICLE_LABELS}: Retrieves a list of vehicle labels that have been saved in the organization.
17
+
18
+ Its very likely that "vehicle", "car", and "license plates" are used interchangeably. Please keep this in mind.
19
+ `;
20
+ const TOOL_HANDLER = async (args, _extra) => {
21
+ const responseType = args.requestType;
22
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
23
+ try {
24
+ switch (responseType) {
25
+ case LprToolRequestType.GET_VEHICLE_EVENTS: {
26
+ const { vehicleEventsArgs } = args;
27
+ if (!vehicleEventsArgs) {
28
+ return createToolTextContent(JSON.stringify({
29
+ error: "vehicleEventsArgs is required. Please try again.",
30
+ }));
31
+ }
32
+ const vehicleEvents = await getVehicleEvents(vehicleEventsArgs, args.timeZone, requestModifiers, sessionId);
33
+ return createToolStructuredContent({
34
+ vehicleEvents,
35
+ });
36
+ }
37
+ case LprToolRequestType.GET_SAVED_VEHICLES: {
38
+ const savedVehicles = await getSavedVehicles(args.timeZone, requestModifiers, sessionId);
39
+ return createToolStructuredContent({
40
+ savedVehicles,
41
+ });
42
+ }
43
+ case LprToolRequestType.GET_VEHICLE_LABELS: {
44
+ const vehicleLabels = await getVehicleLabels(requestModifiers, sessionId);
45
+ return createToolStructuredContent({
46
+ vehicleLabels,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error) {
53
+ return createToolStructuredContent({
54
+ error: error.message,
55
+ });
56
+ }
57
+ return createToolStructuredContent({
58
+ error: "Unknown error",
59
+ });
60
+ }
61
+ return createToolStructuredContent({
62
+ error: "Invalid request type",
63
+ });
64
+ };
65
+ export function createTool(server) {
66
+ server.registerTool(TOOL_NAME, {
67
+ description: TOOL_DESCRIPTION,
68
+ inputSchema: TOOL_ARGS,
69
+ outputSchema: OUTPUT_SCHEMA.shape,
70
+ }, TOOL_HANDLER);
71
+ }
@@ -1,35 +1,7 @@
1
- import { z } from "zod";
2
- import { postApi } from "../network.js";
3
- const PolicyAlertsArgs = z.object({
4
- afterTimestampMs: z
5
- .number()
6
- .optional()
7
- .describe("The start of the time range (in milliseconds since epoch) for which to retrieve alerts. Only alerts that occurred AFTER this timestamp will be returned."),
8
- beforeTimestampMs: z
9
- .number()
10
- .optional()
11
- .describe("The end of the time range (in milliseconds since epoch) for which to retrieve alerts. Only alerts that occurred BEFORE this timestamp will be returned."),
12
- deviceFilter: z
13
- .array(z.string())
14
- .optional()
15
- .describe("A list of UUIDs representing the specific devices to filter alerts by. Only alerts emitted by these devices will be returned."),
16
- locationFilter: z
17
- .array(z.string())
18
- .optional()
19
- .describe("A list of UUIDs representing the specific locations to filter alerts by. Only alerts associated with these locations will be returned."),
20
- maxResults: z
21
- .number()
22
- .optional()
23
- .describe("The maximum number of policy alerts to return. The system may default to a reasonable number (e.g., 20) if not specified, but there is a hard cap (e.g., 100) on the maximum results the API will return."),
24
- // these may be pagination args, we'll leave them out for now
25
- // lastTimestampMs: z.number().optional(),
26
- // lastUuid: z.number().optional(),
27
- });
28
- async function getPolicyAlerts(args, requestModifiers) {
29
- return await postApi("/event/getPolicyAlertsV2", args, requestModifiers);
30
- }
31
- export function createTool(server) {
32
- server.tool("policy-alerts-tool", `
1
+ import { getExpiringPolicyAlerts, getPolicyAlerts } from "../api/policy-alerts-tool-api.js";
2
+ import { ApiPayloadSchema, OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/policy-alerts-tool-types.js";
3
+ const TOOL_NAME = "policy-alerts-tool";
4
+ const TOOL_DESCRIPTION = `
33
5
  Retrieves Rhombus policy alerts. Policy alerts in the Rhombus system are generated based on user-defined alert
34
6
  policies configured in the Rhombus Console. These policies trigger alerts when specific events occur, such as:
35
7
  * AI & Computer Vision Events: Based on intelligent video analytics for motion, people, vehicles, facial recognition,
@@ -38,19 +10,37 @@ policies configured in the Rhombus Console. These policies trigger alerts when s
38
10
  * Physical or Visual Tamper: Detection of physical movement of a device or obstruction of a camera's field of view.
39
11
  * Access Control Events: Such as unauthorized access attempts in restricted areas.
40
12
 
13
+ Can inquire about labels that have been seen.
14
+
41
15
  Please note, this is not an exhaustive list, and there may be other types of triggers or events that generate
42
16
  policy alerts within the Rhombus system.
43
17
 
44
- This tool allows you to filter existing alerts by a specific time range (before or after a timestamp in milliseconds),
18
+ This tool allows you to filter existing alerts by existing/expiring, a specific time range (before or after a timestamp in ISO 8601 format),
45
19
  by a list of device UUIDs, or by a list of location UUIDs.
46
20
  You can also specify the maximum number of results to return.
47
- The output is provided in JSON format.`, {
48
- ...PolicyAlertsArgs.shape,
49
- }, async ({ ...args }, extra) => {
50
- let ret;
51
- ret = await getPolicyAlerts(args, extra._meta?.requestModifiers);
52
- return {
53
- content: [{ type: "text", text: JSON.stringify(ret) }],
54
- };
55
- });
21
+ The output is provided in JSON format.`;
22
+ const TOOL_HANDLER = async (args, extra) => {
23
+ const payload = ApiPayloadSchema.parse(args);
24
+ let ret;
25
+ switch (args.queryType) {
26
+ case "existing":
27
+ ret = await getPolicyAlerts(payload, extra._meta?.requestModifiers, extra.sessionId);
28
+ break;
29
+ case "expiringSoon":
30
+ await getExpiringPolicyAlerts(payload, extra._meta?.requestModifiers, extra.sessionId);
31
+ break;
32
+ default:
33
+ throw new Error(`Unsupported queryType: ${args.queryType}`);
34
+ }
35
+ return {
36
+ content: [{ type: "text", text: JSON.stringify(ret) }],
37
+ structuredContent: ret,
38
+ };
39
+ };
40
+ export function createTool(server) {
41
+ server.registerTool(TOOL_NAME, {
42
+ description: TOOL_DESCRIPTION,
43
+ inputSchema: TOOL_ARGS,
44
+ outputSchema: OUTPUT_SCHEMA.shape,
45
+ }, TOOL_HANDLER);
56
46
  }
@@ -0,0 +1,34 @@
1
+ import { isConfirmed, requireConfirmation } from "../utils/confirmation.js";
2
+ import { rebootCameras } from "../api/reboot-cameras-tool-api.js";
3
+ import { TOOL_ARGS } from "../types/reboot-cameras-tool-types.js";
4
+ const TOOL_NAME = "reboot-cameras";
5
+ const TOOL_DESCRIPTION = "this tool is for rebooting one or more cameras causing them to reconnect to the server, this is a helpful option when a camera is experiencing connectivity issues or is in need of troubleshooting. THIS TOOL PERFORMS AN ACTION.";
6
+ const TOOL_HANDLER = async (args, extra) => {
7
+ const { cameraUuids, confirmationId } = args;
8
+ const confirmation = requireConfirmation(confirmationId);
9
+ if (!isConfirmed(confirmation)) {
10
+ return confirmation;
11
+ }
12
+ const cameraRebootData = await rebootCameras(cameraUuids, extra._meta?.requestModifiers, extra.sessionId);
13
+ if (!cameraRebootData) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text: "Failed to reboot cameras",
19
+ },
20
+ ],
21
+ };
22
+ }
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: JSON.stringify(cameraRebootData),
28
+ },
29
+ ],
30
+ };
31
+ };
32
+ export function createTool(server) {
33
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
34
+ }
@@ -0,0 +1,190 @@
1
+ import { TOOL_ARGS, OUTPUT_SCHEMA, RequestType, } from "../types/report-tool-types.js";
2
+ import { getOccupancyCountReport, getSummaryCountReport, getOccupancyEnabledCameras, getLineCrossingEnabledCameras, getThresholdCrossingCountReport, findPromptConfigurations, getCustomLLMReport, } from "../api/report-tool-api.js";
3
+ const TOOL_NAME = "report-tool";
4
+ const TOOL_DESCRIPTION = `
5
+ This tool generates summary count reports for various types of data within the Rhombus system.
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.
10
+
11
+ This tool can also retrieve a list of cameras that have occupancy reporting enabled, which is useful as a first step
12
+ before running GET_SUMMARY_COUNT_REPORT since the uuid pulled from summaryCountRequest should always be present on
13
+ the list returned from getOccupancyEnabledCameras.
14
+
15
+ Additionally, this tool supports line crossing analytics:
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).
26
+
27
+ The tool also supports custom user-defined event reporting:
28
+ - FIND_PROMPT_CONFIGURATIONS: Retrieves all custom event prompt configurations. This should be called first to discover
29
+ what custom events are available for reporting (e.g., "black dog sightings", "delivery truck arrivals", etc.).
30
+ Each configuration includes the prompt text, UUID, and promptType (COUNT, PERCENT, or BOOLEAN) needed for generating reports.
31
+ - GET_CUSTOM_LLM_REPORT: Generates time series reports for a specific custom event using the prompt UUID.
32
+ The promptType field from the configuration determines which API endpoint is used:
33
+ • COUNT: Returns numeric counts (e.g., "3 black dogs detected")
34
+ • PERCENT: Returns percentage values (e.g., "75% occupancy")
35
+ • BOOLEAN: Returns true/false values (e.g., "parking lot full: true")
36
+ Returns aggregated data over specified time intervals (minutely, quarter-hourly, hourly, daily, weekly, monthly).
37
+ Use this after finding the appropriate prompt configuration to get historical data for custom events.
38
+ `;
39
+ const TOOL_HANDLER = async (args, extra) => {
40
+ const { requestType } = args;
41
+ if (requestType === RequestType.GET_SUMMARY_COUNT_REPORT) {
42
+ const { summaryCountRequest } = args;
43
+ if (!summaryCountRequest) {
44
+ throw new Error("summaryCountRequest is required");
45
+ }
46
+ const { interval, scope, types, rangeStart, rangeEnd, uuid } = summaryCountRequest;
47
+ const report = await getSummaryCountReport(interval, scope, types, uuid ?? undefined, new Date(rangeEnd).getTime(), new Date(rangeStart).getTime(), extra._meta?.requestModifiers, extra.sessionId, args.summaryCountRequest.timeZone);
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: JSON.stringify(report),
53
+ },
54
+ ],
55
+ structuredContent: {
56
+ summaryCountReport: report,
57
+ },
58
+ };
59
+ }
60
+ if (requestType === RequestType.GET_OCCUPANCY_COUNT_REPORT) {
61
+ const { occupancyCountRequest } = args;
62
+ if (!occupancyCountRequest) {
63
+ throw new Error("occupancyCountRequest is required");
64
+ }
65
+ const { deviceUuid, rangeStart, rangeEnd, interval } = occupancyCountRequest;
66
+ const report = await getOccupancyCountReport(deviceUuid, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), interval, extra._meta?.requestModifiers, extra.sessionId);
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify(report),
72
+ },
73
+ ],
74
+ structuredContent: {
75
+ occupancyCountReport: report,
76
+ },
77
+ };
78
+ }
79
+ if (requestType === RequestType.GET_OCCUPANCY_ENABLED_CAMERAS) {
80
+ const report = await getOccupancyEnabledCameras(extra._meta?.requestModifiers, extra.sessionId);
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: JSON.stringify(report),
86
+ },
87
+ ],
88
+ structuredContent: {
89
+ occupancyEnabledCamerasReport: report,
90
+ },
91
+ };
92
+ }
93
+ if (requestType === RequestType.GET_LINE_CROSSING_ENABLED_CAMERAS) {
94
+ const { lineCrossingEnabledCamerasRequest } = args;
95
+ if (!lineCrossingEnabledCamerasRequest) {
96
+ throw new Error("lineCrossingEnabledCamerasRequest is required");
97
+ }
98
+ const { locationUuid } = lineCrossingEnabledCamerasRequest;
99
+ const report = await getLineCrossingEnabledCameras(locationUuid, extra._meta?.requestModifiers, extra.sessionId);
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: JSON.stringify(report),
105
+ },
106
+ ],
107
+ structuredContent: {
108
+ lineCrossingEnabledCamerasReport: report,
109
+ },
110
+ };
111
+ }
112
+ if (requestType === RequestType.GET_THRESHOLD_CROSSING_COUNT_REPORT) {
113
+ const { thresholdCrossingCountRequest } = args;
114
+ if (!thresholdCrossingCountRequest) {
115
+ throw new Error("thresholdCrossingCountRequest is required");
116
+ }
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, extra._meta?.requestModifiers, extra.sessionId);
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: JSON.stringify(report),
124
+ },
125
+ ],
126
+ structuredContent: {
127
+ thresholdCrossingCountReport: report,
128
+ },
129
+ };
130
+ }
131
+ if (requestType === RequestType.FIND_PROMPT_CONFIGURATIONS) {
132
+ const report = await findPromptConfigurations(extra._meta?.requestModifiers, extra.sessionId);
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify(report),
138
+ },
139
+ ],
140
+ structuredContent: {
141
+ promptConfigurationsReport: report,
142
+ },
143
+ };
144
+ }
145
+ if (requestType === RequestType.GET_CUSTOM_LLM_REPORT) {
146
+ const { customLLMReportRequest } = args;
147
+ if (!customLLMReportRequest) {
148
+ throw new Error("customLLMReportRequest is required");
149
+ }
150
+ const { promptUuid, promptType, rangeStart, rangeEnd, interval } = customLLMReportRequest;
151
+ const report = await getCustomLLMReport(promptUuid, promptType, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), interval, extra._meta?.requestModifiers, extra.sessionId);
152
+ // Log the report data to help debug
153
+ console.log("📊 Custom LLM Report Tool Response:", JSON.stringify({
154
+ promptType,
155
+ hasError: report?.error,
156
+ dataPointsCount: report?.timeSeriesDataPoints?.length,
157
+ firstDataPoint: report?.timeSeriesDataPoints?.[0],
158
+ }));
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: JSON.stringify(report),
164
+ },
165
+ ],
166
+ structuredContent: {
167
+ customLLMReport: report,
168
+ },
169
+ };
170
+ }
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: "",
176
+ },
177
+ ],
178
+ structuredContent: {
179
+ error: true,
180
+ errorMsg: "Error while fetching report information",
181
+ },
182
+ };
183
+ };
184
+ export function createTool(server) {
185
+ server.registerTool(TOOL_NAME, {
186
+ description: TOOL_DESCRIPTION,
187
+ inputSchema: TOOL_ARGS.shape,
188
+ outputSchema: OUTPUT_SCHEMA.shape,
189
+ }, TOOL_HANDLER);
190
+ }
@@ -0,0 +1,43 @@
1
+ import { TOOL_ARGS } from "../types/time-conversion-tool-types.js";
2
+ const TOOL_NAME = "time-conversion-tool";
3
+ const TOOL_DESCRIPTION = "This tool is capable of converting to and from epoch and ISO 8601 timestamps. Use this tool when you have one time format and need to convert to the other format. The tool will automatically detect the input format and convert to the other format.";
4
+ const TOOL_HANDLER = async (args, extra) => {
5
+ const { time_to_convert } = args;
6
+ if (typeof time_to_convert === "string") {
7
+ // Input is ISO 8601, convert to epoch
8
+ const date = new Date(time_to_convert);
9
+ return {
10
+ content: [
11
+ {
12
+ type: "text",
13
+ text: JSON.stringify({ epoch: date.getTime() }),
14
+ },
15
+ ],
16
+ };
17
+ }
18
+ else if (typeof time_to_convert === "number") {
19
+ // Input is epoch, convert to ISO 8601
20
+ const date = new Date(time_to_convert);
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: JSON.stringify({ iso: date.toISOString() }),
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ else {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: JSON.stringify({ error: "Invalid input type for time_to_convert" }),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ };
41
+ export function createTool(server) {
42
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
43
+ }
@@ -1,59 +1,19 @@
1
- import { parse } from "chrono-node";
2
- import { DateTime } from "luxon";
3
- import { z } from "zod";
4
- import { logger } from "../logger.js";
5
- function nullToUndefined(value) {
6
- return value === null ? undefined : value;
7
- }
1
+ import { parseTimeDescription } from "../api/time-tool-api.js";
2
+ import { TOOL_ARGS } from "../types/time-tool-types.js";
3
+ const TOOL_NAME = "time-tool";
4
+ const TOOL_DESCRIPTION = "This tool is capable of returning the time from a natural language query. If the user asks about the 'current time' use this tool. Try to kee time_description as close to the users initial query as possible. For example if someone says 'was X person seen today?' then time_description should be 'today'.";
5
+ const TOOL_HANDLER = async (args, extra) => {
6
+ const { time_description, timezone } = args;
7
+ const result = parseTimeDescription(time_description ?? undefined, timezone ?? undefined, extra);
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: JSON.stringify(result),
13
+ },
14
+ ],
15
+ };
16
+ };
8
17
  export function createTool(server) {
9
- server.tool("time-tool", "Tool for converting a natural language time description into a timestamp in milliseconds.", {
10
- time_description: z
11
- .string()
12
- .describe("A natural language description of the time (e.g., '2pm today', 'tomorrow at noon')."),
13
- timezone: z
14
- .string()
15
- .optional()
16
- .describe("Optional IANA timezone string (e.g., 'America/Los_Angeles', 'UTC'). Will default to system timezone if not provided."),
17
- }, async ({ time_description, timezone }, extra) => {
18
- logger.info("EXTRA", extra);
19
- // console.error(`🕛 handling tool call for time ${time_description} using timezone ${timezone}`);
20
- const now = DateTime.now()
21
- .setZone(timezone || undefined)
22
- .toJSDate();
23
- const parsed = parse(time_description, now, { forwardDate: true });
24
- if (!parsed || parsed.length === 0) {
25
- throw new Error(`Could not parse time description: ${time_description}`);
26
- }
27
- const dateComponents = parsed[0].start;
28
- if (!dateComponents) {
29
- throw new Error("Parsed time has no start component");
30
- }
31
- const dt = DateTime.fromObject({
32
- year: nullToUndefined(dateComponents.get("year")),
33
- month: nullToUndefined(dateComponents.get("month")),
34
- day: nullToUndefined(dateComponents.get("day")),
35
- hour: nullToUndefined(dateComponents.get("hour")),
36
- minute: nullToUndefined(dateComponents.get("minute")),
37
- second: nullToUndefined(dateComponents.get("second")),
38
- millisecond: 0,
39
- }, {
40
- zone: timezone || "local",
41
- });
42
- if (!dt.isValid) {
43
- throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
44
- }
45
- const timestamp = dt.toMillis();
46
- return {
47
- content: [
48
- {
49
- type: "text",
50
- text: JSON.stringify({
51
- timestamp,
52
- iso: dt.toISO(),
53
- timezone: dt.zoneName,
54
- }),
55
- },
56
- ],
57
- };
58
- });
18
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_ARGS, TOOL_HANDLER);
59
19
  }