--- name: perplexity-policy-guardrails description: 'Implement content moderation, model selection policy, citation quality enforcement, and per-user usage quotas for Perplexity Sonar API. Trigger with phrases like "perplexity policy", "perplexity guardrails", "perplexity content moderation", "perplexity usage limits", "perplexity safety". ' allowed-tools: Read, Write, Edit, Bash(npx:*) version: 1.12.0 license: MIT author: Jeremy Longshore tags: - saas - perplexity - perplexity-policy compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw --- # Perplexity Policy Guardrails ## Overview Policy enforcement for Perplexity Sonar API. Since Perplexity performs live web searches, guardrails must address: query content moderation (what users can search for), citation reliability (filtering low-quality sources), cost control (model selection + token limits), and responsible AI usage. ## Policy Pipeline ``` User Query │ ▼ Query Moderation (block harmful queries) │ ▼ PII Sanitization (strip personal data) │ ▼ Quota Check (daily limit by user tier) │ ▼ Model Selection (enforce tier-appropriate model) │ ▼ Perplexity API Call │ ▼ Citation Quality Scoring (filter low-trust sources) │ ▼ Response to User ``` ## Prerequisites - Perplexity API configured - Content moderation policy defined - User tier system in place - Redis for quota tracking (optional: in-memory for simple apps) ## Instructions ### Step 1: Query Content Moderation ```typescript const BLOCKED_PATTERNS = [ /\b(write|generate|create)\s+(malware|virus|exploit|ransomware)\b/i, /\b(personal|private)\s+(address|phone|ssn)\s+of\s+\w+/i, /\b(bypass|circumvent|hack)\s+(security|firewall|authentication)\b/i, /\b(how to|tutorial)\s+(stalk|dox|harass)\b/i, ]; const MAX_QUERY_LENGTH = 2000; class PolicyError extends Error { constructor(public code: string, message: string) { super(message); this.name = "PolicyError"; } } function moderateQuery(query: string): string { if (query.length > MAX_QUERY_LENGTH) { throw new PolicyError("QUERY_TOO_LONG", `Query exceeds ${MAX_QUERY_LENGTH} characters`); } for (const pattern of BLOCKED_PATTERNS) { if (pattern.test(query)) { throw new PolicyError("CONTENT_BLOCKED", "Query blocked by content policy"); } } return query; } ``` ### Step 2: Model Selection Policy ```typescript interface ModelPolicy { model: string; maxTokens: number; costPerRequest: number; } const MODEL_POLICIES: Record = { free: { model: "sonar", maxTokens: 256, costPerRequest: 0.005 }, basic: { model: "sonar", maxTokens: 1024, costPerRequest: 0.005 }, pro: { model: "sonar-pro", maxTokens: 2048, costPerRequest: 0.02 }, enterprise: { model: "sonar-pro", maxTokens: 4096, costPerRequest: 0.02 }, }; function enforceModelPolicy( userTier: string, requestedModel?: string ): ModelPolicy { const policy = MODEL_POLICIES[userTier] || MODEL_POLICIES.free; // Prevent free users from using expensive models if (requestedModel === "sonar-pro" && !["pro", "enterprise"].includes(userTier)) { console.warn(`User tier ${userTier} not allowed sonar-pro, using sonar`); return MODEL_POLICIES.free; } return requestedModel ? { ...policy, model: requestedModel } : policy; } ``` ### Step 3: Per-User Usage Quotas ```typescript class UsageQuota { private usage: Map = new Map(); private readonly limits: Record = { free: 50, basic: 200, pro: 1000, enterprise: 5000, }; check(userId: string, tier: string = "free"): void { const key = `${userId}:${new Date().toISOString().slice(0, 10)}`; const entry = this.usage.get(key) || { count: 0, resetAt: this.endOfDay() }; // Reset if past end of day if (Date.now() > entry.resetAt) { entry.count = 0; entry.resetAt = this.endOfDay(); } const limit = this.limits[tier] || this.limits.free; if (entry.count >= limit) { throw new PolicyError( "QUOTA_EXCEEDED", `Daily quota exceeded (${entry.count}/${limit}). Resets at midnight UTC.` ); } entry.count++; this.usage.set(key, entry); } getUsage(userId: string): { used: number; limit: number; remaining: number } { const key = `${userId}:${new Date().toISOString().slice(0, 10)}`; const entry = this.usage.get(key); const used = entry?.count || 0; return { used, limit: 50, remaining: Math.max(0, 50 - used) }; } private endOfDay(): number { const d = new Date(); d.setUTCHours(23, 59, 59, 999); return d.getTime(); } } ``` ### Step 4: Citation Quality Scoring ```typescript const TRUSTED_TLDS = new Set(["gov", "edu", "org"]); const HIGH_QUALITY_DOMAINS = new Set([ "nature.com", "science.org", "arxiv.org", "wikipedia.org", "nih.gov", "cdc.gov", "who.int", ]); const LOW_QUALITY_DOMAINS = new Set([ "reddit.com", "quora.com", "medium.com", "yahoo.com", ]); interface CitationQuality { url: string; trust: "high" | "medium" | "low"; domain: string; } function scoreCitations(citations: string[]): { scored: CitationQuality[]; highTrustPercent: number; } { const scored = citations.map((url) => { const domain = new URL(url).hostname; const tld = domain.split(".").pop() || ""; let trust: "high" | "medium" | "low" = "medium"; if (TRUSTED_TLDS.has(tld) || HIGH_QUALITY_DOMAINS.has(domain)) { trust = "high"; } else if (LOW_QUALITY_DOMAINS.has(domain)) { trust = "low"; } return { url, trust, domain }; }); const highTrust = scored.filter((s) => s.trust === "high").length; return { scored, highTrustPercent: citations.length > 0 ? highTrust / citations.length : 0, }; } ``` ### Step 5: Full Policy Pipeline ```typescript const quota = new UsageQuota(); async function policiedSearch( query: string, userId: string, userTier: string = "free", requestedModel?: string ) { // 1. Content moderation const moderated = moderateQuery(query); // 2. PII sanitization const { clean } = sanitizeQuery(moderated); // 3. Quota check quota.check(userId, userTier); // 4. Model policy const policy = enforceModelPolicy(userTier, requestedModel); // 5. API call const response = await perplexity.chat.completions.create({ model: policy.model, messages: [{ role: "user", content: clean }], max_tokens: policy.maxTokens, }); // 6. Citation quality const citations = (response as any).citations || []; const quality = scoreCitations(citations); return { answer: response.choices[0].message.content, citations: quality.scored, citationQuality: quality.highTrustPercent, model: response.model, tokens: response.usage?.total_tokens, }; } ``` ## Error Handling | Issue | Cause | Solution | |-------|-------|----------| | Query blocked | Content moderation triggered | Review patterns, adjust if false positive | | Quota exceeded | User hit daily limit | Upgrade tier or wait for reset | | Model downgraded | User tier restricts access | Inform user of tier limitations | | Low citation quality | All sources from forums | Add `search_domain_filter` for trusted sources | ## Output - Query content moderation with blocked patterns - Model selection enforced by user tier - Per-user daily quotas - Citation quality scoring and filtering - Full policy pipeline combining all layers ## Resources - [Perplexity API Documentation](https://docs.perplexity.ai) - [Perplexity Responsible Use](https://www.perplexity.ai/hub) ## Next Steps For architecture patterns, see `perplexity-architecture-variants`.