feat(schema-extraction): auto-extract inputSchema via executor during sync
- Add inputSchema, schemaSource, schemaExtractedAt fields to Tool model - Create schema extraction helper that calls executor's /load-and-describe - Update sync routes to extract schema synchronously after tool upsert - Reduce changes feed batch size from 100 to 30 for extraction time - Update /api/tools/update-schema to store full JSON Schema - Add /api/tools/extract-schema endpoint for manual re-extraction - Update tool page UI with schema source badge and re-extract button - Add deprecation comments to parameters, returns, aiAgent in types Authors no longer need to define inputSchema in package.json - it's now automatically extracted from the tool at sync time. Falls back to author-provided parameters if extraction fails.
This commit is contained in:
parent
00441c30db
commit
06271e079f
8 changed files with 556 additions and 53 deletions
|
|
@ -4,6 +4,7 @@ import { validateTpmjsField } from '@tpmjs/types/tpmjs';
|
|||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { env } from '~/env';
|
||||
import { performHealthCheck } from '~/lib/health-check/health-check-service';
|
||||
import { convertJsonSchemaToParameters, extractToolSchema } from '~/lib/schema-extraction';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -42,10 +43,10 @@ export async function POST(request: NextRequest) {
|
|||
? String((checkpoint.checkpoint as { lastSeq?: string })?.lastSeq || '0')
|
||||
: '0';
|
||||
|
||||
// Fetch changes from NPM (limit to 100 per run to avoid timeouts)
|
||||
// Fetch changes from NPM (limit to 30 per run to allow time for schema extraction)
|
||||
const changesResult = await fetchChanges({
|
||||
since: lastSeq,
|
||||
limit: 100,
|
||||
limit: 30,
|
||||
includeDocs: false,
|
||||
});
|
||||
|
||||
|
|
@ -150,6 +151,8 @@ export async function POST(request: NextRequest) {
|
|||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
// Schema will be extracted below
|
||||
schemaSource: toolDef.parameters ? 'author' : null,
|
||||
},
|
||||
update: {
|
||||
description: toolDef.description,
|
||||
|
|
@ -162,7 +165,44 @@ export async function POST(request: NextRequest) {
|
|||
},
|
||||
});
|
||||
|
||||
// Trigger immediate health check (non-blocking)
|
||||
// Extract schema synchronously from executor
|
||||
// Note: We pass null for env as schema extraction doesn't need env values
|
||||
const schemaResult = await extractToolSchema(
|
||||
pkg.name,
|
||||
toolDef.exportName,
|
||||
pkg.version,
|
||||
null
|
||||
);
|
||||
|
||||
if (schemaResult.success) {
|
||||
// Update tool with extracted schema
|
||||
await prisma.tool.update({
|
||||
where: { id: upsertedTool.id },
|
||||
data: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
inputSchema: schemaResult.inputSchema as any,
|
||||
// Also update parameters array for backward compatibility
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: convertJsonSchemaToParameters(schemaResult.inputSchema) as any,
|
||||
schemaSource: 'extracted',
|
||||
schemaExtractedAt: new Date(),
|
||||
},
|
||||
});
|
||||
console.log(`Schema extracted for ${pkg.name}/${toolDef.exportName}`);
|
||||
} else {
|
||||
// Extraction failed - mark schema source appropriately
|
||||
console.log(
|
||||
`Schema extraction failed for ${pkg.name}/${toolDef.exportName}: ${schemaResult.error}`
|
||||
);
|
||||
await prisma.tool.update({
|
||||
where: { id: upsertedTool.id },
|
||||
data: {
|
||||
schemaSource: toolDef.parameters ? 'author' : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger health check (non-blocking) for execution testing
|
||||
performHealthCheck(upsertedTool.id, 'sync').catch((err) => {
|
||||
console.error(
|
||||
`Health check failed for ${pkg.name}/${toolDef.exportName} (${upsertedTool.id}):`,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { validateTpmjsField } from '@tpmjs/types/tpmjs';
|
|||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { env } from '~/env';
|
||||
import { performHealthCheck } from '~/lib/health-check/health-check-service';
|
||||
import { convertJsonSchemaToParameters, extractToolSchema } from '~/lib/schema-extraction';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -164,6 +165,8 @@ export async function POST(request: NextRequest) {
|
|||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
aiAgent: toolDef.aiAgent ? (toolDef.aiAgent as any) : undefined,
|
||||
qualityScore: null, // Will be calculated by metrics sync
|
||||
// Schema will be extracted below
|
||||
schemaSource: toolDef.parameters ? 'author' : null,
|
||||
},
|
||||
update: {
|
||||
description: toolDef.description,
|
||||
|
|
@ -176,7 +179,44 @@ export async function POST(request: NextRequest) {
|
|||
},
|
||||
});
|
||||
|
||||
// Trigger immediate health check (non-blocking)
|
||||
// Extract schema synchronously from executor
|
||||
// Note: We pass null for env as schema extraction doesn't need env values
|
||||
const schemaResult = await extractToolSchema(
|
||||
pkg.name,
|
||||
toolDef.exportName,
|
||||
pkg.version,
|
||||
null
|
||||
);
|
||||
|
||||
if (schemaResult.success) {
|
||||
// Update tool with extracted schema
|
||||
await prisma.tool.update({
|
||||
where: { id: upsertedTool.id },
|
||||
data: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
inputSchema: schemaResult.inputSchema as any,
|
||||
// Also update parameters array for backward compatibility
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: convertJsonSchemaToParameters(schemaResult.inputSchema) as any,
|
||||
schemaSource: 'extracted',
|
||||
schemaExtractedAt: new Date(),
|
||||
},
|
||||
});
|
||||
console.log(`Schema extracted for ${pkg.name}/${toolDef.exportName}`);
|
||||
} else {
|
||||
// Extraction failed - mark schema source appropriately
|
||||
console.log(
|
||||
`Schema extraction failed for ${pkg.name}/${toolDef.exportName}: ${schemaResult.error}`
|
||||
);
|
||||
await prisma.tool.update({
|
||||
where: { id: upsertedTool.id },
|
||||
data: {
|
||||
schemaSource: toolDef.parameters ? 'author' : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger health check (non-blocking) for execution testing
|
||||
performHealthCheck(upsertedTool.id, 'sync').catch((err) => {
|
||||
console.error(
|
||||
`Health check failed for ${pkg.name}/${toolDef.exportName} (${upsertedTool.id}):`,
|
||||
|
|
|
|||
159
apps/web/src/app/api/tools/extract-schema/route.ts
Normal file
159
apps/web/src/app/api/tools/extract-schema/route.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { prisma } from '@tpmjs/db';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { convertJsonSchemaToParameters, extractToolSchema } from '~/lib/schema-extraction';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* POST /api/tools/extract-schema
|
||||
* Manually trigger schema extraction for a tool
|
||||
*
|
||||
* Body:
|
||||
* - packageName: npm package name
|
||||
* - exportName: exported function name
|
||||
*
|
||||
* Rate limited to 1 extraction per minute per tool
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { packageName, exportName } = body;
|
||||
|
||||
if (!packageName || !exportName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'packageName and exportName are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Extract Schema] Looking up tool:', { packageName, exportName });
|
||||
|
||||
// Find the tool by package name and export name
|
||||
const tool = await prisma.tool.findFirst({
|
||||
where: {
|
||||
exportName,
|
||||
package: {
|
||||
npmPackageName: packageName,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
package: {
|
||||
select: {
|
||||
npmPackageName: true,
|
||||
npmVersion: true,
|
||||
env: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tool) {
|
||||
console.log('[Extract Schema] Tool not found:', { packageName, exportName });
|
||||
return NextResponse.json({ success: false, error: 'Tool not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Rate limit: 1 minute cooldown
|
||||
if (tool.schemaExtractedAt) {
|
||||
const timeSinceLastExtraction = Date.now() - tool.schemaExtractedAt.getTime();
|
||||
const cooldownMs = 60000; // 1 minute
|
||||
|
||||
if (timeSinceLastExtraction < cooldownMs) {
|
||||
const retryAfter = Math.ceil((cooldownMs - timeSinceLastExtraction) / 1000);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Rate limited',
|
||||
message: `Please wait ${retryAfter} seconds before trying again`,
|
||||
retryAfter,
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Extract Schema] Extracting schema for:', {
|
||||
packageName: tool.package.npmPackageName,
|
||||
exportName: tool.exportName,
|
||||
version: tool.package.npmVersion,
|
||||
});
|
||||
|
||||
// Extract schema from executor
|
||||
const schemaResult = await extractToolSchema(
|
||||
tool.package.npmPackageName,
|
||||
tool.exportName,
|
||||
tool.package.npmVersion,
|
||||
tool.package.env as Record<string, unknown> | null
|
||||
);
|
||||
|
||||
if (schemaResult.success) {
|
||||
// Update tool with extracted schema
|
||||
const updatedTool = await prisma.tool.update({
|
||||
where: { id: tool.id },
|
||||
data: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
inputSchema: schemaResult.inputSchema as any,
|
||||
// Also update parameters array for backward compatibility
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: convertJsonSchemaToParameters(schemaResult.inputSchema) as any,
|
||||
schemaSource: 'extracted',
|
||||
schemaExtractedAt: new Date(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
exportName: true,
|
||||
inputSchema: true,
|
||||
parameters: true,
|
||||
schemaSource: true,
|
||||
schemaExtractedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[Extract Schema] Schema extracted successfully:', {
|
||||
toolId: updatedTool.id,
|
||||
exportName: updatedTool.exportName,
|
||||
schemaSource: updatedTool.schemaSource,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Schema extracted successfully',
|
||||
schemaSource: 'extracted',
|
||||
tool: updatedTool,
|
||||
});
|
||||
}
|
||||
|
||||
// Extraction failed
|
||||
console.log('[Extract Schema] Extraction failed:', {
|
||||
packageName,
|
||||
exportName,
|
||||
error: schemaResult.error,
|
||||
});
|
||||
|
||||
// Update tool to mark extraction attempt
|
||||
await prisma.tool.update({
|
||||
where: { id: tool.id },
|
||||
data: {
|
||||
schemaExtractedAt: new Date(), // Update timestamp even on failure for rate limiting
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Schema extraction failed',
|
||||
message: schemaResult.error,
|
||||
schemaSource: tool.schemaSource,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Extract Schema] Error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to extract schema',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
import { prisma } from '@tpmjs/db';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { convertJsonSchemaToParameters } from '~/lib/schema-extraction';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* POST /api/tools/update-schema
|
||||
* Update a tool's input schema (parameters field)
|
||||
* Update a tool's input schema
|
||||
*
|
||||
* Called by the executor when it loads a tool and discovers its schema.
|
||||
* Looks up tool by packageName + exportName (unique constraint).
|
||||
* Stores the full JSON Schema and also converts to parameters array for backward compatibility.
|
||||
*
|
||||
* Body:
|
||||
* - packageName: npm package name
|
||||
|
|
@ -48,7 +50,8 @@ export async function POST(request: NextRequest) {
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
parameters: true,
|
||||
inputSchema: true,
|
||||
schemaSource: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -60,49 +63,39 @@ export async function POST(request: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
// Convert JSON Schema to our parameters format
|
||||
const parameters: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
}> = [];
|
||||
if (inputSchema.properties) {
|
||||
for (const [name, prop] of Object.entries(inputSchema.properties)) {
|
||||
const propDef = prop as { type?: string; description?: string };
|
||||
parameters.push({
|
||||
name,
|
||||
type: propDef.type || 'string',
|
||||
required: inputSchema.required?.includes(name) || false,
|
||||
description: propDef.description || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
// Convert JSON Schema to parameters array for backward compatibility
|
||||
const parameters = convertJsonSchemaToParameters(inputSchema);
|
||||
|
||||
// Check if parameters already match (avoid unnecessary updates)
|
||||
const existingParams = tool.parameters as Array<{ name: string }> | null;
|
||||
const existingParamNames =
|
||||
existingParams
|
||||
?.map((p) => p.name)
|
||||
.sort()
|
||||
.join(',') || '';
|
||||
const newParamNames = parameters
|
||||
.map((p) => p.name)
|
||||
.sort()
|
||||
.join(',');
|
||||
|
||||
if (existingParamNames === newParamNames && parameters.length > 0) {
|
||||
// Check if schema already matches (avoid unnecessary updates)
|
||||
const existingSchema = tool.inputSchema as Record<string, unknown> | null;
|
||||
if (
|
||||
existingSchema &&
|
||||
tool.schemaSource === 'extracted' &&
|
||||
JSON.stringify(existingSchema) === JSON.stringify(inputSchema)
|
||||
) {
|
||||
console.log('[Update Schema] Schema already up to date:', { packageName, exportName });
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
updated: false,
|
||||
message: 'Schema already up to date',
|
||||
schemaSource: tool.schemaSource,
|
||||
});
|
||||
}
|
||||
|
||||
// Update the tool
|
||||
const updateData: { parameters: typeof parameters; description?: string } = {
|
||||
// Build update data
|
||||
const updateData: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
inputSchema: any;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
|
||||
parameters: any;
|
||||
schemaSource: string;
|
||||
schemaExtractedAt: Date;
|
||||
description?: string;
|
||||
} = {
|
||||
inputSchema,
|
||||
parameters,
|
||||
schemaSource: 'extracted',
|
||||
schemaExtractedAt: new Date(),
|
||||
};
|
||||
|
||||
if (description) {
|
||||
|
|
@ -117,6 +110,9 @@ export async function POST(request: NextRequest) {
|
|||
exportName: true,
|
||||
description: true,
|
||||
parameters: true,
|
||||
inputSchema: true,
|
||||
schemaSource: true,
|
||||
schemaExtractedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -125,11 +121,13 @@ export async function POST(request: NextRequest) {
|
|||
exportName: updatedTool.exportName,
|
||||
parameterCount: parameters.length,
|
||||
parameterNames: parameters.map((p) => p.name),
|
||||
schemaSource: updatedTool.schemaSource,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
updated: true,
|
||||
schemaSource: updatedTool.schemaSource,
|
||||
tool: updatedTool,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ interface Tool {
|
|||
required: boolean;
|
||||
default?: unknown;
|
||||
}> | null;
|
||||
inputSchema: Record<string, unknown> | null;
|
||||
schemaSource: 'extracted' | 'author' | null;
|
||||
schemaExtractedAt: string | null;
|
||||
returns: {
|
||||
type: string;
|
||||
description: string;
|
||||
|
|
@ -77,6 +80,7 @@ export default function ToolDetailPage({
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const [slug, setSlug] = useState<string>('');
|
||||
const [recheckLoading, setRecheckLoading] = useState(false);
|
||||
const [extractSchemaLoading, setExtractSchemaLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Join slug array to reconstruct package name (e.g., ['@tpmjs', 'text-transformer'] -> '@tpmjs/text-transformer')
|
||||
|
|
@ -208,6 +212,40 @@ export default function ToolDetailPage({
|
|||
}
|
||||
};
|
||||
|
||||
const extractSchema = async () => {
|
||||
if (!tool) return;
|
||||
|
||||
setExtractSchemaLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/tools/extract-schema', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
packageName: tool.package.npmPackageName,
|
||||
exportName: tool.exportName,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
alert(data.message || data.error || 'Schema extraction failed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
// Refresh page to show updated schema
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Schema extraction failed');
|
||||
}
|
||||
} catch {
|
||||
alert('Failed to extract schema');
|
||||
} finally {
|
||||
setExtractSchemaLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<script
|
||||
|
|
@ -410,14 +448,33 @@ console.log(result.text);`}
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* Parameters */}
|
||||
{tool.parameters && tool.parameters.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>Available configuration options</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Parameters / Input Schema */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>Available configuration options</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{tool.schemaSource === 'extracted' ? (
|
||||
<Badge variant="default" size="sm">
|
||||
Auto-extracted
|
||||
</Badge>
|
||||
) : tool.schemaSource === 'author' ? (
|
||||
<Badge variant="secondary" size="sm">
|
||||
Author-provided
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" size="sm">
|
||||
No schema
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tool.parameters && tool.parameters.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{tool.parameters.map((param) => (
|
||||
<div key={param.name} className="border-b border-border pb-4 last:border-0">
|
||||
|
|
@ -446,10 +503,60 @@ console.log(result.text);`}
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
{tool.schemaExtractedAt && (
|
||||
<p className="text-xs text-foreground-tertiary">
|
||||
Schema extracted: {new Date(tool.schemaExtractedAt).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-sm text-foreground-secondary mb-4">
|
||||
No schema available for this tool.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={extractSchema}
|
||||
disabled={extractSchemaLoading}
|
||||
>
|
||||
{extractSchemaLoading ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Extracting...
|
||||
</>
|
||||
) : (
|
||||
'Extract Schema'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{tool.schemaSource !== 'extracted' &&
|
||||
tool.parameters &&
|
||||
tool.parameters.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={extractSchema}
|
||||
disabled={extractSchemaLoading}
|
||||
>
|
||||
{extractSchemaLoading ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Extracting...
|
||||
</>
|
||||
) : (
|
||||
'Re-extract Schema'
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-xs text-foreground-tertiary mt-2">
|
||||
Try to auto-extract schema from the package
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* README */}
|
||||
{pkg.npmReadme && (
|
||||
|
|
|
|||
132
apps/web/src/lib/schema-extraction.ts
Normal file
132
apps/web/src/lib/schema-extraction.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Schema Extraction Service
|
||||
* Extracts inputSchema from tools via the Railway executor's /load-and-describe endpoint
|
||||
*/
|
||||
|
||||
import { env } from '~/env';
|
||||
|
||||
const RAILWAY_EXECUTOR_URL = env.RAILWAY_EXECUTOR_URL;
|
||||
|
||||
export interface SchemaExtractionSuccess {
|
||||
success: true;
|
||||
inputSchema: Record<string, unknown>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SchemaExtractionFailure {
|
||||
success: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type SchemaExtractionResult = SchemaExtractionSuccess | SchemaExtractionFailure;
|
||||
|
||||
/**
|
||||
* Extract inputSchema from a tool by calling the executor's /load-and-describe endpoint
|
||||
*
|
||||
* @param packageName - NPM package name (e.g., "@tpmjs/hello-world")
|
||||
* @param exportName - Export name (e.g., "helloWorldTool" or "default")
|
||||
* @param version - Package version (e.g., "1.0.0")
|
||||
* @param packageEnv - Package-level environment variables (optional)
|
||||
* @returns Schema extraction result with inputSchema or error
|
||||
*/
|
||||
export async function extractToolSchema(
|
||||
packageName: string,
|
||||
exportName: string,
|
||||
version: string,
|
||||
packageEnv?: Record<string, unknown> | null
|
||||
): Promise<SchemaExtractionResult> {
|
||||
try {
|
||||
const response = await fetch(`${RAILWAY_EXECUTOR_URL}/load-and-describe`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
packageName,
|
||||
exportName,
|
||||
version,
|
||||
env: packageEnv || {},
|
||||
}),
|
||||
signal: AbortSignal.timeout(10000), // 10 second timeout per extraction
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${errorText || 'Request failed'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: data.error || 'Extraction failed without error message',
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.tool?.inputSchema) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No inputSchema returned from executor',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
inputSchema: data.tool.inputSchema,
|
||||
description: data.tool.description,
|
||||
};
|
||||
} catch (error) {
|
||||
// Handle timeout specifically
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schema extraction timed out after 10 seconds',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error during extraction',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON Schema to parameters array format (for backward compatibility)
|
||||
*
|
||||
* @param inputSchema - JSON Schema object from executor
|
||||
* @returns Array of parameter objects in legacy format
|
||||
*/
|
||||
export function convertJsonSchemaToParameters(inputSchema: Record<string, unknown>): Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
}> {
|
||||
const parameters: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
}> = [];
|
||||
|
||||
const properties = inputSchema.properties as
|
||||
| Record<string, { type?: string; description?: string }>
|
||||
| undefined;
|
||||
const required = (inputSchema.required as string[]) || [];
|
||||
|
||||
if (properties) {
|
||||
for (const [name, prop] of Object.entries(properties)) {
|
||||
parameters.push({
|
||||
name,
|
||||
type: prop.type || 'string',
|
||||
required: required.includes(name),
|
||||
description: prop.description || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
|
@ -66,9 +66,14 @@ model Tool {
|
|||
|
||||
// Tool Metadata
|
||||
description String @db.Text
|
||||
parameters Json? @db.JsonB
|
||||
returns Json? @db.JsonB
|
||||
aiAgent Json? @map("ai_agent") @db.JsonB
|
||||
parameters Json? @db.JsonB // Legacy/fallback - author-provided parameters array
|
||||
returns Json? @db.JsonB // @deprecated - will be auto-extracted in future
|
||||
aiAgent Json? @map("ai_agent") @db.JsonB // @deprecated - will be auto-extracted in future
|
||||
|
||||
// Schema Extraction Fields
|
||||
inputSchema Json? @map("input_schema") @db.JsonB // Full JSON Schema from executor
|
||||
schemaSource String? @map("schema_source") @db.VarChar(20) // 'extracted' | 'author' | null
|
||||
schemaExtractedAt DateTime? @map("schema_extracted_at")
|
||||
|
||||
// Tool Metrics
|
||||
qualityScore Decimal? @map("quality_score") @db.Decimal(3, 2) // 0.00 to 1.00
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ export type TpmjsCategory = (typeof TPMJS_CATEGORIES)[number];
|
|||
|
||||
/**
|
||||
* Tool parameter schema
|
||||
*
|
||||
* @deprecated Parameters are now auto-extracted from the tool's inputSchema at runtime.
|
||||
* You no longer need to manually specify parameters in your package.json.
|
||||
* This schema is kept for backward compatibility as a fallback if auto-extraction fails.
|
||||
*/
|
||||
export const TpmjsParameterSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
|
|
@ -35,6 +39,9 @@ export type TpmjsParameter = z.infer<typeof TpmjsParameterSchema>;
|
|||
|
||||
/**
|
||||
* Return value schema
|
||||
*
|
||||
* @deprecated Return type information is now auto-extracted from the tool at runtime.
|
||||
* You no longer need to manually specify returns in your package.json.
|
||||
*/
|
||||
export const TpmjsReturnsSchema = z.object({
|
||||
type: z.string().min(1),
|
||||
|
|
@ -57,6 +64,9 @@ export type TpmjsEnv = z.infer<typeof TpmjsEnvSchema>;
|
|||
|
||||
/**
|
||||
* AI Agent guidance schema
|
||||
*
|
||||
* @deprecated AI agent guidance is now auto-extracted from the tool at runtime.
|
||||
* You no longer need to manually specify aiAgent in your package.json.
|
||||
*/
|
||||
export const TpmjsAiAgentSchema = z.object({
|
||||
useCase: z.string().min(10),
|
||||
|
|
@ -68,12 +78,24 @@ export type TpmjsAiAgent = z.infer<typeof TpmjsAiAgentSchema>;
|
|||
|
||||
/**
|
||||
* Individual tool definition within a multi-tool package
|
||||
*
|
||||
* Required fields:
|
||||
* - exportName: The export name of the tool from the package
|
||||
* - description: A description of what the tool does (20-500 chars)
|
||||
*
|
||||
* @deprecated fields (now auto-extracted, kept for backward compatibility):
|
||||
* - parameters: Tool input parameters - auto-extracted from inputSchema
|
||||
* - returns: Tool return type - auto-extracted from tool
|
||||
* - aiAgent: AI agent guidance - auto-extracted from tool
|
||||
*/
|
||||
export const TpmjsToolDefinitionSchema = z.object({
|
||||
exportName: z.string().min(1, 'Export name is required'),
|
||||
description: z.string().min(20, 'Description must be at least 20 characters').max(500),
|
||||
// @deprecated - now auto-extracted from tool's inputSchema
|
||||
parameters: z.array(TpmjsParameterSchema).optional(),
|
||||
// @deprecated - now auto-extracted from tool
|
||||
returns: TpmjsReturnsSchema.optional(),
|
||||
// @deprecated - now auto-extracted from tool
|
||||
aiAgent: TpmjsAiAgentSchema.optional(),
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue