/**
* Analyze Prompt Tool
* Evaluates prompt quality and provides improvement suggestions
* Uses PromptArchitect API with local fallback
*/
import { z } from 'zod';
import {
logger,
apiAnalyzePrompt,
isApiClientAvailable,
} from '../utils/index.js';
export const analyzePromptSchema = z.object({
prompt: z.string().min(1).describe('The prompt to analyze'),
evaluationCriteria: z.array(z.string()).optional().describe('Specific criteria to evaluate'),
});
export type AnalyzePromptInput = z.infer<typeof analyzePromptSchema>;
export interface AnalysisResult {
scores: {
overall: number;
clarity: number;
specificity: number;
structure: number;
actionability: number;
};
suggestions: string[];
strengths: string[];
weaknesses: string[];
metadata: {
wordCount: number;
sentenceCount: number;
hasStructure: boolean;
hasExamples: boolean;
hasConstraints: boolean;
};
}
const DEFAULT_CRITERIA = [
'clarity',
'specificity',
'structure',
'actionability',
];
export async function analyzePrompt(input: AnalyzePromptInput): Promise<AnalysisResult> {
const { prompt, evaluationCriteria = DEFAULT_CRITERIA } = input;
logger.info('Analyzing prompt', {
promptLength: prompt.length,
criteria: evaluationCriteria
});
// Calculate structural metadata (always needed)
const metadata = calculateMetadata(prompt);
// Use PromptArchitect API
if (isApiClientAvailable()) {
try {
const response = await apiAnalyzePrompt({
prompt,
evaluationCriteria,
});
logger.info('Analyzed via PromptArchitect API');
return {
scores: {
overall: response.overallScore,
clarity: response.scores.clarity,
specificity: response.scores.specificity,
structure: response.scores.structure,
actionability: response.scores.actionability,
},
suggestions: response.suggestions || [],
strengths: response.strengths || identifyStrengths(prompt, metadata),
weaknesses: response.weaknesses || identifyWeaknesses(prompt, metadata),
metadata,
};
} catch (error) {
logger.warn('API request failed, using fallback', {
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// Fallback rule-based analysis
logger.warn('Using fallback rule-based analysis');
return performRuleBasedAnalysis(prompt, metadata);
}
function calculateMetadata(prompt: string): AnalysisResult['metadata'] {
const words = prompt.split(/\s+/).filter(w => w.length > 0);
const sentences = prompt.split(/[.!?]+/).filter(s => s.trim().length > 0);
return {
wordCount: words.length,
sentenceCount: sentences.length,
hasStructure: /^#+\s|^\d+\.|^-\s|^\*\s|\[|\]/m.test(prompt),
hasExamples: /example|e\.g\.|for instance|such as|like this/i.test(prompt),
hasConstraints: /must|should|avoid|don't|do not|never|always|required/i.test(prompt),
};
}
function identifyStrengths(prompt: string, metadata: AnalysisResult['metadata']): string[] {
const strengths: string[] = [];
if (metadata.hasStructure) {
strengths.push('Well-structured with clear sections or formatting');
}
if (metadata.hasExamples) {
strengths.push('Includes examples for clarity');
}
if (metadata.hasConstraints) {
strengths.push('Provides clear constraints and requirements');
}
if (metadata.wordCount >= 50 && metadata.wordCount <= 500) {
strengths.push('Appropriate length - detailed but not overwhelming');
}
if (/^(you are|act as|pretend to be)/i.test(prompt)) {
strengths.push('Uses role-based framing');
}
if (/output|response|format|result/i.test(prompt)) {
strengths.push('Specifies expected output format');
}
return strengths.length > 0 ? strengths : ['Prompt is functional and clear'];
}
function identifyWeaknesses(prompt: string, metadata: AnalysisResult['metadata']): string[] {
const weaknesses: string[] = [];
if (!metadata.hasStructure && metadata.wordCount > 100) {
weaknesses.push('Long prompt without clear structure');
}
if (!metadata.hasExamples && metadata.wordCount > 50) {
weaknesses.push('Could benefit from examples');
}
if (metadata.wordCount < 20) {
weaknesses.push('Very short - may lack necessary context');
}
if (!/[?]/.test(prompt) && !/^(create|generate|write|explain|list)/i.test(prompt)) {
weaknesses.push('Unclear what action is expected');
}
if ((prompt.match(/\n/g) || []).length === 0 && metadata.wordCount > 50) {
weaknesses.push('Dense single block - consider breaking into sections');
}
return weaknesses;
}
function performRuleBasedAnalysis(prompt: string, metadata: AnalysisResult['metadata']): AnalysisResult {
// Calculate scores based on heuristics
let clarityScore = 70;
let specificityScore = 60;
let structureScore = 50;
let actionabilityScore = 65;
// Clarity adjustments
const avgSentenceLength = prompt.length / (metadata.sentenceCount || 1);
if (avgSentenceLength < 100) clarityScore += 10;
if (avgSentenceLength > 200) clarityScore -= 15;
// Specificity adjustments
if (metadata.hasExamples) specificityScore += 15;
if (metadata.hasConstraints) specificityScore += 10;
// Structure adjustments
if (metadata.hasStructure) structureScore += 30;
if (metadata.wordCount > 100 && !metadata.hasStructure) structureScore -= 20;
// Actionability adjustments
if (/^(create|generate|write|analyze|explain|list|describe|build)/i.test(prompt)) {
actionabilityScore += 15;
}
if (/output|response|format|result/i.test(prompt)) {
actionabilityScore += 10;
}
// Clamp all scores
const clamp = (n: number) => Math.max(0, Math.min(100, n));
clarityScore = clamp(clarityScore);
specificityScore = clamp(specificityScore);
structureScore = clamp(structureScore);
actionabilityScore = clamp(actionabilityScore);
const overall = Math.round((clarityScore + specificityScore + structureScore + actionabilityScore) / 4);
// Generate suggestions
const suggestions: string[] = [];
if (structureScore < 60) suggestions.push('Add headers or bullet points to organize the prompt');
if (specificityScore < 60) suggestions.push('Include more specific details or examples');
if (actionabilityScore < 70) suggestions.push('Start with a clear action verb (Create, Generate, Write, etc.)');
if (clarityScore < 70) suggestions.push('Break long sentences into shorter, clearer ones');
if (!metadata.hasExamples) suggestions.push('Add an example of expected output');
return {
scores: {
overall,
clarity: clarityScore,
specificity: specificityScore,
structure: structureScore,
actionability: actionabilityScore,
},
suggestions: suggestions.slice(0, 5),
strengths: identifyStrengths(prompt, metadata),
weaknesses: identifyWeaknesses(prompt, metadata),
metadata,
};
}
export default analyzePrompt;