Initial commit
This commit is contained in:
409
templates/shared/streaming-utils.ts
Normal file
409
templates/shared/streaming-utils.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* Streaming Utilities for TheSys C1
|
||||
*
|
||||
* Helper functions for handling streaming responses from
|
||||
* OpenAI SDK, TheSys API, and transforming streams for C1.
|
||||
*
|
||||
* Works with any framework (Vite, Next.js, Cloudflare Workers).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a ReadableStream to a string
|
||||
*/
|
||||
export async function streamToString(stream: ReadableStream<string>): Promise<string> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let result = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// value might be string or Uint8Array
|
||||
if (typeof value === "string") {
|
||||
result += value;
|
||||
} else {
|
||||
result += decoder.decode(value, { stream: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Final decode with stream: false
|
||||
result += decoder.decode();
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ReadableStream to an array of chunks
|
||||
*/
|
||||
export async function streamToArray<T>(stream: ReadableStream<T>): Promise<T[]> {
|
||||
const reader = stream.getReader();
|
||||
const chunks: T[] = [];
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pass-through stream that allows reading while data flows
|
||||
*/
|
||||
export function createPassThroughStream<T>(): {
|
||||
readable: ReadableStream<T>;
|
||||
writable: WritableStream<T>;
|
||||
} {
|
||||
const { readable, writable } = new TransformStream<T, T>();
|
||||
return { readable, writable };
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a stream with a callback function
|
||||
* Similar to @crayonai/stream's transformStream
|
||||
*/
|
||||
export function transformStream<TInput, TOutput>(
|
||||
source: ReadableStream<TInput>,
|
||||
transformer: (chunk: TInput) => TOutput | null,
|
||||
options?: {
|
||||
onStart?: () => void;
|
||||
onEnd?: (data: { accumulated: TOutput[] }) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
): ReadableStream<TOutput> {
|
||||
const accumulated: TOutput[] = [];
|
||||
|
||||
return new ReadableStream<TOutput>({
|
||||
async start(controller) {
|
||||
options?.onStart?.();
|
||||
|
||||
const reader = source.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
options?.onEnd?.({ accumulated });
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
const transformed = transformer(value);
|
||||
|
||||
if (transformed !== null) {
|
||||
accumulated.push(transformed);
|
||||
controller.enqueue(transformed);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
options?.onError?.(err);
|
||||
controller.error(err);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple streams into one
|
||||
*/
|
||||
export function mergeStreams<T>(...streams: ReadableStream<T>[]): ReadableStream<T> {
|
||||
return new ReadableStream<T>({
|
||||
async start(controller) {
|
||||
try {
|
||||
await Promise.all(
|
||||
streams.map(async (stream) => {
|
||||
const reader = stream.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
controller.enqueue(value);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
})
|
||||
);
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a stream into multiple streams
|
||||
*/
|
||||
export function splitStream<T>(
|
||||
source: ReadableStream<T>,
|
||||
count: number
|
||||
): ReadableStream<T>[] {
|
||||
if (count < 2) throw new Error("Count must be at least 2");
|
||||
|
||||
const readers: ReadableStreamDefaultController<T>[] = [];
|
||||
const streams = Array.from({ length: count }, () => {
|
||||
return new ReadableStream<T>({
|
||||
start(controller) {
|
||||
readers.push(controller);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const reader = source.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
readers.forEach((r) => r.close());
|
||||
break;
|
||||
}
|
||||
|
||||
readers.forEach((r) => r.enqueue(value));
|
||||
}
|
||||
} catch (error) {
|
||||
readers.forEach((r) => r.error(error));
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
})();
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer chunks until a condition is met, then flush
|
||||
*/
|
||||
export function bufferStream<T>(
|
||||
source: ReadableStream<T>,
|
||||
shouldFlush: (buffer: T[]) => boolean
|
||||
): ReadableStream<T[]> {
|
||||
return new ReadableStream<T[]>({
|
||||
async start(controller) {
|
||||
const reader = source.getReader();
|
||||
let buffer: T[] = [];
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
if (buffer.length > 0) {
|
||||
controller.enqueue([...buffer]);
|
||||
}
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.push(value);
|
||||
|
||||
if (shouldFlush(buffer)) {
|
||||
controller.enqueue([...buffer]);
|
||||
buffer = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit a stream (delay between chunks)
|
||||
*/
|
||||
export function rateLimit<T>(
|
||||
source: ReadableStream<T>,
|
||||
delayMs: number
|
||||
): ReadableStream<T> {
|
||||
return new ReadableStream<T>({
|
||||
async start(controller) {
|
||||
const reader = source.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
controller.enqueue(value);
|
||||
|
||||
// Wait before next chunk
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a stream creation if it fails
|
||||
*/
|
||||
export async function retryStream<T>(
|
||||
createStream: () => Promise<ReadableStream<T>>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 1000
|
||||
): Promise<ReadableStream<T>> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await createStream();
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error(`Stream creation attempt ${attempt + 1} failed:`, lastError);
|
||||
|
||||
if (attempt < maxRetries - 1) {
|
||||
// Exponential backoff
|
||||
const waitTime = delayMs * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error("Failed to create stream");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Server-Sent Events (SSE) stream
|
||||
*/
|
||||
export function parseSSE(
|
||||
source: ReadableStream<Uint8Array>
|
||||
): ReadableStream<{ event?: string; data: string }> {
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = source.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
let event = "";
|
||||
let data = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("event:")) {
|
||||
event = line.slice(6).trim();
|
||||
} else if (line.startsWith("data:")) {
|
||||
data += line.slice(5).trim();
|
||||
} else if (line === "") {
|
||||
// Empty line signals end of message
|
||||
if (data) {
|
||||
controller.enqueue({ event: event || undefined, data });
|
||||
event = "";
|
||||
data = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backpressure in streams
|
||||
*/
|
||||
export function handleBackpressure<T>(
|
||||
source: ReadableStream<T>,
|
||||
highWaterMark: number = 10
|
||||
): ReadableStream<T> {
|
||||
return new ReadableStream<T>(
|
||||
{
|
||||
async start(controller) {
|
||||
const reader = source.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
controller.enqueue(value);
|
||||
|
||||
// Check if we need to apply backpressure
|
||||
if (controller.desiredSize !== null && controller.desiredSize <= 0) {
|
||||
// Wait a bit before continuing
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
},
|
||||
{ highWaterMark }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log stream chunks for debugging
|
||||
*/
|
||||
export function debugStream<T>(
|
||||
source: ReadableStream<T>,
|
||||
label: string = "Stream"
|
||||
): ReadableStream<T> {
|
||||
let count = 0;
|
||||
|
||||
return transformStream(
|
||||
source,
|
||||
(chunk) => {
|
||||
console.log(`[${label}] Chunk ${++count}:`, chunk);
|
||||
return chunk;
|
||||
},
|
||||
{
|
||||
onStart: () => console.log(`[${label}] Stream started`),
|
||||
onEnd: ({ accumulated }) =>
|
||||
console.log(`[${label}] Stream ended. Total chunks: ${accumulated.length}`),
|
||||
onError: (error) => console.error(`[${label}] Stream error:`, error),
|
||||
}
|
||||
);
|
||||
}
|
||||
318
templates/shared/theme-config.ts
Normal file
318
templates/shared/theme-config.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Reusable Theme Configurations for TheSys C1
|
||||
*
|
||||
* Collection of custom theme objects that can be used across
|
||||
* any framework (Vite, Next.js, Cloudflare Workers).
|
||||
*
|
||||
* Usage:
|
||||
* import { darkTheme, lightTheme, oceanTheme } from "./theme-config";
|
||||
*
|
||||
* <C1Chat theme={oceanTheme} />
|
||||
*/
|
||||
|
||||
export interface C1Theme {
|
||||
mode: "light" | "dark";
|
||||
colors: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
background: string;
|
||||
foreground: string;
|
||||
border: string;
|
||||
muted: string;
|
||||
accent: string;
|
||||
destructive?: string;
|
||||
success?: string;
|
||||
warning?: string;
|
||||
};
|
||||
fonts: {
|
||||
body: string;
|
||||
heading: string;
|
||||
mono?: string;
|
||||
};
|
||||
borderRadius: string;
|
||||
spacing: {
|
||||
base: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Light Themes
|
||||
// ============================================================================
|
||||
|
||||
export const lightTheme: C1Theme = {
|
||||
mode: "light",
|
||||
colors: {
|
||||
primary: "#3b82f6", // Blue
|
||||
secondary: "#8b5cf6", // Purple
|
||||
background: "#ffffff",
|
||||
foreground: "#1f2937",
|
||||
border: "#e5e7eb",
|
||||
muted: "#f3f4f6",
|
||||
accent: "#10b981", // Green
|
||||
destructive: "#ef4444", // Red
|
||||
success: "#10b981", // Green
|
||||
warning: "#f59e0b", // Amber
|
||||
},
|
||||
fonts: {
|
||||
body: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
heading: "'Inter', sans-serif",
|
||||
mono: "'Fira Code', 'Courier New', monospace",
|
||||
},
|
||||
borderRadius: "8px",
|
||||
spacing: {
|
||||
base: "16px",
|
||||
},
|
||||
};
|
||||
|
||||
export const oceanTheme: C1Theme = {
|
||||
mode: "light",
|
||||
colors: {
|
||||
primary: "#0ea5e9", // Sky blue
|
||||
secondary: "#06b6d4", // Cyan
|
||||
background: "#f0f9ff",
|
||||
foreground: "#0c4a6e",
|
||||
border: "#bae6fd",
|
||||
muted: "#e0f2fe",
|
||||
accent: "#0891b2",
|
||||
destructive: "#dc2626",
|
||||
success: "#059669",
|
||||
warning: "#d97706",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Nunito', sans-serif",
|
||||
heading: "'Nunito', sans-serif",
|
||||
mono: "'JetBrains Mono', monospace",
|
||||
},
|
||||
borderRadius: "12px",
|
||||
spacing: {
|
||||
base: "16px",
|
||||
},
|
||||
};
|
||||
|
||||
export const sunsetTheme: C1Theme = {
|
||||
mode: "light",
|
||||
colors: {
|
||||
primary: "#f59e0b", // Amber
|
||||
secondary: "#f97316", // Orange
|
||||
background: "#fffbeb",
|
||||
foreground: "#78350f",
|
||||
border: "#fed7aa",
|
||||
muted: "#fef3c7",
|
||||
accent: "#ea580c",
|
||||
destructive: "#dc2626",
|
||||
success: "#16a34a",
|
||||
warning: "#f59e0b",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Poppins', sans-serif",
|
||||
heading: "'Poppins', sans-serif",
|
||||
mono: "'Source Code Pro', monospace",
|
||||
},
|
||||
borderRadius: "6px",
|
||||
spacing: {
|
||||
base: "16px",
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Dark Themes
|
||||
// ============================================================================
|
||||
|
||||
export const darkTheme: C1Theme = {
|
||||
mode: "dark",
|
||||
colors: {
|
||||
primary: "#60a5fa", // Light blue
|
||||
secondary: "#a78bfa", // Light purple
|
||||
background: "#111827",
|
||||
foreground: "#f9fafb",
|
||||
border: "#374151",
|
||||
muted: "#1f2937",
|
||||
accent: "#34d399",
|
||||
destructive: "#f87171",
|
||||
success: "#34d399",
|
||||
warning: "#fbbf24",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
heading: "'Inter', sans-serif",
|
||||
mono: "'Fira Code', 'Courier New', monospace",
|
||||
},
|
||||
borderRadius: "8px",
|
||||
spacing: {
|
||||
base: "16px",
|
||||
},
|
||||
};
|
||||
|
||||
export const midnightTheme: C1Theme = {
|
||||
mode: "dark",
|
||||
colors: {
|
||||
primary: "#818cf8", // Indigo
|
||||
secondary: "#c084fc", // Purple
|
||||
background: "#0f172a",
|
||||
foreground: "#e2e8f0",
|
||||
border: "#334155",
|
||||
muted: "#1e293b",
|
||||
accent: "#8b5cf6",
|
||||
destructive: "#f87171",
|
||||
success: "#4ade80",
|
||||
warning: "#facc15",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Roboto', sans-serif",
|
||||
heading: "'Roboto', sans-serif",
|
||||
mono: "'IBM Plex Mono', monospace",
|
||||
},
|
||||
borderRadius: "10px",
|
||||
spacing: {
|
||||
base: "16px",
|
||||
},
|
||||
};
|
||||
|
||||
export const forestTheme: C1Theme = {
|
||||
mode: "dark",
|
||||
colors: {
|
||||
primary: "#4ade80", // Green
|
||||
secondary: "#22d3ee", // Cyan
|
||||
background: "#064e3b",
|
||||
foreground: "#d1fae5",
|
||||
border: "#065f46",
|
||||
muted: "#047857",
|
||||
accent: "#10b981",
|
||||
destructive: "#fca5a5",
|
||||
success: "#6ee7b7",
|
||||
warning: "#fde047",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Lato', sans-serif",
|
||||
heading: "'Lato', sans-serif",
|
||||
mono: "'Consolas', monospace",
|
||||
},
|
||||
borderRadius: "8px",
|
||||
spacing: {
|
||||
base: "18px",
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// High Contrast Themes (Accessibility)
|
||||
// ============================================================================
|
||||
|
||||
export const highContrastLight: C1Theme = {
|
||||
mode: "light",
|
||||
colors: {
|
||||
primary: "#0000ff", // Pure blue
|
||||
secondary: "#ff00ff", // Pure magenta
|
||||
background: "#ffffff",
|
||||
foreground: "#000000",
|
||||
border: "#000000",
|
||||
muted: "#f5f5f5",
|
||||
accent: "#008000", // Pure green
|
||||
destructive: "#ff0000",
|
||||
success: "#008000",
|
||||
warning: "#ff8800",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Arial', sans-serif",
|
||||
heading: "'Arial', bold, sans-serif",
|
||||
mono: "'Courier New', monospace",
|
||||
},
|
||||
borderRadius: "2px",
|
||||
spacing: {
|
||||
base: "20px",
|
||||
},
|
||||
};
|
||||
|
||||
export const highContrastDark: C1Theme = {
|
||||
mode: "dark",
|
||||
colors: {
|
||||
primary: "#00ccff", // Bright cyan
|
||||
secondary: "#ff00ff", // Bright magenta
|
||||
background: "#000000",
|
||||
foreground: "#ffffff",
|
||||
border: "#ffffff",
|
||||
muted: "#1a1a1a",
|
||||
accent: "#00ff00", // Bright green
|
||||
destructive: "#ff0000",
|
||||
success: "#00ff00",
|
||||
warning: "#ffaa00",
|
||||
},
|
||||
fonts: {
|
||||
body: "'Arial', sans-serif",
|
||||
heading: "'Arial', bold, sans-serif",
|
||||
mono: "'Courier New', monospace",
|
||||
},
|
||||
borderRadius: "2px",
|
||||
spacing: {
|
||||
base: "20px",
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Theme Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get system theme preference
|
||||
*/
|
||||
export function getSystemTheme(): "light" | "dark" {
|
||||
if (typeof window === "undefined") return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to system theme changes
|
||||
*/
|
||||
export function onSystemThemeChange(callback: (theme: "light" | "dark") => void) {
|
||||
if (typeof window === "undefined") return () => {};
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
callback(e.matches ? "dark" : "light");
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handler);
|
||||
|
||||
return () => mediaQuery.removeEventListener("change", handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme based on user preference
|
||||
*/
|
||||
export function getTheme(
|
||||
preference: "light" | "dark" | "system",
|
||||
lightThemeConfig: C1Theme = lightTheme,
|
||||
darkThemeConfig: C1Theme = darkTheme
|
||||
): C1Theme {
|
||||
if (preference === "system") {
|
||||
const systemPref = getSystemTheme();
|
||||
return systemPref === "dark" ? darkThemeConfig : lightThemeConfig;
|
||||
}
|
||||
|
||||
return preference === "dark" ? darkThemeConfig : lightThemeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* All available themes by name
|
||||
*/
|
||||
export const themes = {
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
ocean: oceanTheme,
|
||||
sunset: sunsetTheme,
|
||||
midnight: midnightTheme,
|
||||
forest: forestTheme,
|
||||
"high-contrast-light": highContrastLight,
|
||||
"high-contrast-dark": highContrastDark,
|
||||
} as const;
|
||||
|
||||
export type ThemeName = keyof typeof themes;
|
||||
|
||||
/**
|
||||
* Get theme by name
|
||||
*/
|
||||
export function getThemeByName(name: ThemeName): C1Theme {
|
||||
return themes[name];
|
||||
}
|
||||
327
templates/shared/tool-schemas.ts
Normal file
327
templates/shared/tool-schemas.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Common Zod Schemas for Tool Calling
|
||||
*
|
||||
* Reusable schemas for common tools across any framework.
|
||||
* These schemas provide runtime validation and type safety.
|
||||
*
|
||||
* Usage:
|
||||
* import { webSearchTool, createOrderTool } from "./tool-schemas";
|
||||
* import zodToJsonSchema from "zod-to-json-schema";
|
||||
*
|
||||
* const tools = [webSearchTool, createOrderTool];
|
||||
*
|
||||
* await client.beta.chat.completions.runTools({
|
||||
* model: "c1/openai/gpt-5/v-20250930",
|
||||
* messages: [...],
|
||||
* tools,
|
||||
* });
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
|
||||
// ============================================================================
|
||||
// Web Search Tool
|
||||
// ============================================================================
|
||||
|
||||
export const webSearchSchema = z.object({
|
||||
query: z.string().min(1).describe("The search query"),
|
||||
max_results: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(10)
|
||||
.default(5)
|
||||
.describe("Maximum number of results to return (1-10)"),
|
||||
include_answer: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe("Include AI-generated answer summary"),
|
||||
});
|
||||
|
||||
export type WebSearchArgs = z.infer<typeof webSearchSchema>;
|
||||
|
||||
export const webSearchTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "web_search",
|
||||
description:
|
||||
"Search the web for current information using a search API. Use this for recent events, news, or information that may have changed recently.",
|
||||
parameters: zodToJsonSchema(webSearchSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Product/Inventory Tools
|
||||
// ============================================================================
|
||||
|
||||
export const productLookupSchema = z.object({
|
||||
product_type: z
|
||||
.enum(["gloves", "hat", "scarf", "all"])
|
||||
.optional()
|
||||
.describe("Type of product to lookup, or 'all' for entire inventory"),
|
||||
filter: z
|
||||
.object({
|
||||
min_price: z.number().optional(),
|
||||
max_price: z.number().optional(),
|
||||
in_stock_only: z.boolean().default(true),
|
||||
})
|
||||
.optional()
|
||||
.describe("Optional filters for product search"),
|
||||
});
|
||||
|
||||
export type ProductLookupArgs = z.infer<typeof productLookupSchema>;
|
||||
|
||||
export const productLookupTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "lookup_product",
|
||||
description:
|
||||
"Look up products in the inventory database. Returns product details including price, availability, and specifications.",
|
||||
parameters: zodToJsonSchema(productLookupSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Order Creation Tool
|
||||
// ============================================================================
|
||||
|
||||
const orderItemSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("gloves"),
|
||||
size: z.enum(["XS", "S", "M", "L", "XL", "XXL"]),
|
||||
color: z.string().min(1),
|
||||
quantity: z.number().int().min(1).max(100),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("hat"),
|
||||
style: z.enum(["beanie", "baseball", "fedora", "bucket"]),
|
||||
color: z.string().min(1),
|
||||
quantity: z.number().int().min(1).max(100),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("scarf"),
|
||||
length: z.enum(["short", "medium", "long"]),
|
||||
material: z.enum(["wool", "cotton", "silk", "cashmere"]),
|
||||
quantity: z.number().int().min(1).max(100),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const createOrderSchema = z.object({
|
||||
customer_email: z
|
||||
.string()
|
||||
.email()
|
||||
.describe("Customer's email address for order confirmation"),
|
||||
items: z
|
||||
.array(orderItemSchema)
|
||||
.min(1)
|
||||
.max(20)
|
||||
.describe("Array of items to include in the order (max 20)"),
|
||||
shipping_address: z.object({
|
||||
street: z.string().min(1),
|
||||
city: z.string().min(1),
|
||||
state: z.string().length(2), // US state code
|
||||
zip: z.string().regex(/^\d{5}(-\d{4})?$/), // ZIP or ZIP+4
|
||||
country: z.string().default("US"),
|
||||
}),
|
||||
notes: z.string().optional().describe("Optional order notes or instructions"),
|
||||
});
|
||||
|
||||
export type CreateOrderArgs = z.infer<typeof createOrderSchema>;
|
||||
export type OrderItem = z.infer<typeof orderItemSchema>;
|
||||
|
||||
export const createOrderTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "create_order",
|
||||
description:
|
||||
"Create a new product order with customer information, items, and shipping address. Returns order ID and confirmation details.",
|
||||
parameters: zodToJsonSchema(createOrderSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Database Query Tool
|
||||
// ============================================================================
|
||||
|
||||
export const databaseQuerySchema = z.object({
|
||||
query_type: z
|
||||
.enum(["select", "aggregate", "search"])
|
||||
.describe("Type of database query to perform"),
|
||||
table: z
|
||||
.string()
|
||||
.describe("Database table name (e.g., 'users', 'products', 'orders')"),
|
||||
filters: z
|
||||
.record(z.any())
|
||||
.optional()
|
||||
.describe("Filter conditions as key-value pairs"),
|
||||
limit: z.number().int().min(1).max(100).default(20).describe("Result limit"),
|
||||
});
|
||||
|
||||
export type DatabaseQueryArgs = z.infer<typeof databaseQuerySchema>;
|
||||
|
||||
export const databaseQueryTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "query_database",
|
||||
description:
|
||||
"Query the database for information. Supports select, aggregate, and search operations on various tables.",
|
||||
parameters: zodToJsonSchema(databaseQuerySchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Data Visualization Tool
|
||||
// ============================================================================
|
||||
|
||||
export const createVisualizationSchema = z.object({
|
||||
chart_type: z
|
||||
.enum(["bar", "line", "pie", "scatter", "area"])
|
||||
.describe("Type of chart to create"),
|
||||
data: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
value: z.number(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.describe("Data points for the visualization"),
|
||||
title: z.string().min(1).describe("Chart title"),
|
||||
x_label: z.string().optional().describe("X-axis label"),
|
||||
y_label: z.string().optional().describe("Y-axis label"),
|
||||
});
|
||||
|
||||
export type CreateVisualizationArgs = z.infer<typeof createVisualizationSchema>;
|
||||
|
||||
export const createVisualizationTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "create_visualization",
|
||||
description:
|
||||
"Create a data visualization chart. Returns chart configuration that will be rendered in the UI.",
|
||||
parameters: zodToJsonSchema(createVisualizationSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Email Tool
|
||||
// ============================================================================
|
||||
|
||||
export const sendEmailSchema = z.object({
|
||||
to: z.string().email().describe("Recipient email address"),
|
||||
subject: z.string().min(1).max(200).describe("Email subject line"),
|
||||
body: z.string().min(1).describe("Email body content (supports HTML)"),
|
||||
cc: z.array(z.string().email()).optional().describe("CC recipients"),
|
||||
bcc: z.array(z.string().email()).optional().describe("BCC recipients"),
|
||||
});
|
||||
|
||||
export type SendEmailArgs = z.infer<typeof sendEmailSchema>;
|
||||
|
||||
export const sendEmailTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "send_email",
|
||||
description:
|
||||
"Send an email to one or more recipients. Use this to send notifications, confirmations, or responses to customers.",
|
||||
parameters: zodToJsonSchema(sendEmailSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Calendar/Scheduling Tool
|
||||
// ============================================================================
|
||||
|
||||
export const scheduleEventSchema = z.object({
|
||||
title: z.string().min(1).describe("Event title"),
|
||||
start_time: z.string().datetime().describe("Event start time (ISO 8601)"),
|
||||
end_time: z.string().datetime().describe("Event end time (ISO 8601)"),
|
||||
description: z.string().optional().describe("Event description"),
|
||||
attendees: z
|
||||
.array(z.string().email())
|
||||
.optional()
|
||||
.describe("List of attendee email addresses"),
|
||||
location: z.string().optional().describe("Event location or meeting link"),
|
||||
reminder_minutes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(15)
|
||||
.describe("Minutes before event to send reminder"),
|
||||
});
|
||||
|
||||
export type ScheduleEventArgs = z.infer<typeof scheduleEventSchema>;
|
||||
|
||||
export const scheduleEventTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "schedule_event",
|
||||
description:
|
||||
"Schedule a calendar event with attendees, location, and reminders.",
|
||||
parameters: zodToJsonSchema(scheduleEventSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// File Upload Tool
|
||||
// ============================================================================
|
||||
|
||||
export const uploadFileSchema = z.object({
|
||||
file_name: z.string().min(1).describe("Name of the file"),
|
||||
file_type: z
|
||||
.string()
|
||||
.describe("MIME type (e.g., 'image/png', 'application/pdf')"),
|
||||
file_size: z.number().int().min(1).describe("File size in bytes"),
|
||||
description: z.string().optional().describe("File description or metadata"),
|
||||
});
|
||||
|
||||
export type UploadFileArgs = z.infer<typeof uploadFileSchema>;
|
||||
|
||||
export const uploadFileTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "upload_file",
|
||||
description:
|
||||
"Upload a file to cloud storage. Returns storage URL and file metadata.",
|
||||
parameters: zodToJsonSchema(uploadFileSchema),
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Export All Tools
|
||||
// ============================================================================
|
||||
|
||||
export const allTools = [
|
||||
webSearchTool,
|
||||
productLookupTool,
|
||||
createOrderTool,
|
||||
databaseQueryTool,
|
||||
createVisualizationTool,
|
||||
sendEmailTool,
|
||||
scheduleEventTool,
|
||||
uploadFileTool,
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper to get tools by category
|
||||
*/
|
||||
export function getToolsByCategory(category: "ecommerce" | "data" | "communication" | "all") {
|
||||
const categories = {
|
||||
ecommerce: [productLookupTool, createOrderTool],
|
||||
data: [databaseQueryTool, createVisualizationTool],
|
||||
communication: [sendEmailTool, scheduleEventTool],
|
||||
all: allTools,
|
||||
};
|
||||
|
||||
return categories[category];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation helper
|
||||
*/
|
||||
export function validateToolArgs<T extends z.ZodType>(
|
||||
schema: T,
|
||||
args: unknown
|
||||
): z.infer<T> {
|
||||
return schema.parse(args);
|
||||
}
|
||||
Reference in New Issue
Block a user