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:
Ajax Davis 2026-01-16 02:33:57 +10:00
parent c21ed5b41e
commit 3456b26c9d
8 changed files with 480 additions and 0 deletions

View file

@ -11,6 +11,7 @@ import { AppHeader } from '~/components/AppHeader';
import { ForkButton } from '~/components/ForkButton';
import { ForkedFromBadge } from '~/components/ForkedFromBadge';
import { LikeButton } from '~/components/LikeButton';
import { UseCasesSection } from '~/components/UseCasesSection';
import { useSession } from '~/lib/auth-client';
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 {
id: string;
slug: string;
@ -55,6 +70,8 @@ interface PublicCollection {
username: string;
};
} | null;
useCases: UseCase[] | null;
useCasesGeneratedAt: string | null;
}
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
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 () => {
try {
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>
</div>
)}
{/* Use Cases Section - at the bottom */}
{collection.tools.length > 0 && (
<UseCasesSection
collectionId={collection.id}
useCases={collection.useCases}
generatedAt={collection.useCasesGeneratedAt}
onUseCasesGenerated={handleUseCasesGenerated}
/>
)}
</div>
) : null}
</main>

View file

@ -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);
}
}

View file

@ -108,6 +108,8 @@ export async function GET(_request: NextRequest, context: RouteContext) {
})),
forkedFromId: collection.forkedFromId,
forkedFrom: collection.forkedFrom,
useCases: collection.useCases,
useCasesGeneratedAt: collection.useCasesGeneratedAt?.toISOString() ?? null,
},
{ requestId }
);

View 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>
);
}

View 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;
}

View file

@ -105,6 +105,13 @@ export const STRICT_RATE_LIMIT: RateLimitConfig = {
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
*/

View file

@ -454,6 +454,10 @@ model Collection {
forks Collection[] @relation("CollectionForks")
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
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

View file

@ -96,6 +96,38 @@ export const CloneCollectionSchema = z.object({
.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)
// ============================================================================
@ -109,6 +141,8 @@ export const CollectionSchema = z.object({
toolCount: z.number(),
forkCount: z.number().default(0),
forkedFromId: z.string().nullable().optional(),
useCases: CollectionUseCasesSchema.nullable().optional(),
useCasesGeneratedAt: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
});