UNPKG

rhombus-node-mcp

Version:
332 lines (318 loc) 18.2 kB
import { findPromptConfigurations, getAuditFeed, getCustomEventsReport, getCustomLLMReport, getDiagnosticFeed, getLineCrossingEnabledCameras, getOccupancyCountReport, getOccupancyEnabledCameras, getPeopleCountEvents, getSummaryCountReport, getThresholdCrossingCountReport, getThresholdCrossingEvents, getUniqueFaceCount, } from "../api/report-tool-api.js"; import { logger } from "../logger.js"; import { OUTPUT_SCHEMA, RequestType, TOOL_ARGS, } from "../types/report-tool-types.js"; import { extractFromToolExtra } from "../util.js"; const TOOL_NAME = "report-tool"; const TOOL_DESCRIPTION = ` **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. **Interval guidance:** A shorter interval (HOURLY instead of DAILY) gives a better representation of data over time. Balance interval and range so you don't request too much data. For ranges spanning a week or so, HOURLY is appropriate. --- **People / occupancy counting strategy** When asked to count people on a camera or at a location, follow this strategy: 1. **Always call GET_OCCUPANCY_ENABLED_CAMERAS first** to discover which cameras have occupancy counting enabled. 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. 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. 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."). **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. --- **Summary and occupancy** - **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. - **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. - **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. --- **Line crossing** - **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. - **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). --- **Custom LLM events** - **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. - **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. - **GET_CUSTOM_EVENTS_REPORT:** Raw individual event values only (not aggregated). Use only when you need per-event granularity, not for reports or trends. --- **Audit and diagnostics** - **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). - **GET_DIAGNOSTIC_FEED:** Device diagnostic events over a time range. - **GET_THRESHOLD_CROSSING_EVENTS:** Individual line-crossing events (not aggregated counts). - **GET_PEOPLE_COUNT_EVENTS:** Most recent people count readings for specified devices. `; const TOOL_HANDLER = async (args, extra) => { const { requestType } = args; const { requestModifiers, sessionId } = extractFromToolExtra(extra); if (requestType === RequestType.GET_SUMMARY_COUNT_REPORT) { const { summaryCountRequest } = args; if (!summaryCountRequest) { throw new Error("summaryCountRequest is required"); } const { interval, scope, types, rangeStart, rangeEnd, uuid, timeZone } = summaryCountRequest; const startTimeMs = new Date(rangeStart).getTime(); const endTimeMs = new Date(rangeEnd).getTime(); const report = await getSummaryCountReport(interval, scope, types, uuid ?? undefined, endTimeMs, startTimeMs, requestModifiers, sessionId, timeZone); const enrichedReport = { ...report }; const isPeopleQuery = types.includes("PEOPLE") && scope === "DEVICE" && uuid; if (isPeopleQuery) { try { const faceCount = await getUniqueFaceCount(uuid, startTimeMs, endTimeMs, requestModifiers, sessionId); enrichedReport.faceCountEnrichment = faceCount; } catch (err) { logger.error("Failed to fetch face count enrichment for summary report", err); } const allZero = !report?.timeSeriesDataPoints?.length || report.timeSeriesDataPoints.every((dp) => !dp.eventCountMap || Object.values(dp.eventCountMap).every((v) => v === 0 || v === null || v === undefined)); if (allZero) { try { const camerasReport = await getOccupancyEnabledCameras(requestModifiers, sessionId); enrichedReport.occupancyEnabledCameras = camerasReport?.cameras?.map((c) => ({ uuid: c.uuid, name: c.name, locationUuid: c.locationUuid, })); enrichedReport.hint = "People detection returned zero results for this camera. This usually means people counting is not enabled on this device. " + "The occupancyEnabledCameras field lists cameras that support occupancy counting. " + "Face recognition data is available in faceCountEnrichment above."; } catch (err) { logger.error("Failed to fetch occupancy-enabled cameras for hint", err); } } } return { content: [ { type: "text", text: JSON.stringify(enrichedReport), }, ], structuredContent: { summaryCountReport: enrichedReport, }, }; } if (requestType === RequestType.GET_OCCUPANCY_COUNT_REPORT) { const { occupancyCountRequest } = args; if (!occupancyCountRequest) { throw new Error("occupancyCountRequest is required"); } const { deviceUuid, rangeStart, rangeEnd, interval } = occupancyCountRequest; const startTimeMs = new Date(rangeStart).getTime(); const endTimeMs = new Date(rangeEnd).getTime(); const report = await getOccupancyCountReport(deviceUuid, startTimeMs, endTimeMs, interval, requestModifiers, sessionId); const enrichedReport = { ...report }; try { const faceCount = await getUniqueFaceCount(deviceUuid, startTimeMs, endTimeMs, requestModifiers, sessionId); enrichedReport.faceCountEnrichment = faceCount; } catch (err) { logger.error("Failed to fetch face count enrichment for occupancy report", err); } const allZero = !report?.timeSeriesDataPoints?.length || report.timeSeriesDataPoints.every((dp) => !dp.eventCountMap || Object.values(dp.eventCountMap).every((v) => v === 0 || v === null || v === undefined)); if (allZero) { try { const camerasReport = await getOccupancyEnabledCameras(requestModifiers, sessionId); const enabledUuids = new Set(camerasReport?.cameras?.map((c) => c.uuid).filter(Boolean) ?? []); if (!enabledUuids.has(deviceUuid)) { enrichedReport.occupancyEnabledCameras = camerasReport?.cameras?.map((c) => ({ uuid: c.uuid, name: c.name, locationUuid: c.locationUuid, })); enrichedReport.hint = "This camera does not have occupancy counting enabled (no occupancy polygon defined). " + "The occupancyEnabledCameras field lists cameras that do support occupancy counting. " + "Face recognition data is available in faceCountEnrichment above."; } else { enrichedReport.hint = "This camera has occupancy counting enabled but returned zero counts for the requested time range. " + "Face recognition data is available in faceCountEnrichment above."; } } catch (err) { logger.error("Failed to fetch occupancy-enabled cameras for hint", err); } } return { content: [ { type: "text", text: JSON.stringify(enrichedReport), }, ], structuredContent: { occupancyCountReport: enrichedReport, }, }; } if (requestType === RequestType.GET_OCCUPANCY_ENABLED_CAMERAS) { const report = await getOccupancyEnabledCameras(requestModifiers, sessionId); return { content: [ { type: "text", text: JSON.stringify(report), }, ], structuredContent: { occupancyEnabledCamerasReport: report, }, }; } if (requestType === RequestType.GET_LINE_CROSSING_ENABLED_CAMERAS) { const { lineCrossingEnabledCamerasRequest } = args; if (!lineCrossingEnabledCamerasRequest) { throw new Error("lineCrossingEnabledCamerasRequest is required"); } const { locationUuid } = lineCrossingEnabledCamerasRequest; const report = await getLineCrossingEnabledCameras(locationUuid, requestModifiers, sessionId); return { content: [ { type: "text", text: JSON.stringify(report), }, ], structuredContent: { lineCrossingEnabledCamerasReport: report, }, }; } if (requestType === RequestType.GET_THRESHOLD_CROSSING_COUNT_REPORT) { const { thresholdCrossingCountRequest } = args; if (!thresholdCrossingCountRequest) { throw new Error("thresholdCrossingCountRequest is required"); } const { deviceUuid, rangeStart, rangeEnd, bucketSize, crossingObject, dedupe, } = thresholdCrossingCountRequest; const report = await getThresholdCrossingCountReport(deviceUuid, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), bucketSize, crossingObject, dedupe, requestModifiers, sessionId); return { content: [ { type: "text", text: JSON.stringify(report), }, ], structuredContent: { thresholdCrossingCountReport: report, }, }; } if (requestType === RequestType.FIND_PROMPT_CONFIGURATIONS) { const report = await findPromptConfigurations(requestModifiers, sessionId); return { content: [ { type: "text", text: JSON.stringify(report), }, ], structuredContent: { promptConfigurationsReport: report, }, }; } if (requestType === RequestType.GET_CUSTOM_LLM_REPORT) { const { customLLMReportRequest } = args; if (!customLLMReportRequest) { throw new Error("customLLMReportRequest is required"); } const { promptUuid, promptType, rangeStart, rangeEnd, interval } = customLLMReportRequest; const report = await getCustomLLMReport(promptUuid, promptType, new Date(rangeStart).getTime(), new Date(rangeEnd).getTime(), interval, requestModifiers, sessionId); // Log the report data to help debug logger.log("📊 Custom LLM Report Tool Response:", JSON.stringify({ promptType, hasError: report?.error, dataPointsCount: report?.timeSeriesDataPoints?.length, firstDataPoint: report?.timeSeriesDataPoints?.[0], })); return { content: [ { type: "text", text: JSON.stringify(report), }, ], structuredContent: { customLLMReport: report, }, }; } if (requestType === RequestType.GET_AUDIT_FEED) { const { auditFeedRequest } = args; if (!auditFeedRequest) { throw new Error("auditFeedRequest is required"); } const report = await getAuditFeed(new Date(auditFeedRequest.startTime).getTime(), new Date(auditFeedRequest.endTime).getTime(), requestModifiers, sessionId); return { content: [{ type: "text", text: JSON.stringify(report) }], structuredContent: { auditFeedReport: report }, }; } if (requestType === RequestType.GET_DIAGNOSTIC_FEED) { const { diagnosticFeedRequest } = args; if (!diagnosticFeedRequest) { throw new Error("diagnosticFeedRequest is required"); } const report = await getDiagnosticFeed(new Date(diagnosticFeedRequest.startTime).getTime(), new Date(diagnosticFeedRequest.endTime).getTime(), requestModifiers, sessionId); return { content: [{ type: "text", text: JSON.stringify(report) }], structuredContent: { diagnosticFeedReport: report }, }; } if (requestType === RequestType.GET_THRESHOLD_CROSSING_EVENTS) { const { thresholdCrossingEventsRequest } = args; if (!thresholdCrossingEventsRequest) { throw new Error("thresholdCrossingEventsRequest is required"); } const report = await getThresholdCrossingEvents(thresholdCrossingEventsRequest.deviceUuid, new Date(thresholdCrossingEventsRequest.startTime).getTime(), new Date(thresholdCrossingEventsRequest.endTime).getTime(), requestModifiers, sessionId); return { content: [{ type: "text", text: JSON.stringify(report) }], structuredContent: { thresholdCrossingEventsReport: report }, }; } if (requestType === RequestType.GET_CUSTOM_EVENTS_REPORT) { const { customEventsReportRequest } = args; if (!customEventsReportRequest) { throw new Error("customEventsReportRequest is required"); } const report = await getCustomEventsReport(customEventsReportRequest.promptUuid, new Date(customEventsReportRequest.startTime).getTime(), new Date(customEventsReportRequest.endTime).getTime(), customEventsReportRequest.interval, requestModifiers, sessionId); return { content: [{ type: "text", text: JSON.stringify(report) }], structuredContent: { customEventsReport: report }, }; } if (requestType === RequestType.GET_PEOPLE_COUNT_EVENTS) { const { peopleCountEventsRequest } = args; if (!peopleCountEventsRequest) { throw new Error("peopleCountEventsRequest is required"); } const report = await getPeopleCountEvents(peopleCountEventsRequest.deviceUuids, requestModifiers, sessionId); return { content: [{ type: "text", text: JSON.stringify(report) }], structuredContent: { peopleCountEventsReport: report }, }; } return { content: [ { type: "text", text: "", }, ], structuredContent: { error: true, errorMsg: "Error while fetching report information", }, }; }; export function createTool(server) { server.registerTool(TOOL_NAME, { title: "Reports", description: TOOL_DESCRIPTION, inputSchema: TOOL_ARGS.shape, outputSchema: OUTPUT_SCHEMA.shape, annotations: { readOnlyHint: true }, }, TOOL_HANDLER); }