Team Ai
Apppublic

Israelbliz/User-Modeling-Agent

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
1"""Task A agent — the Impersonator.2 3Given a UserPersona and an item (title, description, categories, domain),4produce a predicted rating and a generated review that match the user's5behavioral voice.6 7The workflow is a deterministic 4-step pipeline:8 9    1. select_similar_history(persona, item)10         → pick the 3 most similar past reviews from the persona's history11         → "similar" means same domain when possible, else any12         → these ground the generation in the user's actual writing samples13    2. build_prompt(persona, item, similar_history)14         → render the persona + similar reviews + item into a structured prompt15         → the prompt is what the LLM sees16    3. llm.structured(prompt, ReviewOutput)17         → call GPT-4o (reasoning tier) and parse into a Pydantic schema18         → schema enforces (rating: float, review: str, reasoning: str)19    4. postprocess(output, persona)20         → clamp rating to 1-521         → if naija_mode is on, run the review through the style layer22 23The reasoning field is mandatory and exposed in the API response. This is24how the system demonstrates "intelligence per feature" — every generated25review comes with a sentence explaining why this rating, grounded in the26persona's signals.27"""28from __future__ import annotations29 30import logging31from dataclasses import dataclass, field32from typing import Optional33 34from pydantic import BaseModel, Field35 36from core.llm import LLMClient37from core.persona import UserPersona38from core.nigerian import naija_style_review39from core.reflection import reflect_on_review, ReflectionTrace40 41log = logging.getLogger(__name__)42 43 44# ──────────────────────────────────────────────────────────────────────────────45# Schemas46# ──────────────────────────────────────────────────────────────────────────────47 48class ItemInput(BaseModel):49    """Item details given to the Impersonator."""50    parent_asin: str = Field(description="Item ID")51    title: str = Field(description="Item title")52    description: str = Field(default="", description="Item description / synopsis")53    categories: str = Field(default="", description="Category breadcrumbs")54    domain: str = Field(description="Books / Movies_and_TV / Kindle_Store")55    average_rating: Optional[float] = Field(default=None, description="Crowd average rating, if known")56 57 58class GeneratedReview(BaseModel):59    """Structured output from the LLM."""60    rating: float = Field(description="Star rating, 1.0 to 5.0, half-stars allowed")61    review: str = Field(description="The full review text in this user's voice")62    reasoning: str = Field(description="One-sentence justification grounded in the user's persona signals")63 64 65@dataclass66class ImpersonationResult:67    """Final output returned by the agent."""68    rating: float69    review: str70    reasoning: str71    used_history_count: int   # how many past reviews informed the generation72    naija_mode: bool73    # Self-reflection metadata (Stage 3b)74    reflection_iterations: int = 0   # how many critique cycles ran75    reflection_refined: bool = False  # whether the review was revised76    reflection_notes: list[str] = field(default_factory=list)  # critique findings77 78 79# ──────────────────────────────────────────────────────────────────────────────80# Workflow steps81# ──────────────────────────────────────────────────────────────────────────────82 83def select_similar_history(persona: UserPersona, item: ItemInput,84                           k: int = 3) -> list[dict]:85    """Pick up to k past reviews to ground the generation.86 87    Preference order:88      1. same domain as the item89      2. any domain (fallback)90    Within each group we just take the most recent (history_samples is91    already sorted by recency-desc from the persona builder).92    """93    if not persona.history_samples:94        return []95 96    same_domain = [s for s in persona.history_samples if s["domain"] == item.domain]97    other_domain = [s for s in persona.history_samples if s["domain"] != item.domain]98 99    chosen = same_domain[:k]100    if len(chosen) < k:101        chosen.extend(other_domain[:(k - len(chosen))])102    return chosen103 104 105def build_prompt(persona: UserPersona, item: ItemInput,106                 similar_history: list[dict]) -> str:107    """Render the impersonation prompt.108 109    Three sections:110      - PERSONA: who the user is, quantitative + qualitative111      - WRITING SAMPLES: actual reviews this user wrote112      - TARGET ITEM: the new thing they need to review113 114    The prompt is deliberately structured so the LLM has a clear template115    to follow and grounds outputs in real data.116    """117    parts = ["You are simulating a real Amazon reviewer. Generate a review that authentically reflects their voice, rating tendencies, and behavioral patterns.\n"]118 119    parts.append("=" * 60)120    parts.append("THE USER YOU ARE SIMULATING")121    parts.append("=" * 60)122    parts.append(persona.to_prompt_block())123 124    if similar_history:125        parts.append("=" * 60)126        parts.append(f"ACTUAL REVIEWS THIS USER WROTE (study the voice carefully)")127        parts.append("=" * 60)128        for i, h in enumerate(similar_history, 1):129            parts.append(f"\n[Sample {i}] {h['rating']}★ in {h['domain']}:")130            parts.append(h["text"][:600])131 132    parts.append("\n" + "=" * 60)133    parts.append("NEW ITEM TO REVIEW")134    parts.append("=" * 60)135    parts.append(f"Domain: {item.domain}")136    parts.append(f"Title: {item.title}")137    if item.categories:138        parts.append(f"Categories: {item.categories}")139    if item.description:140        parts.append(f"Description: {item.description[:800]}")141    if item.average_rating:142        parts.append(f"Crowd average: {item.average_rating:.1f}★")143 144    parts.append("\n" + "=" * 60)145    parts.append("YOUR TASK")146    parts.append("=" * 60)147    parts.append(148        "Produce three things.\n\n"149        "1. A RATING from 1.0 to 5.0. Predict it in TWO explicit steps:\n"150        "   Step A — The PRIOR: what does this user usually give? Look at their\n"151        "     rating distribution and average. This is your starting point.\n"152        "   Step B — The ITEM EVIDENCE: now read the NEW ITEM carefully. The\n"153        "     title, description, and any crowd average carry signal about\n"154        "     whether THIS specific item is a hit or a miss FOR THIS USER.\n"155        "     - A title or description with negative/lukewarm language\n"156        "       (e.g. 'capable of better', 'lost than found', 'disappointing')\n"157        "       pulls the rating DOWN — even for a generous user.\n"158        "     - Rich, substantive material that fits the user's stated tastes\n"159        "       pulls the rating UP — even for a critical user. A critical\n"160        "       reviewer still gives 4-5★ to things that genuinely engage them.\n"161        "     - Do not assume 'critical tone' means the user dislikes things;\n"162        "       critical users rate highly when the material rewards their\n"163        "       attention. Do not assume a generous user gives 5★ to\n"164        "       everything; they still give 4★ to mild disappointments.\n"165        "   Final rating = the PRIOR adjusted by the ITEM EVIDENCE. If the\n"166        "   item evidence is neutral or absent, stay near the prior. If the\n"167        "   item evidence clearly points somewhere, MOVE toward it.\n\n"168        "2. A REVIEW in this user's voice — match their length, tone,\n"169        "   vocabulary, and quirks visible in their writing samples\n"170        "   (capitalization, sentence structure, how they signal approval or\n"171        "   disapproval). The review's sentiment MUST be consistent with the\n"172        "   rating you chose.\n\n"173        "3. A one-sentence REASONING explaining the rating. It MUST cite BOTH\n"174        "   (a) the persona prior AND (b) the specific item evidence that\n"175        "   adjusted it — e.g. 'This user averages 4.8★, but the title signals\n"176        "   \"capable of better\", a mild letdown, so 4★ not 5★.'"177    )178 179    return "\n".join(parts)180 181 182def postprocess(output: GeneratedReview, persona: UserPersona,183                naija_mode: bool, llm: LLMClient) -> GeneratedReview:184    """Clamp rating, optionally apply Naija style transfer."""185    # Clamp to [1.0, 5.0] and snap to nearest half-star186    rating = max(1.0, min(5.0, output.rating))187    rating = round(rating * 2) / 2188 189    review = output.review.strip()190    if naija_mode and review:191        try:192            review = naija_style_review(review, llm=llm)193        except Exception as e:194            log.warning(f"Naija style transfer failed; returning original. ({e})")195 196    return GeneratedReview(rating=rating, review=review, reasoning=output.reasoning)197 198 199# ──────────────────────────────────────────────────────────────────────────────200# Agent201# ──────────────────────────────────────────────────────────────────────────────202 203class ImpersonationAgent:204    """The Task A agent.205 206    Usage:207        agent = ImpersonationAgent()208        result = agent.run(persona, item, naija_mode=False)209        # result.rating, result.review, result.reasoning210    """211 212    def __init__(self, llm: LLMClient | None = None,213                 history_samples_k: int = 3,214                 use_reflection: bool = True,215                 reflection_max_iterations: int = 2):216        self.llm = llm or LLMClient()217        self.history_samples_k = history_samples_k218        self.use_reflection = use_reflection219        self.reflection_max_iterations = reflection_max_iterations220 221    def run(self, persona: UserPersona, item: ItemInput,222            naija_mode: bool = False) -> ImpersonationResult:223        # Step 1: select grounding history224        similar = select_similar_history(persona, item, k=self.history_samples_k)225        log.info(f"Selected {len(similar)} similar history items for grounding")226 227        # Step 2: build prompt228        prompt = build_prompt(persona, item, similar)229 230        # Step 3: LLM call with structured output231        log.info(f"Calling LLM for impersonation of user {persona.user_id} on item {item.parent_asin}")232        raw_output = self.llm.structured(233            prompt,234            schema=GeneratedReview,235            model="reasoning",236            system="You are an expert behavioral simulator. You write reviews exactly as the specified user would write them, matching their tone, length, rating patterns, and quirks.",237        )238 239        # Step 4: self-reflection — critique + refine (Stage 3b)240        reflection_iterations = 0241        reflection_refined = False242        reflection_notes: list[str] = []243        rating, review = raw_output.rating, raw_output.review244        if self.use_reflection:245            log.info("Running self-reflection on generated review")246            rating, review, trace = reflect_on_review(247                self.llm, persona,248                item_title=item.title, item_domain=item.domain,249                rating=rating, review=review,250                max_iterations=self.reflection_max_iterations,251            )252            reflection_iterations = trace.iterations_run253            reflection_refined = trace.refined254            reflection_notes = list(trace.critiques)255 256        refined_output = GeneratedReview(257            rating=rating, review=review, reasoning=raw_output.reasoning,258        )259 260        # Step 5: postprocess (clamp rating, optional naija style)261        final = postprocess(refined_output, persona, naija_mode=naija_mode, llm=self.llm)262 263        return ImpersonationResult(264            rating=final.rating,265            review=final.review,266            reasoning=final.reasoning,267            used_history_count=len(similar),268            naija_mode=naija_mode,269            reflection_iterations=reflection_iterations,270            reflection_refined=reflection_refined,271            reflection_notes=reflection_notes,272        )273