Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions apps/apollo-vertex/app/api/insights/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type NextRequest, NextResponse } from "next/server";

const FORWARDED_HEADERS = ["authorization", "x-uipath-internal-tenantid"];

function getAuthHeader(request: NextRequest) {
const authHeader = request.headers.get("authorization");
if (!authHeader) {
return null;
}
return authHeader;
}

function buildTargetUrl(path: string[], search: string) {
return `https://alpha.uipath.com/${path.join("/")}${search}`;
}

function forwardHeaders(request: NextRequest, extra?: HeadersInit): Headers {
const headers = new Headers(extra);
for (const name of FORWARDED_HEADERS) {
const value = request.headers.get(name);
if (value) {
headers.set(name, value);
}
}
return headers;
}

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const authHeader = getAuthHeader(request);
if (!authHeader) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { path } = await params;
const targetUrl = buildTargetUrl(path, request.nextUrl.search);

const response = await fetch(targetUrl, {
method: "GET",
headers: forwardHeaders(request),
signal: request.signal,
});

const text = await response.text();
return new NextResponse(text, {
status: response.status,
headers: {
"Content-Type":
response.headers.get("content-type") ?? "application/json",
},
});
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const authHeader = getAuthHeader(request);
if (!authHeader) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { path } = await params;
const targetUrl = buildTargetUrl(path, request.nextUrl.search);
const body = await request.text();

const response = await fetch(targetUrl, {
method: "POST",
headers: forwardHeaders(request, {
"Content-Type": request.headers.get("content-type") ?? "application/json",
}),
body,
signal: request.signal,
});

const text = await response.text();
return new NextResponse(text, {
status: response.status,
headers: {
"Content-Type":
response.headers.get("content-type") ?? "application/json",
},
});
}
1 change: 1 addition & 0 deletions apps/apollo-vertex/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"how_can_i_help_you": "How can I help you?",
"import": "Import",
"info": "Info",
"insights_table_no_valid_fields": "None of the requested field IDs exist in the \"{{source}}\" Insights schema: {{fields}}.",
"japanese": "Japanese",
"korean": "Korean",
"language": "Language",
Expand Down
20 changes: 20 additions & 0 deletions apps/apollo-vertex/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,26 @@
"path": "registry/ai-chat/tools/data-fabric-kpi/index.ts",
"type": "registry:lib",
"target": "components/ui/ai-chat/tools/data-fabric-kpi/index.ts"
},
{
"path": "registry/ai-chat/tools/insights/shared.ts",
"type": "registry:lib",
"target": "components/ui/ai-chat/tools/insights/shared.ts"
},
{
"path": "registry/ai-chat/tools/insights/index.ts",
"type": "registry:lib",
"target": "components/ui/ai-chat/tools/insights/index.ts"
},
{
"path": "registry/ai-chat/tools/insights-table/insights-table-tool.tsx",
"type": "registry:ui",
"target": "components/ui/ai-chat/tools/insights-table/insights-table-tool.tsx"
},
{
"path": "registry/ai-chat/tools/insights-table/index.ts",
"type": "registry:lib",
"target": "components/ui/ai-chat/tools/insights-table/index.ts"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type ToolResolutionFailure =
| { reason: "unknown_entity"; entity: string }
| { reason: "table_no_valid_fields"; entity: string; fields: string }
| { reason: "multi_line_too_few_metrics" }
| { reason: "insights_table_no_valid_fields"; source: string; fields: string }
| ResolverFailure;

export type ResolverResult<T> =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
createInsightsTableTool,
insightsTableClient,
} from "./insights-table-tool";
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { toolDefinition } from "@tanstack/ai";
import { standaloneAdapter } from "@uipath/apollo-dashboarding";
import { DateTime } from "luxon";
import { z } from "zod";
import { TableChartCard } from "../../charts/table-chart-card";
import { ToolResolutionError } from "../../charts/tool-resolution-error";
import { filterSchema, resolveFilters } from "../data-fabric/shared";
import {
buildInsightsTableDataModel,
collectInsightsFieldIds,
collectInsightsTableIds,
generateInsightsSchemaDocs,
type InsightsToolContext,
} from "../insights/shared";

const insightsTableInput = z.object({
dimensions: z
.array(z.string())
.min(1)
.describe(
"Qualified field IDs (Table.Field) to display as table columns. Use IDs exactly as listed in the Schema Reference.",
),
filters: z
.array(filterSchema)
.optional()
.describe("Optional filters to narrow down results."),
});

const insightsTableDef = toolDefinition({
name: "insights_table",
description:
"List records from the Insights tables (PROCESSRUNS, ELEMENTRUNS, INCIDENTS) for the vertical-solution source. Use this tool whenever the user's request mentions process runs, element runs, incidents, or the table names PROCESSRUNS / ELEMENTRUNS / INCIDENTS — regardless of how the user phrases the verb. 'List', 'show', 'how many', 'distribution of', 'over time', 'trend of', 'by status' all map to THIS tool when the underlying data is Insights. The data source decides, not the verb. Field names must be qualified Table.Field IDs copied EXACTLY from the schema (e.g. PROCESSRUNS.STATUS, ELEMENTRUNS.ELEMENTNAME). Supports optional filters (list, search, range including datetime).",
inputSchema: insightsTableInput,
outputSchema: insightsTableInput,
metadata: { skipFollowUp: true },
});

export const insightsTableClient = insightsTableDef.client((input) => input);

type InsightsTableInput = z.infer<typeof insightsTableInput>;

export function createInsightsTableTool(context: InsightsToolContext) {
const today = DateTime.now().toISODate();
const fieldIds = collectInsightsFieldIds(context.schema);
const tableIds = collectInsightsTableIds(context.schema);
const dataModel = buildInsightsTableDataModel(
context.sourceType,
context.schema,
);

const toolPrompt = `You have an "insights_table" tool. It is the ONLY tool that operates on the Insights "${context.sourceType}" source.

PRECONDITION — call this tool whenever the user's request references identifiers from the Schema Reference below (the tables ${tableIds.join(", ")}, any Table.Field under them, or any natural-language phrasing of those table names). Use it regardless of the action verb — "list", "show", "how many", "distribution of", "over time", "trend of", "by status" all map to this tool when the data is Insights. Other registered chart tools operate on a different source and must NOT be called for these tables.

## Schema Reference (source: ${context.sourceType})
${generateInsightsSchemaDocs(context.schema)}

## How to call
- Pass field IDs the user wants as columns in "dimensions". Field IDs are qualified "Table.Field" — use them EXACTLY as listed in the Schema Reference (case-sensitive, including the table prefix). Unqualified or paraphrased names are rejected.
- If the user does not specify fields, pick a reasonable default set (3-8 fields) from the Schema Reference.

## Filters
You can optionally pass filters to narrow results. Filter "field" must also be a qualified "Table.Field" ID from the Schema Reference. Available filter types:
- **list**: match specific values. Use valueType matching the field type (string/number/boolean). Set invert=true to exclude.
- **search**: text pattern matching on string fields. searchFilterType: "default" (contains), "startsWith", "endsWith".
- **range** (numeric): use valueType="number" with min/max numbers.
- **range** (datetime): use valueType="datetime" with min/max as ISO 8601 strings (e.g. "2026-01-01" or "2026-01-01T00:00:00Z"). Today is ${today} — use it to resolve relative phrases like "last 30 days" or "this year" into absolute ISO dates before passing them.
Only add filters when the user asks to filter, search, or narrow results.`;

function renderTable(output: InsightsTableInput, id: string) {
const { dimensions, filters } = output;

const validFieldIds = new Set(fieldIds);
const validDimensions = dimensions.filter((d) => validFieldIds.has(d));

if (validDimensions.length === 0) {
return (
<ToolResolutionError
failure={{
reason: "insights_table_no_valid_fields",
source: context.sourceType,
fields: dimensions.join(", "),
}}
/>
);
}

const normalizedFilters = resolveFilters(filters, {
mode: "single",
validFields: fieldIds,
});

const configuration = {
id,
name: context.sourceType,
type: "table" as const,
dimensions: validDimensions,
filters: normalizedFilters,
};

const adapter = standaloneAdapter({
baseUrl: context.insightsBaseUrl,
accessToken: context.accessToken,
sourceType: context.sourceType,
});

return (
<TableChartCard
configuration={configuration}
dataModel={dataModel}
dataAdapter={adapter}
/>
);
}

return { toolPrompt, renderTable };
}
12 changes: 12 additions & 0 deletions apps/apollo-vertex/registry/ai-chat/tools/insights/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { createInsightsTableTool } from "../insights-table";
export {
buildInsightsTableDataModel,
collectInsightsFieldIds,
generateInsightsSchemaDocs,
mapInsightsFieldKind,
type InsightsField,
type InsightsFieldType,
type InsightsSchema,
type InsightsTable,
type InsightsToolContext,
} from "./shared";
72 changes: 72 additions & 0 deletions apps/apollo-vertex/registry/ai-chat/tools/insights/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {
ColumnDataType,
InfoResponse,
TableDataModel,
} from "@uipath/apollo-dashboarding";

export type InsightsSchema = InfoResponse;
export type InsightsTable = InfoResponse["data"][number];
export type InsightsField = InsightsTable["fields"][number];
export type InsightsFieldType = "numeric" | "string" | "boolean" | "datetime";

export interface InsightsToolContext {
sourceType: string;
accessToken: string;
insightsBaseUrl: string;
schema: InsightsSchema;
}

export function mapInsightsFieldKind(
columnDataType: ColumnDataType,
): InsightsFieldType {
switch (columnDataType) {
case "NUMBER":
case "FLOAT":
return "numeric";
case "TIMESTAMP_NTZ":
case "DATE":
return "datetime";
case "BOOLEAN":
return "boolean";
case "TEXT":
case "LEGACYINGESTION":
case "UNKNOWN":
return "string";
}
}

export function buildInsightsTableDataModel(
sourceType: string,
schema: InsightsSchema,
): TableDataModel {
const fields = schema.data.flatMap((table) =>
table.fields.map((field) => ({
id: field.id,
display: field.column,
type: mapInsightsFieldKind(field.columnDataType),
})),
);
return { id: sourceType, fields };
}

export function collectInsightsFieldIds(schema: InsightsSchema): string[] {
return schema.data.flatMap((table) => table.fields.map((field) => field.id));
}

export function collectInsightsTableIds(schema: InsightsSchema): string[] {
return schema.data.map((table) => table.id);
}

export function generateInsightsSchemaDocs(schema: InsightsSchema): string {
return schema.data
.map((table) => {
const fields = table.fields
.map(
(field) =>
`${field.id} (${mapInsightsFieldKind(field.columnDataType)})`,
)
.join(", ");
return `${table.id}: ${fields}`;
})
.join("\n\n");
}
Loading
Loading