Browse documentation

AI Services

RankFlow AI — AI Evaluation: Quality Metrics, LLM-as-Judge & Improvement Loop

Version: 2.0.0

docs/specs/ai/ai-services-03-evaluation.md
On this page

Version: 2.0.0
Date: 2026-06-16
Scope: Quality metrics, automated evaluation, LLM-as-judge, human-in-the-loop, feedback loop, and continuous improvement
Target Audience: AI Engineers, Content Strategists, Quality Assurance Team
Service Path: src/server/services/ai/evaluation/


1. Quality Philosophy#

1.1 Quality is Not Optional#

Every piece of AI-generated content in RankFlow AI is evaluated before it reaches the customer. There are no exceptions. Quality evaluation is the gate between AI generation and customer-facing content.

1.2 Quality Dimensions#

Content quality is measured across 5 dimensions, each scored 0-100:

Dimension Weight What it Measures
Accuracy 25% Factual correctness, no hallucinations, no false claims
Relevance 20% Content matches the task, audience, and context
Voice Consistency 20% Matches the practice's brand voice (tone, formality, warmth)
Compliance 15% Passes medical/legal compliance checks
Engagement 10% Likely to engage the target audience (measured via heuristics)
Formatting 10% Correct structure, length, keywords, format

Overall Score: Weighted average of all dimensions. Pass threshold: 70/100.

1.3 Quality Tiers#

Score Tier Action
90-100 Excellent Auto-publish, no human review needed
80-89 Good Auto-publish, flag for periodic review
70-79 Acceptable Auto-publish for non-medical, queue for medical review
60-69 Marginal Queue for human review before publish
0-59 Poor Reject, trigger regeneration (max 3 attempts), then human review

2. Quality Metrics Framework#

2.1 Accuracy (25%)#

// src/server/services/ai/evaluation/metrics/accuracy.ts

interface AccuracyScore {
  score: number; // 0-100
  reasons: string[];
  checks: {
    noHallucinations: boolean; // No made-up facts
    noContradictions: boolean; // No internal contradictions
    factualCorrectness: boolean; // Facts match known data (if verifiable)
    noOffTopic: boolean; // Content stays on topic
    claimsSubstantiated: boolean; // Claims have evidence (if applicable)
  };
}

function evaluateAccuracy(content: string, context: EvaluationContext): AccuracyScore {
  const checks = {
    noHallucinations: !containsHallucinations(content),
    noContradictions: !containsContradictions(content),
    factualCorrectness: verifyFacts(content, context.practiceId),
    noOffTopic: !isOffTopic(content, context.taskType),
    claimsSubstantiated: checkSubstantiatedClaims(content, context.isMedical),
  };
  
  const score = calculateWeightedScore(checks, [0.3, 0.2, 0.2, 0.15, 0.15]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

// Hallucination detection:
// - Use LLM-as-judge to identify claims that seem fabricated
// - Cross-reference with practice data (services, locations, etc.)
// - Flag medical claims that are not in the practice's verified services
// - Check for "best in city" claims without evidence

2.2 Relevance (20%)#

// src/server/services/ai/evaluation/metrics/relevance.ts

interface RelevanceScore {
  score: number;
  reasons: string[];
  checks: {
    matchesTask: boolean; // Content matches the requested task type
    matchesAudience: boolean; // Appropriate for target audience
    matchesContext: boolean; // Includes relevant context (city, services, etc.)
    includesKeywords: boolean; // Includes target brand keywords
    appropriateLength: boolean; // Length matches platform requirements
  };
}

function evaluateRelevance(content: string, context: EvaluationContext): RelevanceScore {
  const checks = {
    matchesTask: checkTaskMatch(content, context.taskType),
    matchesAudience: checkAudienceMatch(content, context.targetAudience),
    matchesContext: checkContextInclusion(content, context.practiceId),
    includesKeywords: checkKeywordInclusion(content, context.brandKeywords),
    appropriateLength: checkLength(content, context.taskType),
  };
  
  const score = calculateWeightedScore(checks, [0.3, 0.25, 0.2, 0.15, 0.1]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

2.3 Voice Consistency (20%)#

// src/server/services/ai/evaluation/metrics/voice.ts

interface VoiceScore {
  score: number;
  reasons: string[];
  checks: {
    toneMatch: boolean; // Matches practice tone (warm, professional, etc.)
    formalityMatch: boolean; // Matches practice formality level
    warmthMatch: boolean; // Matches practice warmth level
    technicalityMatch: boolean; // Matches practice technicality level
    noGenericAI: boolean; // Doesn't feel like generic AI output
  };
}

function evaluateVoice(content: string, context: EvaluationContext): VoiceScore {
  const brandProfile = context.brandProfile;
  
  const checks = {
    toneMatch: checkToneMatch(content, brandProfile.tone),
    formalityMatch: checkFormalityMatch(content, brandProfile.formalityLevel),
    warmthMatch: checkWarmthMatch(content, brandProfile.warmthLevel),
    technicalityMatch: checkTechnicalityMatch(content, brandProfile.technicalityLevel),
    noGenericAI: !isGenericAI(content), // Heuristic: "We are a..." = generic
  };
  
  const score = calculateWeightedScore(checks, [0.3, 0.2, 0.2, 0.15, 0.15]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

// Generic AI detection heuristics:
// - Starts with "We are a [category] located in [city]" = generic
// - Uses "In today's world..." or "In conclusion..." = generic
// - No specific details about the practice = generic
// - Overly formal language without warmth = generic
// - No local context or cultural references = generic

2.4 Compliance (15%)#

// src/server/services/ai/evaluation/metrics/compliance.ts

interface ComplianceScore {
  score: number;
  reasons: string[];
  checks: {
    noBannedPhrases: boolean; // Doesn't contain banned phrases
    medicalDisclaimer: boolean; // Has medical disclaimer if needed
    noFalseClaims: boolean; // No unverified claims
    noSuperlatives: boolean; // No unsubstantiated superlatives
    noSensitiveTopics: boolean; // No sensitive topics without review
  };
}

function evaluateCompliance(content: string, context: EvaluationContext): ComplianceScore {
  const checks = {
    noBannedPhrases: !containsBannedPhrases(content),
    medicalDisclaimer: hasMedicalDisclaimer(content, context.isMedical),
    noFalseClaims: !containsFalseClaims(content),
    noSuperlatives: !containsUnsubstantiatedSuperlatives(content),
    noSensitiveTopics: !containsSensitiveTopics(content, context.isMedical),
  };
  
  const score = calculateWeightedScore(checks, [0.3, 0.25, 0.2, 0.15, 0.1]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

// Banned phrase detection (regex + keyword list):
const BANNED_PHRASES = [
  "guaranteed cure",
  "100% success",
  "permanent fix",
  "no side effects",
  "miracle treatment",
  "instant results",
  "never fails",
  "completely safe",
  "risk-free",
  "doctor recommended" // without attribution
];

// Medical disclaimer check:
// For medical content, must contain one of:
// - "Consult a [doctor/dentist] for personalized advice"
// - "This information is for educational purposes"
// - "Always seek professional medical advice"

2.5 Engagement (10%)#

// src/server/services/ai/evaluation/metrics/engagement.ts

interface EngagementScore {
  score: number;
  reasons: string[];
  checks: {
    hasHook: boolean; // Has an engaging opening
    hasCTA: boolean; // Has a clear call to action
    readable: boolean; // Flesch reading ease score > 60
    actionable: boolean; // Content is actionable (not just informational)
    emotionalConnection: boolean; // Creates emotional connection with audience
  };
}

function evaluateEngagement(content: string, context: EvaluationContext): EngagementScore {
  const checks = {
    hasHook: hasEngagingHook(content, context.taskType),
    hasCTA: hasCallToAction(content),
    readable: checkReadability(content) > 60,
    actionable: isActionable(content),
    emotionalConnection: hasEmotionalConnection(content, context.targetAudience),
  };
  
  const score = calculateWeightedScore(checks, [0.25, 0.25, 0.2, 0.15, 0.15]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

// Readability score (Flesch-Kincaid):
// > 90: Very easy (5th grade)
// 80-90: Easy (6th grade)
// 70-80: Fairly easy (7th grade)
// 60-70: Standard (8-9th grade)
// 50-60: Fairly difficult (10-12th grade)
// < 50: Difficult (college level)
// Target: 60-80 for most content

2.6 Formatting (10%)#

// src/server/services/ai/evaluation/metrics/formatting.ts

interface FormattingScore {
  score: number;
  reasons: string[];
  checks: {
    correctLength: boolean; // Within platform length limits
    correctStructure: boolean; // Has required sections (hook, body, CTA)
    includesKeywords: boolean; // Includes required brand keywords
    properFormatting: boolean; // No markdown errors, proper line breaks
    platformAppropriate: boolean; // Format matches platform (e.g., Instagram vs. LinkedIn)
  };
}

function evaluateFormatting(content: string, context: EvaluationContext): FormattingScore {
  const checks = {
    correctLength: checkLength(content, context.taskType, context.platform),
    correctStructure: checkStructure(content, context.taskType),
    includesKeywords: checkRequiredKeywords(content, context.brandKeywords),
    properFormatting: checkFormatting(content),
    platformAppropriate: checkPlatformFormat(content, context.platform),
  };
  
  const score = calculateWeightedScore(checks, [0.25, 0.25, 0.2, 0.15, 0.15]);
  
  return {
    score,
    reasons: generateReasons(checks),
    checks,
  };
}

// Length constraints by platform:
// GBP Post: 1500 chars max
// Instagram: 2200 chars max (but optimal: 125-150)
// Facebook: 63,206 chars max (but optimal: 40-80 words)
// LinkedIn: 3000 chars max (but optimal: 100-150 words)
// Twitter: 280 chars max
// Meta Description: 160 chars max
// Alt Text: 125 chars max

3. Automated Evaluation Pipeline#

3.1 Pipeline Flow#

┌─────────────────────────────────────────────────────────────┐
│  AI-Generated Content → Evaluation Pipeline                  │
│                                                              │
│  Step 1: Fast Heuristic Checks (local, no LLM)              │
│  ├─ Banned phrase scan (regex, < 1ms)                        │
│  ├─ Length validation (< 1ms)                                │
│  ├─ Keyword inclusion (< 1ms)                                │
│  ├─ Medical disclaimer check (if medical) (< 1ms)            │
│  └─ JSON validation (if JSON mode) (< 1ms)                  │
│  If any fail → REJECT immediately (no LLM cost)            │
│                                                              │
│  Step 2: LLM-as-Judge Evaluation (expensive but thorough)    │
│  ├─ Accuracy evaluation (Claude Haiku, ~$0.002)              │
│  ├─ Relevance evaluation (Claude Haiku, ~$0.002)              │
│  ├─ Voice consistency evaluation (Claude Haiku, ~$0.002)     │
│  ├─ Engagement evaluation (Claude Haiku, ~$0.002)             │
│  └─ Formatting evaluation (Claude Haiku, ~$0.002)             │
│  → Combined score (0-100)                                    │
│                                                              │
│  Step 3: Compliance Check (local + LLM)                      │
│  ├─ Banned phrases (already checked)                         │
│  ├─ Medical claims verification (Claude Haiku, ~$0.002)    │
│  └─ False claims detection (Claude Haiku, ~$0.002)           │
│                                                              │
│  Step 4: Decision                                             │
│  ├─ Score ≥ 90 → AUTO-PUBLISH                               │
│  ├─ Score 70-89 → AUTO-PUBLISH (non-medical) / QUEUE (medical)│
│  ├─ Score 60-69 → QUEUE for human review                      │
│  └─ Score < 60 → REJECT + REGENERATE (max 3 attempts)       │
│                                                              │
│  Step 5: Audit & Logging                                      │
│  ├─ Log quality score to `ai_usage` table                    │
│  ├─ Log evaluation details to `ai_evaluation` table          │
│  └─ Update quality metrics dashboard                         │
└─────────────────────────────────────────────────────────────┘

3.2 Evaluation Cost#

Step Cost Speed Coverage
Fast Heuristics $0.00 < 5ms 100% of content
LLM-as-Judge (5 dims) ~$0.01 ~500ms 100% of content
Compliance Check ~$0.004 ~300ms 100% of content
Total per generation ~$0.014 ~800ms Full evaluation

Total evaluation cost per generation: ~$0.014 (about 10% of generation cost).

3.3 Evaluation Caching#

// If the exact same content was evaluated before, serve cached score
// Cache key: hash(content + task + practiceId)
// Cache TTL: 24 hours (content doesn't change, but standards might)
// Cache hit rate: ~30% (many clients share similar content structures)

4. LLM-as-Judge#

4.1 What is LLM-as-Judge?#

Instead of writing complex rule-based evaluation code, we use a separate LLM (Claude Haiku — fast and cheap) to evaluate content quality. The judge LLM receives:

  • The generated content
  • The original prompt/context
  • The quality criteria
  • The brand voice profile

And returns a structured evaluation score.

4.2 Judge Prompt Template#

You are a content quality evaluator. Evaluate the following content based on the criteria below.

## Content to Evaluate
"""{{content}}"""

## Context
- Task: {{taskType}}
- Practice: {{practiceName}} ({{category}} in {{city}})
- Audience: {{targetAudience}}
- Brand Voice: {{tone}}, {{formalityLevel}}, {{warmthLevel}}
- Medical Content: {{isMedical}}

## Evaluation Criteria

### 1. Accuracy (0-100)
- Does the content contain factual errors or hallucinations?
- Are there any contradictions?
- Are claims substantiated?

### 2. Relevance (0-100)
- Does the content match the task and audience?
- Does it include appropriate context (city, services)?
- Does it include brand keywords?

### 3. Voice Consistency (0-100)
- Does the tone match the brand voice ({{tone}})?
- Does it feel personalized or generic?
- Does it avoid AI-sounding phrases?

### 4. Compliance (0-100)
- Does it contain banned phrases (guaranteed cure, 100% success, etc.)?
- Does it have medical disclaimers if needed?
- Are superlatives substantiated?

### 5. Engagement (0-100)
- Is the hook engaging?
- Is there a clear call to action?
- Is it readable and actionable?

### 6. Formatting (0-100)
- Is the length appropriate for the platform?
- Is the structure correct?
- Are keywords included?

## Output Format
Return JSON:
{
  "accuracy": { "score": 0-100, "reason": "..." },
  "relevance": { "score": 0-100, "reason": "..." },
  "voice": { "score": 0-100, "reason": "..." },
  "compliance": { "score": 0-100, "reason": "..." },
  "engagement": { "score": 0-100, "reason": "..." },
  "formatting": { "score": 0-100, "reason": "..." },
  "overall": 0-100,
  "pass": true/false,
  "issues": ["issue1", "issue2"]
}

4.3 Judge Configuration#

// The judge LLM is always Claude Haiku (fast, cheap, reliable)
const JUDGE_CONFIG = {
  model: "claude-haiku",
  temperature: 0.0, // Deterministic evaluation
  maxTokens: 1024,
  jsonMode: true,
};

// Evaluation cost: ~$0.002 per dimension (6 dimensions = ~$0.012 total)
// Can be optimized by evaluating all dimensions in a single prompt (~$0.005)

4.4 Judge Calibration#

// To ensure the judge is consistent, we calibrate it against human ratings:

// 1. Collect 100 pieces of content with human ratings
// 2. Run the judge on the same content
// 3. Calculate correlation between human and judge scores
// 4. If correlation < 0.8, adjust the judge prompt or model
// 5. Re-calibrate monthly with new human-rated samples

// Calibration dataset:
// - 100 content pieces (mix of tasks: GBP, social, landing page, blog)
// - 3 human raters per piece (average score)
// - Judge scores per piece
// - Target: Pearson correlation > 0.85 between human and judge

5. Human-in-the-Loop#

5.1 When Human Review is Required#

Scenario Auto-Action Human Review? SLA
Score 90-100 Publish No
Score 80-89 Publish Optional (flagged)
Score 70-79 (non-medical) Publish No
Score 70-79 (medical) Queue Yes (required) 24 hours
Score 60-69 Queue Yes (required) 24 hours
Score < 60 Reject + Regenerate Yes (if 3 failures) 48 hours
Compliance FAIL Reject Yes (always) 24 hours
Banned phrase detected Reject Yes (always) 24 hours

5.2 Human Review Queue#

// Content items queued for human review go to the `content_review_queue` table
// Admin reviews them in the `/admin/ai-evaluation` dashboard

interface ContentReviewItem {
  id: string;
  content: string;
  taskType: TaskType;
  practiceId: string;
  practiceName: string;
  
  // Generated metadata
  generatedAt: Date;
  model: string;
  qualityScore: number;
  
  // Evaluation results
  evaluation: {
    accuracy: number;
    relevance: number;
    voice: number;
    compliance: number;
    engagement: number;
    formatting: number;
    issues: string[];
  };
  
  // Review status
  status: "pending" | "approved" | "rejected" | "edited";
  reviewedBy: string | null; // User ID
  reviewedAt: Date | null;
  
  // Feedback
  humanScore: number | null; // 0-100
  humanFeedback: string | null;
  editsMade: string | null; // If edited, what was changed
  
  // SLA
  slaDeadline: Date; // 24 hours from generation
  slaBreached: boolean;
}

5.3 Human Review UI#

┌─────────────────────────────────────────────────────────────┐
│  Admin Dashboard > AI Evaluation > Content Review Queue    │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Pending Reviews: 12 (3 SLA Breached)               │   │
│  │  [Filter: All | Medical | Non-Medical | SLA Breached]│  │
│  │                                                      │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │  GBP Post — Dr. Smith's Dental Clinic        │   │   │
│  │  │  Generated: 2026-06-16 09:30 (3 hours ago)  │   │   │
│  │  │  Model: claude-haiku | Quality: 65/100       │   │   │
│  │  │  Issues: Low engagement, missing CTA          │   │   │
│  │  │                                              │   │   │
│  │  │  "🦷 Regular dental checkups are important..."│   │   │
│  │  │  (1500 chars, full text in preview)           │   │   │
│  │  │                                              │   │   │
│  │  │  [Approve] [Edit] [Reject] [Regenerate]     │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  │                                                      │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │  Social Post — MediCare Hospital              │   │   │
│  │  │  Generated: 2026-06-16 08:00 (5 hours ago)  │   │   │
│  │  │  Model: gpt-4o-mini | Quality: 72/100        │   │   │
│  │  │  Issues: Compliance warning (medical claim)  │   │   │
│  │  │  SLA: 24 hours (expires in 19 hours)          │   │   │
│  │  │  [Approve] [Edit] [Reject] [Regenerate]     │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

5.4 Human Feedback Loop#

// When a human reviews content, their feedback is used to improve the system:

// 1. Human rates the content (0-100) and provides feedback
// 2. System compares human rating to automated score
// 3. If difference > 15 points: flag for judge calibration
// 4. Human feedback is stored in `ai_feedback` table
// 5. Feedback is used to:
//    - Improve the judge prompt (adjust criteria weights)
//    - Update the practice's brand voice profile
//    - Improve the prompt template for that task
//    - Train future prompt improvements

interface AIFeedback {
  id: string;
  contentId: string;
  practiceId: string;
  taskType: string;
  
  // Human feedback
  humanScore: number;
  humanFeedback: string;
  humanIssues: string[];
  
  // Automated scores (for comparison)
  automatedScore: number;
  automatedBreakdown: Record<string, number>;
  
  // Action taken
  action: "approved" | "rejected" | "edited" | "regenerated";
  editedContent: string | null;
  
  // Learning
  usedForTraining: boolean;
  usedForCalibration: boolean;
  
  createdAt: Date;
}

6. Feedback Loop & Improvement#

6.1 Continuous Improvement Cycle#

┌─────────────────────────────────────────────────────────────┐
│  CONTINUOUS IMPROVEMENT CYCLE                               │
│                                                              │
│  1. GENERATE → AI generates content using current prompt    │
│                                                              │
│  2. EVALUATE → Automated evaluation + human review         │
│                                                              │
│  3. FEEDBACK → Humans rate and provide feedback            │
│                                                              │
│  4. ANALYZE → System analyzes patterns in feedback          │
│     - Common issues per task type                           │
│     - Common issues per practice                            │
│     - Judge calibration gaps                                │
│     - Prompt effectiveness                                  │
│                                                              │
│  5. IMPROVE → Generate prompt improvement suggestions        │
│     - "Add CTA to GBP post template"                        │
│     - "Reduce technicality for pediatric clients"         │
│     - "Improve medical disclaimer placement"                │
│                                                              │
│  6. TEST → A/B test the improved prompt                     │
│                                                              │
│  7. DEPLOY → Promote winning prompt to active               │
│                                                              │
│  8. REPEAT → Cycle continues                                │
└─────────────────────────────────────────────────────────────┘

6.2 Weekly Quality Report#

Every week, the system generates an automated quality report:

┌─────────────────────────────────────────────────────────────┐
│  Weekly AI Quality Report (2026-06-09 to 2026-06-16)       │
│                                                              │
│  GENERATIONS                                                │
│  Total: 1,234 | Approved: 1,180 (95.6%) | Rejected: 54    │
│                                                              │
│  QUALITY SCORES                                             │
│  Overall: 78.5/100 (▼ 1.2 from last week)                   │
│  ├─ Accuracy: 82.3 (▼ 0.5)                                  │
│  ├─ Relevance: 80.1 (▼ 2.1) ⚠️                              │
│  ├─ Voice: 76.8 (▼ 1.5) ⚠️                                │
│  ├─ Compliance: 95.2 (▲ 0.8) ✅                           │
│  ├─ Engagement: 71.4 (▼ 3.2) ⚠️⚠️                        │
│  └─ Formatting: 85.6 (▲ 1.0) ✅                           │
│                                                              │
│  TOP ISSUES (This Week)                                     │
│  1. Missing CTA in 23% of social posts → Suggest adding    │
│     CTA to social_caption template                          │
│  2. Voice mismatch in 15% of GBP posts → Calibrate Meta    │
│     Prompt for 3 practices                                   │
│  3. Engagement score dropped → Review hashtag strategy     │
│  4. 2 compliance failures → Update banned phrase list      │
│                                                              │
│  JUDGE CALIBRATION                                          │
│  Human-Judge correlation: 0.84 (▼ 0.03) → Needs recal      │
│                                                              │
│  PROMPT IMPROVEMENTS                                        │
│  ├─ gbp_post v2.1 → v2.2 (A/B test: +4.2% engagement)     │
│  ├─ social_caption v1.3 → v1.4 (A/B test: +2.1% voice)    │
│  └─ blog_post v2.2 → v3.0 (A/B test: +5.8% accuracy)      │
│                                                              │
│  COST                                                       │
│  Total AI cost: $892.50 | Eval cost: $89.25 (10%)           │
│  Cost per generation: $0.72 | Target: $0.70                  │
│                                                              │
│  RECOMMENDATIONS                                            │
│  1. Add CTA requirement to social_caption template          │
│  2. Recalibrate judge for engagement scoring               │
│  3. Review 3 practices with low voice scores                 │
│  4. Consider downgrading blog_post model to reduce cost    │
└─────────────────────────────────────────────────────────────┘

6.3 Automated Prompt Improvement Suggestions#

The system can suggest prompt improvements based on feedback patterns:

// Example: If 30% of social posts are missing CTAs
// System suggests:
const suggestion = {
  taskType: "social_caption",
  issue: "Missing call to action in 30% of posts",
  currentPrompt: "Write a social media post about {{topic}}.",
  suggestedPrompt: "Write a social media post about {{topic}}. IMPORTANT: Always end with a clear call to action (e.g., 'Book now', 'Visit us', 'Call {{phone}}').",
  expectedImprovement: "+15% engagement score",
  confidence: 0.85,
  aBTestRecommended: true,
};

// Admin can approve the suggestion, which triggers an A/B test

7. Quality Score Dashboard#

7.1 Admin Dashboard: AI Quality Metrics#

┌─────────────────────────────────────────────────────────────┐
│  Admin Dashboard > AI Quality Metrics                        │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  REAL-TIME METRICS (10s refresh)                    │   │
│  │                                                      │   │
│  │  Generations/Min: 45    Avg Latency: 1200ms         │   │
│  │  Avg Quality Score: 79.2/100                        │   │
│  │  Rejection Rate: 4.2%                               │   │
│  │  Compliance Rate: 98.8%                             │   │
│  │                                                      │   │
│  │  [Quality Score] ████████░░ 79.2                    │   │
│  │  [Rejection Rate] ██░░░░░░░░ 4.2%                  │   │
│  │  [Compliance] ██████████░ 98.8%                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  QUALITY BY TASK (Last 7 Days)                       │   │
│  │                                                      │   │
│  │  Task           │ Score │ Reject │ Compliance │ Cost │   │
│  │  ───────────────┼───────┼────────┼────────────┼──────│   │
│  │  gbp_post       │ 82.1  │ 3.1%   │ 99.2%      │ $0.003│  │
│  │  social_caption │ 76.5  │ 5.8%   │ 98.5%      │ $0.001│  │
│  │  landing_page   │ 85.3  │ 2.1%   │ 100%       │ $0.015│  │
│  │  blog_post      │ 80.2  │ 4.5%   │ 99.0%      │ $0.012│  │
│  │  review_reply   │ 88.7  │ 1.2%   │ 99.8%      │ $0.002│  │
│  │  schema_markup  │ 91.2  │ 0.5%   │ 100%       │ $0.008│  │
│  │  translation    │ 78.1  │ 6.2%   │ 97.5%      │ $0.008│  │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  QUALITY BY PRACTICE (Top 10 Lowest Scores)        │   │
│  │                                                      │   │
│  │  Practice              │ Score │ Main Issue         │   │
│  │  ──────────────────────┼───────┼────────────────────│   │
│  │  Dr. Smith's Dental   │ 65.2  │ Voice mismatch     │   │
│  │  MediCare Hospital     │ 68.1  │ Missing CTAs       │   │
│  │  Radiant Smiles       │ 70.3  │ Low engagement     │   │
│  │  ...                    │ ...   │ ...               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  QUALITY TREND (30 Days)                             │   │
│  │                                                      │   │
│  │  [Line chart: Overall, Accuracy, Relevance, Voice,     │   │
│  │   Compliance, Engagement over 30 days]              │   │
│  │                                                      │   │
│  │  Trend: Overall score stable at 78-80. Engagement     │   │
│  │  dropped 3 points this week. Action recommended.    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  CONTENT REVIEW QUEUE                                │   │
│  │  Pending: 12 | SLA Breached: 3 | Medical: 5          │   │
│  │  [View Queue]                                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  JUDGE CALIBRATION STATUS                            │   │
│  │  Human-Judge Correlation: 0.84 (Target: 0.85)       │   │
│  │  Last Calibrated: 2026-06-01 (15 days ago)            │   │
│  │  [Recalibrate Now]                                   │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

8. Content Approval Workflow#

8.1 Approval States#

┌──────────┐     ┌──────────────┐     ┌───────────┐     ┌──────────┐
│  DRAFT   │────→│ PENDING_REVIEW│────→│ APPROVED  │────→│ PUBLISHED│
│(AI gen)  │     │(24h auto)     │     │(human ok) │     │(live)    │
└──────────┘     └──────────────┘     └───────────┘     └──────────┘
     │                │     │               │
     │                │     │               │
     ▼                ▼     ▼               ▼
  ┌─────────┐   ┌──────────┐          ┌──────────┐
  │ REJECTED│   │ AUTO-    │          │  EDITED   │
  │ (regen) │   │ PUBLISHED│          │ (requeue) │
  └─────────┘   │(non-med) │          └──────────┘
                └──────────┘

8.2 Approval Rules by Content Type#

Content Type Auto-Publish Threshold Medical Review? SLA
GBP Post Score ≥ 70 Yes (if medical) 24h
Social Post Score ≥ 70 Yes (if medical) 24h
Landing Page Score ≥ 80 Always 24h
Blog Post Score ≥ 75 Yes (if medical) 24h
Review Reply Score ≥ 70 No None
Citation Desc Score ≥ 60 No None
Schema Markup Score ≥ 80 Always 24h
Meta Desc Score ≥ 60 No None
Alt Text Score ≥ 60 No None
Translation Score ≥ 70 Yes (if medical) 24h
Image Prompt Score ≥ 60 No None
Report Summary Score ≥ 75 No None

8.3 Medical Content Approval (24h Queue)#

// Medical content has a mandatory 24-hour approval queue:
// 1. AI generates content
// 2. Content is evaluated (must score ≥ 70)
// 3. Content is placed in `content_review_queue` with 24h SLA
// 4. Admin receives notification (email + Slack)
// 5. Admin reviews and approves/rejects within 24h
// 6. If admin doesn't respond within 24h:
//    - If score ≥ 80: Auto-publish (content is good enough)
//    - If score < 80: Auto-reject + alert admin (needs attention)
// 7. If rejected: Content is returned to draft state with feedback
// 8. If approved: Content moves to PUBLISHED state
// 9. If edited: Content is updated and re-evaluated

// Medical content is defined as:
// - isMedical = true (from Practice table)
// - OR content contains medical keywords (treatment, diagnosis, medication, surgery, etc.)
// - OR content is for a practice in categories: CLINIC, HOSPITAL, DOCTOR, DENTAL, etc.

9. Evaluation Database Schema#

-- AI Evaluation Results (per generation)
CREATE TABLE ai_evaluations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  usage_id UUID NOT NULL REFERENCES ai_usage(id) ON DELETE CASCADE,
  practice_id UUID NOT NULL REFERENCES practices(id) ON DELETE CASCADE,
  task_type VARCHAR(50) NOT NULL,
  
  -- Scores (0-100)
  overall_score INTEGER NOT NULL,
  accuracy_score INTEGER,
  relevance_score INTEGER,
  voice_score INTEGER,
  compliance_score INTEGER,
  engagement_score INTEGER,
  formatting_score INTEGER,
  
  -- Pass/Fail
  passed BOOLEAN NOT NULL DEFAULT FALSE,
  
  -- Evaluation details (JSON)
  evaluation_details JSONB NOT NULL,
  -- {
  --   "accuracy": { "score": 82, "reason": "No hallucinations detected" },
  --   "relevance": { "score": 75, "reason": "Missing city context" },
  --   ...
  --   "issues": ["Missing CTA", "Too technical for audience"]
  -- }
  
  -- Judge info
  judge_model VARCHAR(50) NOT NULL DEFAULT "claude-haiku",
  judge_latency_ms INTEGER,
  judge_cost_usd DECIMAL(10, 6),
  
  created_at TIMESTAMP DEFAULT NOW()
);

-- Content Review Queue
CREATE TABLE content_review_queue (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content_id UUID NOT NULL, -- References the content being reviewed
  content_type VARCHAR(50) NOT NULL, -- gbp_post, social_post, landing_page, etc.
  content_preview TEXT NOT NULL,
  
  practice_id UUID NOT NULL REFERENCES practices(id) ON DELETE CASCADE,
  task_type VARCHAR(50) NOT NULL,
  
  -- Generation metadata
  generated_at TIMESTAMP NOT NULL,
  model VARCHAR(50) NOT NULL,
  quality_score INTEGER NOT NULL,
  evaluation_details JSONB,
  
  -- Review status
  status VARCHAR(20) NOT NULL DEFAULT "pending", -- pending, approved, rejected, edited, auto_published, auto_rejected
  reviewed_by UUID REFERENCES users(id),
  reviewed_at TIMESTAMP,
  
  -- Human feedback
  human_score INTEGER,
  human_feedback TEXT,
  edited_content TEXT,
  
  -- SLA
  sla_deadline TIMESTAMP NOT NULL,
  sla_breached BOOLEAN DEFAULT FALSE,
  
  -- Auto-publish on SLA breach
  auto_publish_on_breach BOOLEAN DEFAULT FALSE, -- Set based on score (score >= 80 = true)
  
  created_at TIMESTAMP DEFAULT NOW()
);

-- AI Feedback (human ratings for improvement)
CREATE TABLE ai_feedback (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  usage_id UUID NOT NULL REFERENCES ai_usage(id) ON DELETE CASCADE,
  practice_id UUID NOT NULL REFERENCES practices(id) ON DELETE CASCADE,
  task_type VARCHAR(50) NOT NULL,
  
  -- Human feedback
  human_score INTEGER NOT NULL,
  human_feedback TEXT,
  human_issues TEXT[], -- JSON array
  
  -- Automated scores (for comparison)
  automated_score INTEGER NOT NULL,
  automated_breakdown JSONB,
  
  -- Action
  action VARCHAR(20) NOT NULL, -- approved, rejected, edited, regenerated
  edited_content TEXT,
  
  -- Learning flags
  used_for_training BOOLEAN DEFAULT FALSE,
  used_for_calibration BOOLEAN DEFAULT FALSE,
  
  created_at TIMESTAMP DEFAULT NOW()
);

-- Quality Metrics (aggregated, for dashboard)
CREATE TABLE ai_quality_metrics (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  date DATE NOT NULL,
  practice_id UUID REFERENCES practices(id) ON DELETE CASCADE, -- NULL = system-wide
  task_type VARCHAR(50), -- NULL = all tasks
  
  -- Aggregated metrics
  total_generations INTEGER NOT NULL DEFAULT 0,
  approved_count INTEGER NOT NULL DEFAULT 0,
  rejected_count INTEGER NOT NULL DEFAULT 0,
  pending_count INTEGER NOT NULL DEFAULT 0,
  
  avg_quality_score DECIMAL(5, 2),
  avg_accuracy_score DECIMAL(5, 2),
  avg_relevance_score DECIMAL(5, 2),
  avg_voice_score DECIMAL(5, 2),
  avg_compliance_score DECIMAL(5, 2),
  avg_engagement_score DECIMAL(5, 2),
  avg_formatting_score DECIMAL(5, 2),
  
  avg_cost_usd DECIMAL(10, 6),
  avg_latency_ms INTEGER,
  
  UNIQUE(date, practice_id, task_type)
);

End of AI Evaluation Documentation — RankFlow AI v2.0.0