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 { 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
forkedFrom: collection.forkedFrom,
|
||||
useCases: collection.useCases,
|
||||
useCasesGeneratedAt: collection.useCasesGeneratedAt?.toISOString() ?? null,
|
||||
},
|
||||
{ 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,
|
||||
};
|
||||
|
||||
/** 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue