feat(collections): add AI-generated use cases
Add "Example Use Cases" section to collection pages that generates practical workflow examples showing how tools can work together. - Add useCases and useCasesGeneratedAt fields to Collection model - Create use-cases-generator.ts using Vercel AI SDK with gpt-4.1-mini - Add POST /api/collections/[id]/use-cases/generate endpoint - Add AI_GENERATION_RATE_LIMIT (5 req/hour per IP) - Create UseCasesSection component with generate/regenerate UI - Generate 6 use cases: 3 simple (1-2 tools) + 3 complex (3-5 tools) - Include useCases in public collection API response
This commit is contained in:
parent
c21ed5b41e
commit
3456b26c9d
8 changed files with 480 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ import { AppHeader } from '~/components/AppHeader';
|
||||||
import { ForkButton } from '~/components/ForkButton';
|
import { ForkButton } from '~/components/ForkButton';
|
||||||
import { ForkedFromBadge } from '~/components/ForkedFromBadge';
|
import { ForkedFromBadge } from '~/components/ForkedFromBadge';
|
||||||
import { LikeButton } from '~/components/LikeButton';
|
import { LikeButton } from '~/components/LikeButton';
|
||||||
|
import { UseCasesSection } from '~/components/UseCasesSection';
|
||||||
import { useSession } from '~/lib/auth-client';
|
import { useSession } from '~/lib/auth-client';
|
||||||
|
|
||||||
interface CollectionTool {
|
interface CollectionTool {
|
||||||
|
|
@ -30,6 +31,20 @@ interface CollectionTool {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UseCaseToolStep {
|
||||||
|
toolName: string;
|
||||||
|
packageName: string;
|
||||||
|
purpose: string;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCase {
|
||||||
|
id: string;
|
||||||
|
userPrompt: string;
|
||||||
|
description: string;
|
||||||
|
toolSequence: UseCaseToolStep[];
|
||||||
|
}
|
||||||
|
|
||||||
interface PublicCollection {
|
interface PublicCollection {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
@ -55,6 +70,8 @@ interface PublicCollection {
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
} | null;
|
} | null;
|
||||||
|
useCases: UseCase[] | null;
|
||||||
|
useCasesGeneratedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function McpUrlSection({ username, slug }: { username: string; slug: string }) {
|
function McpUrlSection({ username, slug }: { username: string; slug: string }) {
|
||||||
|
|
@ -184,6 +201,20 @@ export default function PrettyCollectionDetailPage(): React.ReactElement {
|
||||||
// Check if current user is the owner
|
// Check if current user is the owner
|
||||||
const isOwner = session?.user?.id && collection?.createdBy?.id === session.user.id;
|
const isOwner = session?.user?.id && collection?.createdBy?.id === session.user.id;
|
||||||
|
|
||||||
|
// Handler for when use cases are generated
|
||||||
|
const handleUseCasesGenerated = useCallback(
|
||||||
|
(useCases: UseCase[], generatedAt: string) => {
|
||||||
|
if (collection) {
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
useCases,
|
||||||
|
useCasesGeneratedAt: generatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[collection]
|
||||||
|
);
|
||||||
|
|
||||||
const fetchCollection = useCallback(async () => {
|
const fetchCollection = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/public/users/${username}/collections/${slug}`);
|
const response = await fetch(`/api/public/users/${username}/collections/${slug}`);
|
||||||
|
|
@ -337,6 +368,16 @@ export default function PrettyCollectionDetailPage(): React.ReactElement {
|
||||||
<p className="text-foreground-secondary">This collection is empty.</p>
|
<p className="text-foreground-secondary">This collection is empty.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Use Cases Section - at the bottom */}
|
||||||
|
{collection.tools.length > 0 && (
|
||||||
|
<UseCasesSection
|
||||||
|
collectionId={collection.id}
|
||||||
|
useCases={collection.useCases}
|
||||||
|
generatedAt={collection.useCasesGeneratedAt}
|
||||||
|
onUseCasesGenerated={handleUseCasesGenerated}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { generateUseCases } from '~/lib/ai/use-cases-generator';
|
||||||
|
import { apiForbidden, apiInternalError, apiNotFound, apiSuccess } from '~/lib/api-response';
|
||||||
|
import { AI_GENERATION_RATE_LIMIT, checkRateLimitDistributed } from '~/lib/rate-limit';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const maxDuration = 60; // AI generation can take time
|
||||||
|
|
||||||
|
type RouteContext = {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/collections/[id]/use-cases/generate
|
||||||
|
* Generate AI-powered use cases for a public collection
|
||||||
|
* Rate limited to 5 requests per hour per IP (expensive AI operation)
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest, context: RouteContext) {
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Rate limit check (strict for AI operations)
|
||||||
|
const rateLimitResponse = await checkRateLimitDistributed(request, AI_GENERATION_RATE_LIMIT);
|
||||||
|
if (rateLimitResponse) {
|
||||||
|
return rateLimitResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await context.params;
|
||||||
|
|
||||||
|
// Fetch collection with tools
|
||||||
|
const collection = await prisma.collection.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
tools: {
|
||||||
|
include: {
|
||||||
|
tool: {
|
||||||
|
include: {
|
||||||
|
package: {
|
||||||
|
select: { npmPackageName: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { position: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return apiNotFound('Collection', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only public collections can have use cases generated
|
||||||
|
if (!collection.isPublic) {
|
||||||
|
return apiForbidden('Use cases can only be generated for public collections', requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate collection has tools
|
||||||
|
if (collection.tools.length === 0) {
|
||||||
|
return apiForbidden(
|
||||||
|
'Collection must have at least one tool to generate use cases',
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare tool info for AI
|
||||||
|
const toolsInfo = collection.tools.map((ct) => ({
|
||||||
|
name: ct.tool.name,
|
||||||
|
description: ct.tool.description,
|
||||||
|
packageName: ct.tool.package.npmPackageName,
|
||||||
|
inputSchema: ct.tool.inputSchema,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Generate use cases with AI (3 simple + 3 complex = 6 total)
|
||||||
|
const result = await generateUseCases(collection.name, collection.description, toolsInfo);
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
const now = new Date();
|
||||||
|
await prisma.collection.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
useCases: result.useCases,
|
||||||
|
useCasesGeneratedAt: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return apiSuccess(
|
||||||
|
{
|
||||||
|
useCases: result.useCases,
|
||||||
|
generatedAt: now.toISOString(),
|
||||||
|
},
|
||||||
|
{ requestId }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API Error] POST /api/collections/[id]/use-cases/generate:', error);
|
||||||
|
return apiInternalError('Failed to generate use cases', requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -108,6 +108,8 @@ export async function GET(_request: NextRequest, context: RouteContext) {
|
||||||
})),
|
})),
|
||||||
forkedFromId: collection.forkedFromId,
|
forkedFromId: collection.forkedFromId,
|
||||||
forkedFrom: collection.forkedFrom,
|
forkedFrom: collection.forkedFrom,
|
||||||
|
useCases: collection.useCases,
|
||||||
|
useCasesGeneratedAt: collection.useCasesGeneratedAt?.toISOString() ?? null,
|
||||||
},
|
},
|
||||||
{ requestId }
|
{ requestId }
|
||||||
);
|
);
|
||||||
|
|
|
||||||
170
apps/web/src/components/UseCasesSection.tsx
Normal file
170
apps/web/src/components/UseCasesSection.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||||
|
import { Button } from '@tpmjs/ui/Button/Button';
|
||||||
|
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface UseCaseToolStep {
|
||||||
|
toolName: string;
|
||||||
|
packageName: string;
|
||||||
|
purpose: string;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCase {
|
||||||
|
id: string;
|
||||||
|
userPrompt: string;
|
||||||
|
description: string;
|
||||||
|
toolSequence: UseCaseToolStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCasesSectionProps {
|
||||||
|
collectionId: string;
|
||||||
|
useCases: UseCase[] | null;
|
||||||
|
generatedAt: string | null;
|
||||||
|
onUseCasesGenerated?: (useCases: UseCase[], generatedAt: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UseCasesSection({
|
||||||
|
collectionId,
|
||||||
|
useCases,
|
||||||
|
generatedAt,
|
||||||
|
onUseCasesGenerated,
|
||||||
|
}: UseCasesSectionProps) {
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
setIsGenerating(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/collections/${collectionId}/use-cases/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(`Rate limited. Try again in ${Math.ceil(data.retryAfter / 60)} minute(s).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error?.message || 'Failed to generate use cases');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
onUseCasesGenerated?.(data.data.useCases, data.data.generatedAt);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to generate use cases');
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 bg-primary/10 rounded-lg">
|
||||||
|
<Icon icon="star" className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">Example Use Cases</h2>
|
||||||
|
</div>
|
||||||
|
{useCases && useCases.length > 0 && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleGenerate} disabled={isGenerating}>
|
||||||
|
{isGenerating ? (
|
||||||
|
<>
|
||||||
|
<Icon icon="loader" className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
|
Regenerating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon="loader" className="w-4 h-4 mr-1.5" />
|
||||||
|
Regenerate
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-error/10 border border-error/20 rounded-lg text-sm text-error">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!useCases || useCases.length === 0 ? (
|
||||||
|
<div className="p-6 bg-surface border border-border rounded-xl text-center">
|
||||||
|
{isGenerating ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Icon icon="loader" className="w-8 h-8 mx-auto text-primary animate-spin" />
|
||||||
|
<p className="text-foreground-secondary">Generating use cases with AI...</p>
|
||||||
|
<p className="text-xs text-foreground-tertiary">This may take a few seconds</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Icon icon="star" className="w-8 h-8 mx-auto text-foreground-tertiary" />
|
||||||
|
<p className="text-foreground-secondary">See how these tools can work together</p>
|
||||||
|
<Button onClick={handleGenerate} disabled={isGenerating}>
|
||||||
|
<Icon icon="star" className="w-4 h-4 mr-1.5" />
|
||||||
|
Suggest Use Cases
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{useCases.map((useCase) => (
|
||||||
|
<div
|
||||||
|
key={useCase.id}
|
||||||
|
className="p-4 bg-surface border border-border rounded-xl space-y-3"
|
||||||
|
>
|
||||||
|
{/* User Prompt */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-1.5 bg-primary/10 rounded-lg shrink-0 mt-0.5">
|
||||||
|
<Icon icon="message" className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-foreground-tertiary mb-1">Example prompt:</p>
|
||||||
|
<p className="text-foreground font-medium">"{useCase.userPrompt}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-foreground-secondary pl-9">{useCase.description}</p>
|
||||||
|
|
||||||
|
{/* Tool Sequence */}
|
||||||
|
<div className="pl-9">
|
||||||
|
<p className="text-xs text-foreground-tertiary mb-2">Tool workflow:</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{useCase.toolSequence
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
.map((step, index) => (
|
||||||
|
<div key={`${useCase.id}-${step.order}`} className="flex items-center gap-2">
|
||||||
|
<Badge variant="secondary" className="text-xs" title={step.purpose}>
|
||||||
|
<span className="text-foreground-tertiary mr-1">{step.order}.</span>
|
||||||
|
{step.toolName}
|
||||||
|
</Badge>
|
||||||
|
{index < useCase.toolSequence.length - 1 && (
|
||||||
|
<Icon icon="chevronRight" className="w-3 h-3 text-foreground-tertiary" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{generatedAt && (
|
||||||
|
<p className="text-xs text-foreground-tertiary text-center">
|
||||||
|
Generated {new Date(generatedAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
apps/web/src/lib/ai/use-cases-generator.ts
Normal file
121
apps/web/src/lib/ai/use-cases-generator.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* AI-powered use case generation for tool collections
|
||||||
|
* Uses Vercel AI SDK with structured output to generate realistic workflows
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { openai } from '@ai-sdk/openai';
|
||||||
|
import { generateObject } from 'ai';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Schema for structured output
|
||||||
|
const ToolStepSchema = z.object({
|
||||||
|
toolName: z.string().describe('Name of the tool being invoked'),
|
||||||
|
packageName: z.string().describe('NPM package name containing the tool'),
|
||||||
|
purpose: z.string().describe('Why this tool is called at this step (max 100 chars)'),
|
||||||
|
order: z.number().int().min(1).describe('Execution order (1-based)'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UseCaseOutputSchema = z.object({
|
||||||
|
id: z.string().describe('Unique identifier for this use case'),
|
||||||
|
userPrompt: z
|
||||||
|
.string()
|
||||||
|
.describe('Example user prompt that would trigger this workflow (30-200 chars)'),
|
||||||
|
description: z.string().describe('Brief description of what this accomplishes (50-150 chars)'),
|
||||||
|
toolSequence: z.array(ToolStepSchema).min(1).max(10).describe('Ordered sequence of tool calls'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UseCasesOutputSchema = z.object({
|
||||||
|
useCases: z.array(UseCaseOutputSchema).min(6).max(6).describe('Array of EXACTLY 6 use cases'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GeneratedUseCase = z.infer<typeof UseCaseOutputSchema>;
|
||||||
|
export type GeneratedUseCases = z.infer<typeof UseCasesOutputSchema>;
|
||||||
|
|
||||||
|
interface ToolInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
packageName: string;
|
||||||
|
inputSchema: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemPrompt(): string {
|
||||||
|
return `You are an expert at understanding AI tool collections and generating practical use cases.
|
||||||
|
|
||||||
|
Your task is to analyze a collection of MCP (Model Context Protocol) tools and suggest realistic use cases that demonstrate how an AI agent would use these tools together to accomplish tasks.
|
||||||
|
|
||||||
|
Guidelines for generating use cases:
|
||||||
|
1. Each use case should have a realistic, natural user prompt (what a human would actually ask)
|
||||||
|
2. The tool sequence should show logical orchestration - how tools would be called in order
|
||||||
|
3. Focus on practical, achievable workflows that make sense for the tools available
|
||||||
|
4. Tools can be called multiple times if needed
|
||||||
|
5. Consider data flow between tools - output from one tool may inform the next
|
||||||
|
6. Keep descriptions concise but informative
|
||||||
|
7. Generate unique IDs using the pattern "uc-" followed by a short descriptive slug
|
||||||
|
|
||||||
|
Important:
|
||||||
|
- Only use tools that are actually in the collection
|
||||||
|
- Be creative but realistic - suggest workflows users would actually want
|
||||||
|
- Vary the complexity - some simple (1-2 tools), some more complex (3-5 tools)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUserPrompt(
|
||||||
|
collectionName: string,
|
||||||
|
collectionDescription: string | null,
|
||||||
|
tools: ToolInfo[]
|
||||||
|
): string {
|
||||||
|
const toolsDescription = tools
|
||||||
|
.map(
|
||||||
|
(t) => `
|
||||||
|
**${t.name}** (from ${t.packageName})
|
||||||
|
Description: ${t.description}
|
||||||
|
${t.inputSchema ? `Input Schema: ${JSON.stringify(t.inputSchema, null, 2)}` : 'No input schema available'}`
|
||||||
|
)
|
||||||
|
.join('\n---\n');
|
||||||
|
|
||||||
|
return `Generate EXACTLY 6 different use cases for this tool collection. You MUST return exactly 6 use cases.
|
||||||
|
|
||||||
|
**Collection Name:** ${collectionName}
|
||||||
|
${collectionDescription ? `**Description:** ${collectionDescription}` : ''}
|
||||||
|
|
||||||
|
**Available Tools (${tools.length} total):**
|
||||||
|
${toolsDescription}
|
||||||
|
|
||||||
|
IMPORTANT: Create EXACTLY 6 use cases with this structure:
|
||||||
|
|
||||||
|
**First 3 use cases - SIMPLE (1-2 tools each):**
|
||||||
|
- Quick, focused tasks that use just 1 or 2 tools
|
||||||
|
- Straightforward user prompts
|
||||||
|
- Good for showing basic capabilities
|
||||||
|
|
||||||
|
**Last 3 use cases - COMPLEX (3-5 tools each):**
|
||||||
|
- Multi-step workflows that chain 3-5 tools together
|
||||||
|
- More sophisticated user prompts
|
||||||
|
- Show how tools can work together for advanced tasks
|
||||||
|
|
||||||
|
Each use case should have a unique purpose and demonstrate different capabilities of the collection.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate use cases for a collection of tools using AI
|
||||||
|
*/
|
||||||
|
export async function generateUseCases(
|
||||||
|
collectionName: string,
|
||||||
|
collectionDescription: string | null,
|
||||||
|
tools: ToolInfo[]
|
||||||
|
): Promise<GeneratedUseCases> {
|
||||||
|
if (tools.length === 0) {
|
||||||
|
throw new Error('Collection must have at least one tool to generate use cases');
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt = buildSystemPrompt();
|
||||||
|
const userPrompt = buildUserPrompt(collectionName, collectionDescription, tools);
|
||||||
|
|
||||||
|
const result = await generateObject({
|
||||||
|
model: openai('gpt-4.1-mini'),
|
||||||
|
schema: UseCasesOutputSchema,
|
||||||
|
system: systemPrompt,
|
||||||
|
prompt: userPrompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.object;
|
||||||
|
}
|
||||||
|
|
@ -105,6 +105,13 @@ export const STRICT_RATE_LIMIT: RateLimitConfig = {
|
||||||
windowSeconds: 60,
|
windowSeconds: 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** AI generation rate limit: 5 requests per hour (expensive AI operations) */
|
||||||
|
export const AI_GENERATION_RATE_LIMIT: RateLimitConfig = {
|
||||||
|
limit: 5,
|
||||||
|
windowSeconds: 3600, // 1 hour
|
||||||
|
prefix: 'ai-gen',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to add timeout to promises
|
* Helper to add timeout to promises
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,10 @@ model Collection {
|
||||||
forks Collection[] @relation("CollectionForks")
|
forks Collection[] @relation("CollectionForks")
|
||||||
forkCount Int @default(0) @map("fork_count")
|
forkCount Int @default(0) @map("fork_count")
|
||||||
|
|
||||||
|
// AI-generated use cases
|
||||||
|
useCases Json? @map("use_cases") @db.JsonB
|
||||||
|
useCasesGeneratedAt DateTime? @map("use_cases_generated_at")
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,38 @@ export const CloneCollectionSchema = z.object({
|
||||||
.optional(), // If not provided, will use original name or append "(copy)"
|
.optional(), // If not provided, will use original name or append "(copy)"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Use Case Types (AI-generated workflows)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const UseCaseToolStepSchema = z.object({
|
||||||
|
toolName: z.string().describe('Name of the tool being invoked'),
|
||||||
|
packageName: z.string().describe('NPM package name containing the tool'),
|
||||||
|
purpose: z.string().max(100).describe('Why this tool is called at this step'),
|
||||||
|
order: z.number().int().min(1).describe('Execution order (1-based)'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UseCaseSchema = z.object({
|
||||||
|
id: z.string().describe('Unique identifier for this use case'),
|
||||||
|
userPrompt: z
|
||||||
|
.string()
|
||||||
|
.min(20)
|
||||||
|
.max(200)
|
||||||
|
.describe('Example user prompt that triggers this workflow'),
|
||||||
|
description: z.string().min(30).max(150).describe('Brief description of what this accomplishes'),
|
||||||
|
toolSequence: z
|
||||||
|
.array(UseCaseToolStepSchema)
|
||||||
|
.min(1)
|
||||||
|
.max(10)
|
||||||
|
.describe('Ordered sequence of tool calls'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CollectionUseCasesSchema = z.array(UseCaseSchema).max(6);
|
||||||
|
|
||||||
|
export type UseCaseToolStep = z.infer<typeof UseCaseToolStepSchema>;
|
||||||
|
export type UseCase = z.infer<typeof UseCaseSchema>;
|
||||||
|
export type CollectionUseCases = z.infer<typeof CollectionUseCasesSchema>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Response Types (for API responses)
|
// Response Types (for API responses)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -109,6 +141,8 @@ export const CollectionSchema = z.object({
|
||||||
toolCount: z.number(),
|
toolCount: z.number(),
|
||||||
forkCount: z.number().default(0),
|
forkCount: z.number().default(0),
|
||||||
forkedFromId: z.string().nullable().optional(),
|
forkedFromId: z.string().nullable().optional(),
|
||||||
|
useCases: CollectionUseCasesSchema.nullable().optional(),
|
||||||
|
useCasesGeneratedAt: z.date().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue