mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(playground): add useToolsBuilder/useStructuredOutput hooks
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// src/app/(dashboard)/dashboard/playground/hooks/useStructuredOutput.ts
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { StructuredOutputSchema } from "@/shared/schemas/playground";
|
||||
|
||||
export interface StructuredOutputSchemaInput {
|
||||
name: string;
|
||||
schema: Record<string, unknown>;
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
export interface UseStructuredOutputState {
|
||||
enabled: boolean;
|
||||
schema: StructuredOutputSchemaInput | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ValidateResponseResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UseStructuredOutput extends UseStructuredOutputState {
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
/**
|
||||
* Set and validate the JSON schema via Zod StructuredOutputSchema.
|
||||
* Clears error on success; sets error on validation failure.
|
||||
*/
|
||||
setSchema: (s: StructuredOutputSchemaInput) => void;
|
||||
/**
|
||||
* Validate a response body against the current schema.
|
||||
* - First tries JSON.parse if content is a string.
|
||||
* - Then checks that the parsed value is a non-null object (basic shape validation).
|
||||
* Returns { valid: true } on success; { valid: false, error } on failure.
|
||||
*/
|
||||
validateResponse: (content: unknown) => ValidateResponseResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for the Structured Output (JSON mode) toggle in the Build tab.
|
||||
* Manages toggle state + JSON schema + client-side response validation.
|
||||
*/
|
||||
export function useStructuredOutput(): UseStructuredOutput {
|
||||
const [enabled, setEnabledState] = useState(false);
|
||||
const [schema, setSchemaState] = useState<StructuredOutputSchemaInput | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const setEnabled = useCallback((value: boolean): void => {
|
||||
setEnabledState(value);
|
||||
}, []);
|
||||
|
||||
const setSchema = useCallback((s: StructuredOutputSchemaInput): void => {
|
||||
// Validate via StructuredOutputSchema (wraps the input as if it were a full response_format object).
|
||||
const parsed = StructuredOutputSchema.safeParse({
|
||||
type: "json_schema",
|
||||
json_schema: s,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
const message = parsed.error.issues.map((i) => i.message).join("; ");
|
||||
setError(message);
|
||||
return;
|
||||
}
|
||||
setSchemaState(s);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const validateResponse = useCallback(
|
||||
(content: unknown): ValidateResponseResult => {
|
||||
if (schema == null) {
|
||||
return { valid: false, error: "No schema set" };
|
||||
}
|
||||
|
||||
// Step 1: parse JSON if the content is a string.
|
||||
let parsed: unknown = content;
|
||||
if (typeof content === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
return { valid: false, error: "Response is not valid JSON" };
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: basic shape validation — must be a non-null object.
|
||||
// Full JSON Schema validation would require ajv (not present in this project).
|
||||
// We do a structural check: verify it's an object, and for each key in the
|
||||
// schema's `properties` (if present), check that the key exists in the response.
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return { valid: false, error: "Response is not a JSON object" };
|
||||
}
|
||||
|
||||
const responseObj = parsed as Record<string, unknown>;
|
||||
const schemaObj = schema.schema;
|
||||
|
||||
// If the schema has a "properties" field, validate that required keys are present.
|
||||
const properties = schemaObj["properties"];
|
||||
if (properties != null && typeof properties === "object" && !Array.isArray(properties)) {
|
||||
const required = schemaObj["required"];
|
||||
if (Array.isArray(required)) {
|
||||
for (const key of required) {
|
||||
if (typeof key === "string" && !(key in responseObj)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Response is missing required field: "${key}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
[schema],
|
||||
);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
schema,
|
||||
error,
|
||||
setEnabled,
|
||||
setSchema,
|
||||
validateResponse,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// src/app/(dashboard)/dashboard/playground/hooks/useToolsBuilder.ts
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { ToolDefinitionSchema } from "@/shared/schemas/playground";
|
||||
import type { ToolDefinition } from "@/lib/playground/codeExport";
|
||||
|
||||
export interface ToolsBuildResult {
|
||||
ok: true;
|
||||
}
|
||||
|
||||
export interface ToolsBuildError {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type ToolsBuildOutcome = ToolsBuildResult | ToolsBuildError;
|
||||
|
||||
export interface UseToolsBuilderState {
|
||||
tools: ToolDefinition[];
|
||||
errors: Map<number, string>;
|
||||
}
|
||||
|
||||
export interface UseToolsBuilder extends UseToolsBuilderState {
|
||||
/**
|
||||
* Add a tool after Zod validation.
|
||||
* Returns { ok: true } on success; { ok: false, error } if validation fails.
|
||||
*/
|
||||
add: (tool: ToolDefinition) => ToolsBuildOutcome;
|
||||
/** Remove tool at given index. No-op if index out of bounds. */
|
||||
remove: (index: number) => void;
|
||||
/**
|
||||
* Replace tool at index after Zod validation.
|
||||
* Returns { ok: true } on success; { ok: false, error } if validation fails.
|
||||
* Clears previous error for that index on success.
|
||||
*/
|
||||
update: (index: number, tool: ToolDefinition) => ToolsBuildOutcome;
|
||||
/** Remove all tools and clear all errors. */
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side manager for the `tools[]` array (Function Calling / Build tab).
|
||||
*
|
||||
* Validates each tool via Zod `ToolDefinitionSchema` before adding/updating.
|
||||
* Stores per-index validation errors so the UI can show inline hints.
|
||||
*/
|
||||
export function useToolsBuilder(): UseToolsBuilder {
|
||||
const [tools, setTools] = useState<ToolDefinition[]>([]);
|
||||
const [errors, setErrors] = useState<Map<number, string>>(new Map());
|
||||
|
||||
const add = useCallback((tool: ToolDefinition): ToolsBuildOutcome => {
|
||||
const parsed = ToolDefinitionSchema.safeParse(tool);
|
||||
if (!parsed.success) {
|
||||
const message = parsed.error.issues.map((i) => i.message).join("; ");
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
setTools((prev) => [...prev, parsed.data as ToolDefinition]);
|
||||
return { ok: true };
|
||||
}, []);
|
||||
|
||||
const remove = useCallback((index: number): void => {
|
||||
setTools((prev) => {
|
||||
if (index < 0 || index >= prev.length) return prev;
|
||||
const next = [...prev];
|
||||
next.splice(index, 1);
|
||||
return next;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(index);
|
||||
// Re-index errors for items after the removed one.
|
||||
const reindexed = new Map<number, string>();
|
||||
next.forEach((v, k) => {
|
||||
reindexed.set(k > index ? k - 1 : k, v);
|
||||
});
|
||||
return reindexed;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const update = useCallback((index: number, tool: ToolDefinition): ToolsBuildOutcome => {
|
||||
const parsed = ToolDefinitionSchema.safeParse(tool);
|
||||
if (!parsed.success) {
|
||||
const message = parsed.error.issues.map((i) => i.message).join("; ");
|
||||
setErrors((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(index, message);
|
||||
return next;
|
||||
});
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
setTools((prev) => {
|
||||
if (index < 0 || index >= prev.length) return prev;
|
||||
const next = [...prev];
|
||||
next[index] = parsed.data as ToolDefinition;
|
||||
return next;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(index);
|
||||
return next;
|
||||
});
|
||||
return { ok: true };
|
||||
}, []);
|
||||
|
||||
const clear = useCallback((): void => {
|
||||
setTools([]);
|
||||
setErrors(new Map());
|
||||
}, []);
|
||||
|
||||
return {
|
||||
tools,
|
||||
errors,
|
||||
add,
|
||||
remove,
|
||||
update,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user