chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
generateA2uiImpl,
|
||||
buildA2uiOperationsFromToolCall,
|
||||
RENDER_A2UI_TOOL_SCHEMA,
|
||||
CUSTOM_CATALOG_ID,
|
||||
} from "../generate-a2ui";
|
||||
|
||||
describe("generateA2uiImpl", () => {
|
||||
it("returns system prompt from context entries", () => {
|
||||
const result = generateA2uiImpl({
|
||||
messages: [],
|
||||
contextEntries: [
|
||||
{ value: "Component catalog info" },
|
||||
{ value: "More context" },
|
||||
],
|
||||
});
|
||||
expect(result.systemPrompt).toContain("Component catalog info");
|
||||
expect(result.systemPrompt).toContain("More context");
|
||||
});
|
||||
|
||||
it("filters empty/missing context values", () => {
|
||||
const result = generateA2uiImpl({
|
||||
messages: [],
|
||||
contextEntries: [{ value: "" }, { noValue: true }, { value: "keep" }],
|
||||
});
|
||||
expect(result.systemPrompt).toBe("keep");
|
||||
});
|
||||
|
||||
it("returns tool schema and choice", () => {
|
||||
const result = generateA2uiImpl({ messages: [] });
|
||||
expect(result.toolSchema.name).toBe("render_a2ui");
|
||||
expect(result.toolChoice).toBe("render_a2ui");
|
||||
});
|
||||
|
||||
it("passes messages through", () => {
|
||||
const msgs = [{ role: "user", content: "hello" }];
|
||||
const result = generateA2uiImpl({ messages: msgs });
|
||||
expect(result.messages).toBe(msgs);
|
||||
});
|
||||
|
||||
it("returns the default catalog ID", () => {
|
||||
const result = generateA2uiImpl({ messages: [] });
|
||||
expect(result.catalogId).toBe(CUSTOM_CATALOG_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildA2uiOperationsFromToolCall", () => {
|
||||
it("builds create_surface + update_components", () => {
|
||||
const result = buildA2uiOperationsFromToolCall({
|
||||
surfaceId: "s1",
|
||||
catalogId: "cat1",
|
||||
components: [{ id: "root", component: "Title" }],
|
||||
});
|
||||
expect(result.a2ui_operations).toHaveLength(2);
|
||||
expect(result.a2ui_operations[0].type).toBe("create_surface");
|
||||
expect(result.a2ui_operations[1].type).toBe("update_components");
|
||||
});
|
||||
|
||||
it("includes update_data_model when data provided", () => {
|
||||
const result = buildA2uiOperationsFromToolCall({
|
||||
surfaceId: "s1",
|
||||
catalogId: "cat1",
|
||||
components: [{ id: "root" }],
|
||||
data: { key: "value" },
|
||||
});
|
||||
expect(result.a2ui_operations).toHaveLength(3);
|
||||
expect(result.a2ui_operations[2].type).toBe("update_data_model");
|
||||
});
|
||||
|
||||
it("defaults surfaceId and catalogId", () => {
|
||||
const result = buildA2uiOperationsFromToolCall({ components: [] });
|
||||
expect(result.a2ui_operations[0].surfaceId).toBe("dynamic-surface");
|
||||
expect(result.a2ui_operations[0].catalogId).toBe(CUSTOM_CATALOG_ID);
|
||||
});
|
||||
|
||||
it("warns on empty components", () => {
|
||||
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
buildA2uiOperationsFromToolCall({
|
||||
surfaceId: "s1",
|
||||
catalogId: "c1",
|
||||
components: [],
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("empty components"),
|
||||
"s1",
|
||||
);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getWeatherImpl } from "../get-weather";
|
||||
|
||||
describe("getWeatherImpl", () => {
|
||||
it("returns all required fields", () => {
|
||||
const result = getWeatherImpl("Tokyo");
|
||||
expect(result).toHaveProperty("city");
|
||||
expect(result).toHaveProperty("temperature");
|
||||
expect(result).toHaveProperty("humidity");
|
||||
expect(result).toHaveProperty("wind_speed");
|
||||
expect(result).toHaveProperty("feels_like");
|
||||
expect(result).toHaveProperty("conditions");
|
||||
});
|
||||
|
||||
it("passes city name through", () => {
|
||||
expect(getWeatherImpl("San Francisco").city).toBe("San Francisco");
|
||||
});
|
||||
|
||||
it("is deterministic for the same city", () => {
|
||||
const r1 = getWeatherImpl("Tokyo");
|
||||
const r2 = getWeatherImpl("Tokyo");
|
||||
expect(r1.temperature).toBe(r2.temperature);
|
||||
expect(r1.conditions).toBe(r2.conditions);
|
||||
});
|
||||
|
||||
it("produces different results for different cities", () => {
|
||||
const r1 = getWeatherImpl("Tokyo");
|
||||
const r2 = getWeatherImpl("London");
|
||||
expect(r1).not.toEqual(r2);
|
||||
});
|
||||
|
||||
it("temperature is within expected range (20-95)", () => {
|
||||
const result = getWeatherImpl("Berlin");
|
||||
expect(result.temperature).toBeGreaterThanOrEqual(20);
|
||||
expect(result.temperature).toBeLessThanOrEqual(95);
|
||||
});
|
||||
|
||||
it("humidity is within expected range (30-90)", () => {
|
||||
const result = getWeatherImpl("Paris");
|
||||
expect(result.humidity).toBeGreaterThanOrEqual(30);
|
||||
expect(result.humidity).toBeLessThanOrEqual(90);
|
||||
});
|
||||
|
||||
it("wind_speed is within expected range (2-30)", () => {
|
||||
const result = getWeatherImpl("Sydney");
|
||||
expect(result.wind_speed).toBeGreaterThanOrEqual(2);
|
||||
expect(result.wind_speed).toBeLessThanOrEqual(30);
|
||||
});
|
||||
|
||||
it("conditions is one of the known values", () => {
|
||||
const known = [
|
||||
"Sunny",
|
||||
"Partly Cloudy",
|
||||
"Cloudy",
|
||||
"Overcast",
|
||||
"Light Rain",
|
||||
"Heavy Rain",
|
||||
"Thunderstorm",
|
||||
"Snow",
|
||||
"Foggy",
|
||||
"Windy",
|
||||
];
|
||||
const result = getWeatherImpl("Miami");
|
||||
expect(known).toContain(result.conditions);
|
||||
});
|
||||
|
||||
it("is case-insensitive for seed (lowercased internally)", () => {
|
||||
const r1 = getWeatherImpl("tokyo");
|
||||
const r2 = getWeatherImpl("TOKYO");
|
||||
expect(r1.temperature).toBe(r2.temperature);
|
||||
});
|
||||
|
||||
it("feels_like is within ±5 of temperature", () => {
|
||||
const result = getWeatherImpl("Berlin");
|
||||
expect(
|
||||
Math.abs(result.feels_like - result.temperature),
|
||||
).toBeLessThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { queryDataImpl } from "../query-data";
|
||||
|
||||
describe("queryDataImpl", () => {
|
||||
it("returns an array", () => {
|
||||
expect(Array.isArray(queryDataImpl("test"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns 66 rows (11 categories x 6 months)", () => {
|
||||
expect(queryDataImpl("test")).toHaveLength(66);
|
||||
});
|
||||
|
||||
it("rows have expected columns", () => {
|
||||
const row = queryDataImpl("test")[0];
|
||||
expect(row).toHaveProperty("date");
|
||||
expect(row).toHaveProperty("category");
|
||||
expect(row).toHaveProperty("subcategory");
|
||||
expect(row).toHaveProperty("amount");
|
||||
expect(row).toHaveProperty("type");
|
||||
expect(row).toHaveProperty("notes");
|
||||
});
|
||||
|
||||
it("returns same data regardless of query", () => {
|
||||
const r1 = queryDataImpl("revenue");
|
||||
const r2 = queryDataImpl("expenses");
|
||||
expect(r1).toEqual(r2);
|
||||
});
|
||||
|
||||
it("is deterministic (seeded RNG)", () => {
|
||||
const r1 = queryDataImpl("test");
|
||||
const r2 = queryDataImpl("test");
|
||||
expect(r1).toEqual(r2);
|
||||
});
|
||||
|
||||
it("categories are Revenue or Expenses", () => {
|
||||
const rows = queryDataImpl("test");
|
||||
const categories = new Set(rows.map((r) => r.category));
|
||||
expect(categories).toEqual(new Set(["Revenue", "Expenses"]));
|
||||
});
|
||||
|
||||
it("types are income or expense", () => {
|
||||
const rows = queryDataImpl("test");
|
||||
const types = new Set(rows.map((r) => r.type));
|
||||
expect(types).toEqual(new Set(["income", "expense"]));
|
||||
});
|
||||
|
||||
it("dates are in 2026", () => {
|
||||
const rows = queryDataImpl("test");
|
||||
for (const row of rows) {
|
||||
expect(row.date).toMatch(/^2026-/);
|
||||
}
|
||||
});
|
||||
|
||||
it("amount is a numeric string", () => {
|
||||
const rows = queryDataImpl("test");
|
||||
for (const row of rows) {
|
||||
expect(Number(row.amount)).not.toBeNaN();
|
||||
expect(Number(row.amount)).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
manageSalesTodosImpl,
|
||||
getSalesTodosImpl,
|
||||
INITIAL_SALES_TODOS,
|
||||
} from "../sales-todos";
|
||||
|
||||
describe("INITIAL_SALES_TODOS", () => {
|
||||
it("has 3 items", () => {
|
||||
expect(INITIAL_SALES_TODOS).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("has fixed IDs", () => {
|
||||
expect(INITIAL_SALES_TODOS[0].id).toBe("st-001");
|
||||
expect(INITIAL_SALES_TODOS[1].id).toBe("st-002");
|
||||
expect(INITIAL_SALES_TODOS[2].id).toBe("st-003");
|
||||
});
|
||||
});
|
||||
|
||||
describe("manageSalesTodosImpl", () => {
|
||||
it("assigns UUID to todos missing ID", () => {
|
||||
const result = manageSalesTodosImpl([{ title: "New deal" }]);
|
||||
expect(result[0].id).toBeTruthy();
|
||||
expect(result[0].id.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves existing IDs", () => {
|
||||
const result = manageSalesTodosImpl([{ id: "keep-me", title: "Deal" }]);
|
||||
expect(result[0].id).toBe("keep-me");
|
||||
});
|
||||
|
||||
it("provides defaults for missing fields", () => {
|
||||
const result = manageSalesTodosImpl([{ title: "Minimal" }]);
|
||||
expect(result[0].stage).toBe("prospect");
|
||||
expect(result[0].value).toBe(0);
|
||||
expect(result[0].completed).toBe(false);
|
||||
expect(result[0].dueDate).toBe("");
|
||||
expect(result[0].assignee).toBe("");
|
||||
});
|
||||
|
||||
it("preserves provided fields", () => {
|
||||
const result = manageSalesTodosImpl([
|
||||
{
|
||||
id: "x",
|
||||
title: "Big Deal",
|
||||
stage: "negotiation",
|
||||
value: 50000,
|
||||
dueDate: "2026-05-01",
|
||||
assignee: "Alice",
|
||||
completed: true,
|
||||
},
|
||||
]);
|
||||
expect(result[0].title).toBe("Big Deal");
|
||||
expect(result[0].stage).toBe("negotiation");
|
||||
expect(result[0].value).toBe(50000);
|
||||
expect(result[0].completed).toBe(true);
|
||||
});
|
||||
|
||||
it("handles empty array", () => {
|
||||
const result = manageSalesTodosImpl([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("handles multiple todos", () => {
|
||||
const result = manageSalesTodosImpl([
|
||||
{ title: "A" },
|
||||
{ title: "B" },
|
||||
{ title: "C" },
|
||||
]);
|
||||
expect(result).toHaveLength(3);
|
||||
// Each should get a unique ID
|
||||
const ids = new Set(result.map((r) => r.id));
|
||||
expect(ids.size).toBe(3);
|
||||
});
|
||||
|
||||
it("replaces empty string id with a generated UUID", () => {
|
||||
const result = manageSalesTodosImpl([{ id: "", title: "x" }]);
|
||||
expect(result[0].id).toBeTruthy();
|
||||
expect(result[0].id).not.toBe("");
|
||||
expect(result[0].id.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSalesTodosImpl", () => {
|
||||
it("returns initial todos when undefined", () => {
|
||||
const result = getSalesTodosImpl(undefined);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].id).toBe("st-001");
|
||||
});
|
||||
|
||||
it("returns initial todos when null", () => {
|
||||
const result = getSalesTodosImpl(null);
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("returns empty array when given empty array", () => {
|
||||
const result = getSalesTodosImpl([]);
|
||||
// empty array means user cleared all todos — return empty, not defaults
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns provided todos when non-empty", () => {
|
||||
const todos = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Test",
|
||||
stage: "prospect" as const,
|
||||
value: 100,
|
||||
dueDate: "",
|
||||
assignee: "",
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
const result = getSalesTodosImpl(todos);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].title).toBe("Test");
|
||||
});
|
||||
|
||||
it("returns a copy, not the original INITIAL_SALES_TODOS reference", () => {
|
||||
const result = getSalesTodosImpl(undefined);
|
||||
expect(result).not.toBe(INITIAL_SALES_TODOS);
|
||||
expect(result).toEqual(INITIAL_SALES_TODOS);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { scheduleMeetingImpl } from "../schedule-meeting";
|
||||
|
||||
describe("scheduleMeetingImpl", () => {
|
||||
it("returns pending_approval status", () => {
|
||||
expect(scheduleMeetingImpl("discuss roadmap").status).toBe(
|
||||
"pending_approval",
|
||||
);
|
||||
});
|
||||
it("includes the reason", () => {
|
||||
expect(scheduleMeetingImpl("quarterly review").reason).toBe(
|
||||
"quarterly review",
|
||||
);
|
||||
});
|
||||
it("uses default 30-minute duration", () => {
|
||||
expect(scheduleMeetingImpl("sync").duration_minutes).toBe(30);
|
||||
});
|
||||
it("accepts custom duration", () => {
|
||||
expect(scheduleMeetingImpl("deep dive", 60).duration_minutes).toBe(60);
|
||||
});
|
||||
it("includes a message", () => {
|
||||
const result = scheduleMeetingImpl("onboarding", 45);
|
||||
expect(result.message).toContain("onboarding");
|
||||
expect(result.message).toContain("45");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { searchFlightsImpl } from "../search-flights";
|
||||
import type { Flight } from "../types";
|
||||
|
||||
describe("searchFlightsImpl", () => {
|
||||
const mockFlight: Flight = {
|
||||
airline: "Test Air",
|
||||
airlineLogo: "https://example.com/logo.png",
|
||||
flightNumber: "TA100",
|
||||
origin: "SFO",
|
||||
destination: "JFK",
|
||||
date: "Tue, Apr 15",
|
||||
departureTime: "08:00",
|
||||
arrivalTime: "16:00",
|
||||
duration: "5h",
|
||||
status: "On Time",
|
||||
statusColor: "#22c55e",
|
||||
price: "$299",
|
||||
currency: "USD",
|
||||
};
|
||||
|
||||
it("returns flights and schema", () => {
|
||||
const result = searchFlightsImpl([mockFlight]);
|
||||
expect(result).toHaveProperty("flights");
|
||||
expect(result).toHaveProperty("schema");
|
||||
});
|
||||
|
||||
it("passes flights through unchanged", () => {
|
||||
const result = searchFlightsImpl([mockFlight]);
|
||||
expect(result.flights).toHaveLength(1);
|
||||
expect(result.flights[0]).toEqual(mockFlight);
|
||||
});
|
||||
|
||||
it("returns empty schema object", () => {
|
||||
const result = searchFlightsImpl([mockFlight]);
|
||||
expect(result.schema).toEqual({});
|
||||
});
|
||||
|
||||
it("handles empty flights array", () => {
|
||||
const result = searchFlightsImpl([]);
|
||||
expect(result.flights).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles multiple flights", () => {
|
||||
const result = searchFlightsImpl([
|
||||
mockFlight,
|
||||
{ ...mockFlight, flightNumber: "TA200" },
|
||||
]);
|
||||
expect(result.flights).toHaveLength(2);
|
||||
expect(result.flights[1].flightNumber).toBe("TA200");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Dynamic A2UI generation via secondary LLM call.
|
||||
* TypeScript equivalent of showcase/shared/python/tools/generate_a2ui.py.
|
||||
*/
|
||||
|
||||
export const CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog";
|
||||
|
||||
export const DESIGN_A2UI_SURFACE_TOOL_SCHEMA = {
|
||||
name: "_design_a2ui_surface",
|
||||
description: "Render a dynamic A2UI v0.9 surface.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
surfaceId: { type: "string", description: "Unique surface identifier." },
|
||||
catalogId: { type: "string", description: "The catalog ID." },
|
||||
components: {
|
||||
type: "array",
|
||||
items: { type: "object" },
|
||||
description: "A2UI v0.9 component array (flat format).",
|
||||
},
|
||||
data: {
|
||||
type: "object",
|
||||
description: "Optional initial data model for the surface.",
|
||||
},
|
||||
},
|
||||
required: ["surfaceId", "catalogId", "components"],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export interface GenerateA2UIInput {
|
||||
messages: Array<Record<string, unknown>>;
|
||||
contextEntries?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface GenerateA2UIResult {
|
||||
systemPrompt: string;
|
||||
toolSchema: typeof DESIGN_A2UI_SURFACE_TOOL_SCHEMA;
|
||||
toolChoice: string;
|
||||
messages: Array<Record<string, unknown>>;
|
||||
catalogId: string;
|
||||
}
|
||||
|
||||
export function generateA2uiImpl(input: GenerateA2UIInput): GenerateA2UIResult {
|
||||
const contextText = (input.contextEntries ?? [])
|
||||
.filter(
|
||||
(e): e is Record<string, unknown> & { value: string } =>
|
||||
typeof e === "object" &&
|
||||
typeof e.value === "string" &&
|
||||
e.value.length > 0,
|
||||
)
|
||||
.map((e) => e.value)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
systemPrompt: contextText,
|
||||
toolSchema: DESIGN_A2UI_SURFACE_TOOL_SCHEMA,
|
||||
toolChoice: "_design_a2ui_surface",
|
||||
messages: input.messages,
|
||||
catalogId: CUSTOM_CATALOG_ID,
|
||||
};
|
||||
}
|
||||
|
||||
export interface A2UIOperation {
|
||||
type: "create_surface" | "update_components" | "update_data_model";
|
||||
surfaceId: string;
|
||||
catalogId?: string;
|
||||
components?: Array<Record<string, unknown>>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function buildA2uiOperationsFromToolCall(
|
||||
args: Record<string, unknown>,
|
||||
): {
|
||||
a2ui_operations: A2UIOperation[];
|
||||
} {
|
||||
const surfaceId = (args.surfaceId as string) ?? "dynamic-surface";
|
||||
const catalogId = (args.catalogId as string) ?? CUSTOM_CATALOG_ID;
|
||||
const components = (args.components as Array<Record<string, unknown>>) ?? [];
|
||||
const data = args.data as Record<string, unknown> | undefined;
|
||||
|
||||
if (components.length === 0) {
|
||||
console.warn(
|
||||
"buildA2uiOperationsFromToolCall: empty components for surface",
|
||||
surfaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const ops: A2UIOperation[] = [
|
||||
{ type: "create_surface", surfaceId, catalogId },
|
||||
{ type: "update_components", surfaceId, components },
|
||||
];
|
||||
|
||||
if (data) {
|
||||
ops.push({ type: "update_data_model", surfaceId, data });
|
||||
}
|
||||
|
||||
return { a2ui_operations: ops };
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Mock weather data tool implementation.
|
||||
*
|
||||
* TypeScript equivalent of showcase/shared/python/tools/get_weather.py.
|
||||
* Uses a simple seed derived from the city name so repeated calls for
|
||||
* the same city return consistent results within a session.
|
||||
*/
|
||||
|
||||
import { WeatherResult } from "./types";
|
||||
|
||||
const CONDITIONS = [
|
||||
"Sunny",
|
||||
"Partly Cloudy",
|
||||
"Cloudy",
|
||||
"Overcast",
|
||||
"Light Rain",
|
||||
"Heavy Rain",
|
||||
"Thunderstorm",
|
||||
"Snow",
|
||||
"Foggy",
|
||||
"Windy",
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple seeded pseudo-random number generator (mulberry32).
|
||||
* Produces deterministic output for a given seed so the same city
|
||||
* always returns the same weather within a process lifetime.
|
||||
*/
|
||||
function seededRandom(seed: number): () => number {
|
||||
let t = seed;
|
||||
return () => {
|
||||
t = (t + 0x6d2b79f5) | 0;
|
||||
let v = Math.imul(t ^ (t >>> 15), 1 | t);
|
||||
v = (v + Math.imul(v ^ (v >>> 7), 61 | v)) ^ v;
|
||||
return ((v ^ (v >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function hashString(s: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function randInt(rng: () => number, min: number, max: number): number {
|
||||
return Math.floor(rng() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randChoice<T>(rng: () => number, arr: T[]): T {
|
||||
return arr[Math.floor(rng() * arr.length)];
|
||||
}
|
||||
|
||||
export function getWeatherImpl(city: string): WeatherResult {
|
||||
const rng = seededRandom(hashString(city.toLowerCase()));
|
||||
const temperature = randInt(rng, 20, 95);
|
||||
|
||||
return {
|
||||
city,
|
||||
temperature,
|
||||
humidity: randInt(rng, 30, 90),
|
||||
wind_speed: randInt(rng, 2, 30),
|
||||
feels_like: temperature + randInt(rng, -5, 5),
|
||||
conditions: randChoice(rng, CONDITIONS),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Shared TypeScript tool implementations for CopilotKit showcase.
|
||||
*
|
||||
* Pure functions with no framework imports — consumed by
|
||||
* langgraph-typescript, mastra, claude-sdk-typescript, and any
|
||||
* future TypeScript-based showcase packages.
|
||||
*/
|
||||
|
||||
export { getWeatherImpl } from "./get-weather";
|
||||
export { queryDataImpl } from "./query-data";
|
||||
export type { DataRow } from "./query-data";
|
||||
export {
|
||||
manageSalesTodosImpl,
|
||||
getSalesTodosImpl,
|
||||
INITIAL_SALES_TODOS,
|
||||
} from "./sales-todos";
|
||||
export { searchFlightsImpl } from "./search-flights";
|
||||
export { scheduleMeetingImpl } from "./schedule-meeting";
|
||||
export type { ScheduleMeetingResult } from "./schedule-meeting";
|
||||
export {
|
||||
generateA2uiImpl,
|
||||
buildA2uiOperationsFromToolCall,
|
||||
RENDER_A2UI_TOOL_SCHEMA,
|
||||
CUSTOM_CATALOG_ID,
|
||||
} from "./generate-a2ui";
|
||||
export type {
|
||||
GenerateA2UIInput,
|
||||
GenerateA2UIResult,
|
||||
A2UIOperation,
|
||||
} from "./generate-a2ui";
|
||||
export type { SalesTodo, SalesStage, Flight, WeatherResult } from "./types";
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Query data tool implementation — returns mock financial data.
|
||||
*
|
||||
* TypeScript equivalent of showcase/shared/python/tools/query_data.py.
|
||||
* In the future this could read a CSV, but for TS backend simplicity
|
||||
* we use generated mock data matching the Python fallback format.
|
||||
*/
|
||||
|
||||
export interface DataRow {
|
||||
date: string;
|
||||
category: string;
|
||||
subcategory: string;
|
||||
amount: string;
|
||||
type: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
// Seeded random for deterministic mock data
|
||||
function seededRandom(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
||||
return s / 0x7fffffff;
|
||||
};
|
||||
}
|
||||
|
||||
function generateMockData(): DataRow[] {
|
||||
const rand = seededRandom(42);
|
||||
const categories: Array<{
|
||||
category: string;
|
||||
subcategory: string;
|
||||
type: string;
|
||||
}> = [
|
||||
{
|
||||
category: "Revenue",
|
||||
subcategory: "Enterprise Subscriptions",
|
||||
type: "income",
|
||||
},
|
||||
{ category: "Revenue", subcategory: "Pro Tier Upgrades", type: "income" },
|
||||
{ category: "Revenue", subcategory: "API Usage Overages", type: "income" },
|
||||
{ category: "Revenue", subcategory: "Consulting Services", type: "income" },
|
||||
{ category: "Revenue", subcategory: "Marketplace Sales", type: "income" },
|
||||
{
|
||||
category: "Expenses",
|
||||
subcategory: "Engineering Salaries",
|
||||
type: "expense",
|
||||
},
|
||||
{ category: "Expenses", subcategory: "Product Team", type: "expense" },
|
||||
{
|
||||
category: "Expenses",
|
||||
subcategory: "AWS Infrastructure",
|
||||
type: "expense",
|
||||
},
|
||||
{ category: "Expenses", subcategory: "Marketing", type: "expense" },
|
||||
{ category: "Expenses", subcategory: "Customer Success", type: "expense" },
|
||||
{ category: "Expenses", subcategory: "AI Model Costs", type: "expense" },
|
||||
];
|
||||
|
||||
const notes: Record<string, string> = {
|
||||
"Enterprise Subscriptions": "3 new enterprise customers",
|
||||
"Pro Tier Upgrades": "31 upgrades + reduced churn",
|
||||
"API Usage Overages": "Heavy usage from top-10 accounts",
|
||||
"Consulting Services": "2 implementation projects",
|
||||
"Marketplace Sales": "Partner integrations revenue",
|
||||
"Engineering Salaries": "7 engineers + 2 contractors",
|
||||
"Product Team": "PM + designers + QA",
|
||||
"AWS Infrastructure": "Compute + storage + bandwidth",
|
||||
Marketing: "Paid ads + content + events",
|
||||
"Customer Success": "3 CSMs + tooling",
|
||||
"AI Model Costs": "OpenAI + Anthropic API spend",
|
||||
};
|
||||
|
||||
const rows: DataRow[] = [];
|
||||
const months = ["01", "02", "03", "04", "05", "06"];
|
||||
|
||||
for (const month of months) {
|
||||
for (const cat of categories) {
|
||||
const baseAmount =
|
||||
cat.type === "income"
|
||||
? 15000 + Math.floor(rand() * 35000)
|
||||
: 8000 + Math.floor(rand() * 40000);
|
||||
const day = String(1 + Math.floor(rand() * 28)).padStart(2, "0");
|
||||
rows.push({
|
||||
date: `2026-${month}-${day}`,
|
||||
category: cat.category,
|
||||
subcategory: cat.subcategory,
|
||||
amount: String(baseAmount),
|
||||
type: cat.type,
|
||||
notes: notes[cat.subcategory] ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
const MOCK_DATA: DataRow[] = generateMockData();
|
||||
|
||||
/**
|
||||
* Query the database. Takes natural language.
|
||||
*
|
||||
* Always call before showing a chart or graph. Returns the full
|
||||
* dataset as a list of row objects.
|
||||
*/
|
||||
export function queryDataImpl(_query: string): DataRow[] {
|
||||
return MOCK_DATA;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Sales todos tool implementation.
|
||||
*
|
||||
* TypeScript equivalent of showcase/shared/python/tools/sales_todos.py.
|
||||
*/
|
||||
|
||||
import { SalesTodo } from "./types";
|
||||
|
||||
export const INITIAL_SALES_TODOS: SalesTodo[] = [
|
||||
{
|
||||
id: "st-001",
|
||||
title: "Follow up with Acme Corp on enterprise proposal",
|
||||
stage: "proposal",
|
||||
value: 85000,
|
||||
dueDate: "2026-04-15",
|
||||
assignee: "Sarah Chen",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "st-002",
|
||||
title: "Qualify lead from TechFlow demo request",
|
||||
stage: "prospect",
|
||||
value: 42000,
|
||||
dueDate: "2026-04-18",
|
||||
assignee: "Mike Johnson",
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "st-003",
|
||||
title: "Send contract to DataViz Inc for final review",
|
||||
stage: "negotiation",
|
||||
value: 120000,
|
||||
dueDate: "2026-04-20",
|
||||
assignee: "Sarah Chen",
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Assign crypto.randomUUID() to any todos missing an ID, then return
|
||||
* the updated list.
|
||||
*/
|
||||
export function manageSalesTodosImpl(todos: Partial<SalesTodo>[]): SalesTodo[] {
|
||||
return todos.map((todo) => ({
|
||||
id: todo.id || crypto.randomUUID(),
|
||||
title: todo.title ?? "",
|
||||
stage: todo.stage ?? "prospect",
|
||||
value: todo.value ?? 0,
|
||||
dueDate: todo.dueDate ?? "",
|
||||
assignee: todo.assignee ?? "",
|
||||
completed: todo.completed ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current todos or initial defaults if none provided.
|
||||
*/
|
||||
export function getSalesTodosImpl(
|
||||
currentTodos?: Partial<SalesTodo>[] | null,
|
||||
): SalesTodo[] {
|
||||
if (currentTodos != null) {
|
||||
return currentTodos.length > 0 ? manageSalesTodosImpl(currentTodos) : [];
|
||||
}
|
||||
return [...INITIAL_SALES_TODOS];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Schedule meeting tool implementation — HITL gated.
|
||||
*
|
||||
* TypeScript equivalent of showcase/shared/python/tools/schedule_meeting.py.
|
||||
*/
|
||||
|
||||
export interface ScheduleMeetingResult {
|
||||
status: "pending_approval";
|
||||
reason: string;
|
||||
duration_minutes: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function scheduleMeetingImpl(
|
||||
reason: string,
|
||||
durationMinutes: number = 30,
|
||||
): ScheduleMeetingResult {
|
||||
return {
|
||||
status: "pending_approval",
|
||||
reason,
|
||||
duration_minutes: durationMinutes,
|
||||
message: `Meeting request: ${reason} (${durationMinutes} min). Awaiting user time selection.`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Search flights tool implementation.
|
||||
*
|
||||
* Simple passthrough that wraps flights for AG-UI rendering.
|
||||
* The frontend GenUI components handle presentation.
|
||||
*/
|
||||
|
||||
import { Flight } from "./types";
|
||||
|
||||
/**
|
||||
* Wrap the provided flights array for consumption by the frontend
|
||||
* flight-search GenUI component.
|
||||
*/
|
||||
export function searchFlightsImpl(flights: Flight[]): {
|
||||
flights: Flight[];
|
||||
schema: Record<string, unknown>;
|
||||
} {
|
||||
return { flights, schema: {} };
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Shared type definitions for showcase tools.
|
||||
*
|
||||
* These mirror the Python types in showcase/shared/python/tools/types.py
|
||||
* and the frontend types in showcase/shared/frontend/src/types.ts.
|
||||
*/
|
||||
|
||||
export type SalesStage =
|
||||
| "prospect"
|
||||
| "qualified"
|
||||
| "proposal"
|
||||
| "negotiation"
|
||||
| "closed-won"
|
||||
| "closed-lost";
|
||||
|
||||
export interface SalesTodo {
|
||||
id: string;
|
||||
title: string;
|
||||
stage: SalesStage;
|
||||
value: number;
|
||||
dueDate: string;
|
||||
assignee: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface Flight {
|
||||
airline: string;
|
||||
airlineLogo: string;
|
||||
flightNumber: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
date: string;
|
||||
departureTime: string;
|
||||
arrivalTime: string;
|
||||
duration: string;
|
||||
status: string;
|
||||
statusColor: string;
|
||||
price: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface WeatherResult {
|
||||
city: string;
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
wind_speed: number;
|
||||
feels_like: number;
|
||||
conditions: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
root: "showcase/shared/typescript",
|
||||
include: ["tools/__tests__/**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user