Backend: thread on_fallback callback through analysis chain (gemini_service → agents → analysis_service → handlers). The handler sends a 'model_fallback' WebSocket message exactly once per analysis when the primary model is unavailable. Frontend: handle 'model_fallback' WS message and show a dismissible yellow toast at the bottom of the screen with an 8-second auto-dismiss. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
9.6 KiB
Python
Executable file
178 lines
9.6 KiB
Python
Executable file
from typing import List, Optional, Tuple
|
|
|
|
from app.agents.base_agent import BaseAgent
|
|
from app.models.schemas import PreviousReviewContext, SubReview
|
|
from app.services.gemini_service import GeminiService
|
|
from app.services.reference_docs import ReferenceDocsService
|
|
|
|
|
|
class BrandAgent(BaseAgent):
|
|
"""Brand Agent - analyzes proofs against Barclays brand guidelines using Gemini."""
|
|
|
|
name = "Brand Agent"
|
|
|
|
def __init__(self, gemini_service: GeminiService, reference_docs: ReferenceDocsService):
|
|
"""
|
|
Initialize the Brand Agent.
|
|
|
|
Args:
|
|
gemini_service: Service for making Gemini API calls
|
|
reference_docs: Service for loading reference documents
|
|
"""
|
|
self.gemini = gemini_service
|
|
self.reference_docs = reference_docs
|
|
|
|
def _get_brand_context(self, brand: str) -> str:
|
|
"""
|
|
Get the appropriate brand specification based on the brand selection.
|
|
|
|
Args:
|
|
brand: Brand name ('Barclays' or 'Barclaycard')
|
|
|
|
Returns:
|
|
Brand specification content
|
|
"""
|
|
if brand == "Barclays":
|
|
return self.reference_docs.get_barclays_brand_spec()
|
|
else:
|
|
# Default to Barclaycard
|
|
return self.reference_docs.get_barclaycard_brand_spec()
|
|
|
|
def _build_revision_context(self, previous_review: PreviousReviewContext) -> str:
|
|
"""Build prompt section for revision-aware analysis."""
|
|
issues_list = "\n".join(f" - {issue}" for issue in previous_review.issues) if previous_review.issues else " (No issues)"
|
|
return f"""
|
|
---
|
|
|
|
**REVISION CONTEXT**
|
|
|
|
This is a revision of a previously reviewed proof. The previous version (Version {previous_review.version}) had the following brand review:
|
|
|
|
- RAG Status: {previous_review.ragStatus}
|
|
- Feedback: {previous_review.feedback}
|
|
- Issues identified:
|
|
{issues_list}
|
|
|
|
When analyzing this revision, you MUST:
|
|
1. Compare against the previous issues and determine which have been RESOLVED
|
|
2. Identify which previous issues are still OUTSTANDING (not fixed)
|
|
3. Identify any NEW issues introduced in this revision
|
|
|
|
Your response MUST include:
|
|
- resolvedIssues: Array of issues from the previous version that have been fixed
|
|
- outstandingIssues: Array of issues from the previous version that remain unfixed
|
|
- newIssues: Array of new issues not present in the previous version
|
|
|
|
---
|
|
"""
|
|
|
|
async def analyze(
|
|
self,
|
|
images: List[Tuple[bytes, str]],
|
|
previous_review: Optional[PreviousReviewContext] = None,
|
|
brand: str = "Barclaycard",
|
|
channel: Optional[str] = None,
|
|
sub_channel: Optional[str] = None,
|
|
proof_type: Optional[str] = None,
|
|
on_fallback=None,
|
|
) -> SubReview:
|
|
"""
|
|
Analyze the proof for brand guideline adherence.
|
|
|
|
Args:
|
|
images: List of (file_data, mime_type) tuples representing the proof
|
|
previous_review: Optional context from previous version for revision-aware analysis
|
|
brand: Brand to analyze against ('Barclays' or 'Barclaycard')
|
|
channel: Target channel (e.g. "Social", "Digital")
|
|
sub_channel: Target sub-channel (e.g. "Meta", "Google")
|
|
proof_type: Proof format type (e.g. "In-feed 1x1", "Banner")
|
|
|
|
Returns:
|
|
SubReview with brand compliance assessment
|
|
"""
|
|
# Get the appropriate brand specification
|
|
brand_context = self._get_brand_context(brand)
|
|
|
|
# Build revision context if available
|
|
revision_context = ""
|
|
if previous_review:
|
|
revision_context = self._build_revision_context(previous_review)
|
|
|
|
prompt = f"""You are a brand expert for {brand}. Your role is to analyze marketing proofs for adherence to {brand} brand guidelines.
|
|
|
|
Here is the {brand} brand specification to use for your analysis:
|
|
|
|
{brand_context}
|
|
{revision_context}
|
|
---
|
|
|
|
**PROOF METADATA**
|
|
- Channel: {channel or "Not specified"}
|
|
- Sub-Channel: {sub_channel or "Not specified"}
|
|
- Proof Type: {proof_type or "Not specified"}
|
|
|
|
Use this metadata to focus your analysis on the specific brand requirements for this channel and format.
|
|
---
|
|
|
|
Analyze the uploaded proof against the {brand} brand specification above, checking for:
|
|
|
|
1. **Logo Usage**: Is the {brand} logo used correctly? Check minimum size, clear space, placement, and that it hasn't been altered. Verify correct logo colors per guidelines.
|
|
|
|
2. **Card Portal** (if applicable for {brand}): If the Card Portal asset is present, verify it follows sizing rules (stroke weight and corner radius based on shortest side), proper border color, rotation limits, and that the logo is not placed outside it.
|
|
|
|
3. **Color Palette**: Are only approved {brand} masterbrand colors used? Check for proper WCAG-compliant color pairings and that sacred/primary colors are used appropriately.
|
|
|
|
4. **Typography**: Is Barclays Effra (or Arial fallback) used correctly? Check font weights (Medium/Bold for headings, Regular/Medium for body), sizes per type scale, and line spacing.
|
|
|
|
5. **Design Principles**: Does the overall design reflect the brand principles defined in the specification?
|
|
|
|
6. **Sacred Assets**: Verify that sacred/primary brand assets are present and not altered or misused.
|
|
|
|
7. **Accessibility**: Check for legible font sizes, proper contrast, and appropriate use of special typography elements per guidelines.
|
|
|
|
Provide your analysis as a JSON object. Be specific about any issues found.
|
|
|
|
IMPORTANT: Do NOT include page numbers, document names, or source citations in your feedback (e.g., no "Page 10", "per Page 12", "Social Media Guidelines Page 8"). All feedback must be self-contained and directly actionable without requiring users to look up external references.
|
|
|
|
RAG Status Guidelines:
|
|
- **Green**: Fully compliant with brand guidelines, no issues
|
|
- **Amber**: Minor deviations that should be addressed but don't severely impact brand integrity
|
|
- **Red**: Significant brand guideline issues that must be fixed before use
|
|
|
|
If the proof is nonsensical, not a marketing material, or cannot be analyzed, set analysisStatus to 'low_confidence'.
|
|
|
|
**Response Format:**
|
|
- Keep feedback brief and scannable
|
|
- Use bullet points for each finding
|
|
- Structure each feedback bullet as two clearly labelled parts separated by a line break:
|
|
**Issue:** [Clear description of what's wrong]
|
|
**Recommendation:** [Actionable fix — what to do and how]
|
|
- Always capitalise "Issue:" and "Recommendation:" and bold them with double asterisks (**)
|
|
- Always place the Recommendation on a new line after the Issue (not on the same line)
|
|
- Example:
|
|
"• **Issue:** The logo is placed in the bottom-left corner, which doesn't align with brand guidelines.
|
|
**Recommendation:** Move the logo to the top-right corner as specified in the brand guidelines."
|
|
- IMPORTANT: Use British English spelling throughout all output (e.g. "authorised" not "authorized", "colour" not "color", "capitalise" not "capitalize", "organised" not "organized", "centre" not "center", "analysed" not "analyzed").
|
|
- IMPORTANT: Never use the words "violation", "violates", or "violated" in your output. Use constructive alternatives such as "issue", "doesn't align with", "doesn't meet", or "conflicts with".
|
|
- IMPORTANT: Use Plain English throughout. Choose simple, clear words over complex vocabulary. Prefer: "add" over "incorporate/integrate", "about" over "regarding", "qualifies as" over "constitutes", "use" over "utilise", "before" over "prior to", "to" over "in order to", "try" over "endeavour", "then" over "subsequently", "put in place" over "implement", "keep/contain" over "constrain", "standard interest rate" over "reversion rate". Avoid unnecessary jargon (e.g. use "exaggerated claim" instead of "puffery"). Feedback should be easy to understand for all users.
|
|
- IMPORTANT: Apply consistent punctuation and capitalisation throughout:
|
|
(a) Always capitalise the first word after a full stop, including labels like "Recommendation:" and "Issue:".
|
|
(b) End every bullet point with a full stop if it is a complete sentence. If bullets are short fragments, omit the full stop — but be consistent within the same output.
|
|
(c) Write "e.g." with no comma after it (e.g. "Apply rotation" not "e.g., Apply rotation").
|
|
- IMPORTANT: When providing example corrections in recommendations, always show the example in the format you are recommending. If recommending sentence case, write the example in sentence case (e.g. "Apply now" not "Apply Now"). If quoting the user's original error, show it first, then the corrected version: "Change 'Apply Now' to 'Apply now' (sentence case)."
|
|
- IMPORTANT: Always spell out acronyms in full on first use, with the abbreviation in parentheses. Use the short form only for subsequent mentions within the same output. Common acronyms to expand include: WCAG (Web Content Accessibility Guidelines), FSCS (Financial Services Compensation Scheme), GDE (Global Digital Expression), APR (Annual Percentage Rate), CTA (Call-to-Action), FCA (Financial Conduct Authority), PRA (Prudential Regulation Authority), T&Cs (Terms and Conditions). Apply this rule to any acronym, not only those listed here.
|
|
"""
|
|
|
|
# Determine if revision fields should be included
|
|
include_revision_fields = previous_review is not None
|
|
|
|
# Use single-image or multi-image analysis depending on input
|
|
if len(images) == 1:
|
|
file_data, file_type = images[0]
|
|
return await self.gemini.analyze_with_image(
|
|
prompt, file_data, file_type, include_revision_fields=include_revision_fields, on_fallback=on_fallback
|
|
)
|
|
else:
|
|
return await self.gemini.analyze_with_images(
|
|
prompt, images, include_revision_fields=include_revision_fields, on_fallback=on_fallback
|
|
)
|