AUXteam commited on
Commit
9c10f0a
Β·
verified Β·
1 Parent(s): 4ba3568

Feat: Implement dynamic validation and pipeline logic from IPYNB

Browse files
Files changed (1) hide show
  1. backend/services/persona_pipeline.py +210 -0
backend/services/persona_pipeline.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ import logging
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ import tinytroupe
10
+ from tinytroupe.factory import TinyPersonFactory
11
+ from tinytroupe.validation import TinyPersonValidator
12
+ from tinytroupe.agent import TinyPerson
13
+ from openai import OpenAI
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+ # Blablador client
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ def get_blablador_client() -> OpenAI:
22
+ # Use google if fallback is needed, but for now we follow the script rules:
23
+ api_key = os.environ.get("GOOGLE_API_KEY", os.environ.get("OPENAI_API_KEY", "dummy_key"))
24
+ return OpenAI(
25
+ api_key=api_key,
26
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/" if "generative" in os.environ.get("OPENAI_API_BASE", "") or "GOOGLE_API_KEY" in os.environ else "https://generativelanguage.googleapis.com/v1beta/openai/"
27
+ )
28
+
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # Input schemas
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ @dataclass
34
+ class CompanyProfile:
35
+ name: str
36
+ industry: str
37
+ description: str
38
+ market: str
39
+ size: str
40
+ challenges: list[str] = field(default_factory=list)
41
+
42
+ def to_context_string(self) -> str:
43
+ challenges_text = (
44
+ "\n".join(f"- {c}" for c in self.challenges)
45
+ if self.challenges else "None specified."
46
+ )
47
+ return f"""
48
+ Company: {self.name}
49
+ Industry: {self.industry}
50
+ Size: {self.size}
51
+ Market: {self.market}
52
+
53
+ Description:
54
+ {self.description}
55
+
56
+ Key challenges:
57
+ {challenges_text}
58
+ """.strip()
59
+
60
+ @dataclass
61
+ class CustomerSegment:
62
+ name: str
63
+ description: str
64
+ typical_needs: list[str] = field(default_factory=list)
65
+ typical_fears: list[str] = field(default_factory=list)
66
+ size_hint: int = 1
67
+
68
+ def to_role_brief(self, index: int, total: int) -> str:
69
+ needs_text = "\n".join(f"- {n}" for n in self.typical_needs) or "Not specified."
70
+ fears_text = "\n".join(f"- {f}" for f in self.typical_fears) or "Not specified."
71
+ variation_hint = _variation_hint(index, total)
72
+
73
+ return f"""
74
+ Customer segment: {self.name}
75
+
76
+ Segment description:
77
+ {self.description}
78
+
79
+ Typical needs from the company:
80
+ {needs_text}
81
+
82
+ Typical fears or blockers:
83
+ {fears_text}
84
+
85
+ {variation_hint}
86
+ Generate a single, specific individual who plausibly belongs to this segment.
87
+ Give them a realistic name, age, occupation, background, and personality.
88
+ Make them feel like a real person, not a stereotype.
89
+ """.strip()
90
+
91
+ def _variation_hint(index: int, total: int) -> str:
92
+ if total <= 1:
93
+ return ""
94
+ hints = [
95
+ "This persona should be on the younger end of the segment's age range.",
96
+ "This persona should be on the older end of the segment's age range.",
97
+ "This persona should be more digitally savvy than average for this segment.",
98
+ "This persona should be more traditional and skeptical of technology.",
99
+ "This persona should have a higher income than typical for this segment.",
100
+ "This persona should have a tighter budget than typical for this segment.",
101
+ "This persona should be particularly vocal and opinionated.",
102
+ "This persona should be more passive and conflict-averse.",
103
+ ]
104
+ return f"Variation note: {hints[index % len(hints)]}"
105
+
106
+ # ─────────────────────────────────────────────────────────────────────────────
107
+ # Step 1 β€” Generate validation expectations
108
+ # ─────────────────────────────────────────────────────────────────────────────
109
+
110
+ def generate_validation_expectations(company: CompanyProfile, segment: CustomerSegment, client: OpenAI) -> str:
111
+ prompt = f"""
112
+ You are a persona design expert helping validate AI-generated customer personas.
113
+
114
+ Given the company profile and customer segment below, write realistic and grounded
115
+ validation expectations describing what a persona from this segment SHOULD look like.
116
+
117
+ These expectations will be used to automatically score a generated persona.
118
+ Be specific. Be realistic. Include likely flaws, contradictions, and tensions
119
+ that a real person in this situation would have. Do not over-idealise.
120
+
121
+ Use these sections:
122
+ - Demographics (plausible age range, location, education, household situation)
123
+ - Professional traits (occupation, income level, career pressures)
124
+ - Personal traits (personality tendencies, stress points, blind spots)
125
+ - Relationship with {company.name} (why they use it, what frustrates them, loyalty level)
126
+ - Tastes and lifestyle (spending habits, hobbies, media, channels)
127
+ - Mindset and values (beliefs, fears, motivations)
128
+
129
+ ---
130
+ COMPANY PROFILE:
131
+ {company.to_context_string()}
132
+
133
+ CUSTOMER SEGMENT:
134
+ Name: {segment.name}
135
+ Description: {segment.description}
136
+ Typical needs: {", ".join(segment.typical_needs) or "not specified"}
137
+ Typical fears: {", ".join(segment.typical_fears) or "not specified"}
138
+ ---
139
+
140
+ Return only the expectations text. No preamble, no commentary, no markdown.
141
+ """.strip()
142
+
143
+ try:
144
+ response = client.chat.completions.create(
145
+ model="gemini-3-flash-preview",
146
+ max_tokens=1000,
147
+ messages=[{"role": "user", "content": prompt}],
148
+ )
149
+ return response.choices[0].message.content.strip()
150
+ except Exception as e:
151
+ logger.error(f"Error generating expectations: {e}")
152
+ return "Must be a realistic persona matching the segment."
153
+
154
+ # ─────────────────────────────────────────────────────────────────────────────
155
+ # Step 2 β€” Generate + validate a single persona
156
+ # ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ def generate_single_persona(
159
+ company: CompanyProfile,
160
+ segment: CustomerSegment,
161
+ expectations: str,
162
+ index: int,
163
+ total: int,
164
+ min_score: float = 0.7,
165
+ max_attempts: int = 3,
166
+ ) -> tuple[TinyPerson, float, str]:
167
+
168
+ factory = TinyPersonFactory(company.to_context_string())
169
+ role_brief = segment.to_role_brief(index, total)
170
+
171
+ person, score, justification = None, 0.0, ""
172
+
173
+ for attempt in range(1, max_attempts + 1):
174
+ logger.info(f" Attempt {attempt}/{max_attempts}...")
175
+
176
+ # Adding a sleep delay before generation to prevent 429
177
+ time.sleep(10)
178
+
179
+ person = factory.generate_person(role_brief)
180
+ if person is None:
181
+ logger.warning("Generation returned None, retrying...")
182
+ continue
183
+
184
+ logger.info(f"β†’ {person.minibio()[:80]}...")
185
+
186
+ # Add delay before validation
187
+ time.sleep(10)
188
+
189
+ try:
190
+ score, justification = TinyPersonValidator.validate_person(
191
+ person,
192
+ expectations=expectations,
193
+ include_agent_spec=True,
194
+ max_content_length=None,
195
+ )
196
+ # Safe unpack
197
+ if score is None: score = 0.0
198
+ except Exception as e:
199
+ logger.warning(f"Validation failed: {e}")
200
+ score = 0.0
201
+
202
+ logger.info(f" Score: {score:.2f}")
203
+
204
+ if score >= min_score:
205
+ logger.info(f" βœ“ Accepted")
206
+ break
207
+ if attempt < max_attempts:
208
+ logger.info(f" βœ— Below {min_score}, retrying...")
209
+
210
+ return person, score, justification