Codex commited on
Commit
25de2c3
·
1 Parent(s): ea773cd

Added AI-powered persona generator that creates 1-5 custom personas with UUIDs based on natural language descriptions, which can then be used across all storytelling tools to generate multiple stories simultaneously.

Browse files
Files changed (5) hide show
  1. README.md +83 -18
  2. app.py +388 -48
  3. src/index.ts +199 -12
  4. src/tools/generate_personas.ts +144 -0
  5. src/utils/persona_loader.ts +148 -12
README.md CHANGED
@@ -40,9 +40,63 @@ Designed to help product teams feel their users, not just document them.
40
 
41
  ## 🛠️ 2. MCP-Compatible Tools
42
 
43
- Each tool is exposed through the MCP server (`src/index.ts`) and powered by Claude via the `@anthropic-sdk/sdk`. Schemas are enforced through Anthropics JSON mode so outputs are always valid.
44
 
45
- ### 1. `user_story`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  Generates a structured narrative user story + acceptance criteria from a product and persona.
47
 
48
  **Input**
@@ -63,7 +117,7 @@ Generates a structured narrative user story + acceptance criteria from a product
63
  }
64
  ```
65
 
66
- ### 2. `customer_experience_tale`
67
  Creates a short narrative describing a persona experiencing a specific problem.
68
 
69
  **Input**
@@ -85,7 +139,7 @@ Creates a short narrative describing a persona experiencing a specific problem.
85
  }
86
  ```
87
 
88
- ### 3. `feature_impact_story`
89
  Shows the before and after of a persona interacting with a feature.
90
 
91
  **Input**
@@ -107,7 +161,7 @@ Shows the before and after of a persona interacting with a feature.
107
  }
108
  ```
109
 
110
- ### 4. `journey_map_story`
111
  Generates a narrative for a specific stage of the user journey.
112
 
113
  **Input**
@@ -134,20 +188,31 @@ Generates a narrative for a specific stage of the user journey.
134
 
135
  ---
136
 
137
- ## 🧬 3. Persona Data (`data/personas.json`)
138
 
139
- ```json
140
- [
141
- {
142
- "id": "busy_parent",
143
- "name": "Busy Parent",
144
- "role": "Working parent of two kids",
145
- "goals": ["Save time", "Reduce mental load", "Keep kids on schedule"],
146
- "frustrations": ["Too many apps", "Slow support", "Juggling work and home"],
147
- "tech_comfort": "medium",
148
- "quote": "If it's not simple, I don't have time for it."
149
- },
150
- {
 
 
 
 
 
 
 
 
 
 
 
151
  "id": "ops_engineer",
152
  "name": "Ops Engineer",
153
  "role": "On-call site reliability engineer",
 
40
 
41
  ## 🛠️ 2. MCP-Compatible Tools
42
 
43
+ Each tool is exposed through the MCP server (`src/index.ts`) and powered by Claude via the `@anthropic-sdk/sdk`. Schemas are enforced through Anthropic's JSON mode so outputs are always valid.
44
 
45
+ ### 1. `list_personas`
46
+ Lists all available personas with their key attributes.
47
+
48
+ **Input**
49
+ ```json
50
+ {
51
+ "limit": "number (optional)"
52
+ }
53
+ ```
54
+
55
+ **Output**
56
+ ```json
57
+ {
58
+ "personas": [
59
+ {
60
+ "id": "string",
61
+ "name": "string",
62
+ "age": "number",
63
+ "location": "string",
64
+ "job_title": "string",
65
+ "industry": "string",
66
+ "tech_literacy": "string",
67
+ "primary_goals": ["string"],
68
+ "pain_points": ["string"],
69
+ "user_story": "string"
70
+ }
71
+ ],
72
+ "total": "number"
73
+ }
74
+ ```
75
+
76
+ ### 2. `search_personas`
77
+ Search and filter personas by various criteria.
78
+
79
+ **Input**
80
+ ```json
81
+ {
82
+ "role": "string (optional)",
83
+ "industry": "string (optional)",
84
+ "age_range": "string (optional, e.g., '25-35')",
85
+ "tech_literacy": "string (optional)",
86
+ "location": "string (optional)"
87
+ }
88
+ ```
89
+
90
+ **Output**
91
+ ```json
92
+ {
93
+ "personas": ["array of matching personas"],
94
+ "count": "number",
95
+ "filters": "object with applied filters"
96
+ }
97
+ ```
98
+
99
+ ### 3. `user_story`
100
  Generates a structured narrative user story + acceptance criteria from a product and persona.
101
 
102
  **Input**
 
117
  }
118
  ```
119
 
120
+ ### 4. `customer_experience_tale`
121
  Creates a short narrative describing a persona experiencing a specific problem.
122
 
123
  **Input**
 
139
  }
140
  ```
141
 
142
+ ### 5. `feature_impact_story`
143
  Shows the before and after of a persona interacting with a feature.
144
 
145
  **Input**
 
161
  }
162
  ```
163
 
164
+ ### 6. `journey_map_story`
165
  Generates a narrative for a specific stage of the user journey.
166
 
167
  **Input**
 
188
 
189
  ---
190
 
191
+ ## 📦 3. MCP Resources
192
 
193
+ Personas are also exposed as MCP resources for easy browsing:
194
+
195
+ - `personas://all` - Complete list of all personas with summaries
196
+ - `personas://[id]` - Individual persona details (e.g., `personas://persona-001`)
197
+
198
+ Resources allow LLM clients to browse and reference personas without making tool calls.
199
+
200
+ ---
201
+
202
+ ## 🧬 4. Persona Data (`data/personas.json`)
203
+
204
+ The persona database contains detailed user profiles with:
205
+ - Demographics (age, location, income, education)
206
+ - Job information (title, industry, experience)
207
+ - Technology preferences (literacy level, devices, apps)
208
+ - Goals and motivations
209
+ - Pain points and frustrations
210
+ - Purchasing habits and interests
211
+ - Personality traits and communication style
212
+
213
+ Example personas include marketing managers, developers, healthcare directors, customer support specialists, and more across various industries and demographics.
214
+
215
+ ---
216
  "id": "ops_engineer",
217
  "name": "Ops Engineer",
218
  "role": "On-call site reliability engineer",
app.py CHANGED
@@ -72,16 +72,54 @@ User Story: {p.get("user_story", "")}"""
72
  return "Persona: Default user"
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def build_persona_profile(persona_name: str, persona_override: str):
76
- """Return the persona label for display and the context payload."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  if persona_name == CUSTOM_PERSONA_LABEL:
78
  custom_label = persona_override.strip() or "Custom Persona"
79
  context = get_persona_context("", custom_label)
80
- return custom_label, context
81
 
82
  persona_id = persona_options.get(persona_name, "busy_parent")
83
  context = get_persona_context(persona_id)
84
- return persona_name or "Persona", context
85
 
86
 
87
  def toggle_custom_persona(selected_value: str):
@@ -114,8 +152,8 @@ def generate_with_claude(system_prompt: str, user_prompt: str) -> str:
114
  }, indent=2)
115
 
116
  def generate_user_story(product: str, persona_name: str, persona_override: str):
117
- """Generate a user story for the given product and persona."""
118
- _, persona_context = build_persona_profile(persona_name, persona_override)
119
 
120
  system_prompt = """You are an expert UX writer and product strategist.
121
  Generate a structured user story in JSON format with the following fields:
@@ -125,23 +163,28 @@ Generate a structured user story in JSON format with the following fields:
125
 
126
  Return ONLY valid JSON, no additional text."""
127
 
128
- user_prompt = f"""{persona_context}
 
 
129
 
130
  Generate a user story for this persona using the product: {product}
131
 
132
  Format as JSON with the keys: story, acceptance_criteria (array), risks (array)"""
133
 
134
- response = generate_with_claude(system_prompt, user_prompt)
135
- try:
136
- # Try to parse and re-format as pretty JSON
137
- data = json.loads(response)
138
- return json.dumps(data, indent=2)
139
- except:
140
- return response
 
 
 
141
 
142
  def generate_experience_tale(problem: str, persona_name: str, persona_override: str):
143
  """Generate a customer experience tale."""
144
- _, persona_context = build_persona_profile(persona_name, persona_override)
145
 
146
  system_prompt = """You are an expert in customer experience and user research.
147
  Generate a narrative about a persona experiencing a problem in JSON format with:
@@ -152,22 +195,28 @@ Generate a narrative about a persona experiencing a problem in JSON format with:
152
 
153
  Return ONLY valid JSON, no additional text."""
154
 
155
- user_prompt = f"""{persona_context}
 
 
156
 
157
  Describe how this persona experiences this problem: {problem}
158
 
159
  Format as JSON with keys: title, narrative, pain_points (array), opportunities (array)"""
160
 
161
- response = generate_with_claude(system_prompt, user_prompt)
162
- try:
163
- data = json.loads(response)
164
- return json.dumps(data, indent=2)
165
- except:
166
- return response
 
 
 
 
167
 
168
  def generate_feature_impact(feature: str, persona_name: str, persona_override: str):
169
  """Generate a before/after feature impact story."""
170
- _, persona_context = build_persona_profile(persona_name, persona_override)
171
 
172
  system_prompt = """You are an expert at showing product impact through user narratives.
173
  Generate a before/after story in JSON format with:
@@ -178,22 +227,28 @@ Generate a before/after story in JSON format with:
178
 
179
  Return ONLY valid JSON, no additional text."""
180
 
181
- user_prompt = f"""{persona_context}
 
 
182
 
183
  Show the before/after impact of this feature on the persona's experience: {feature}
184
 
185
  Format as JSON with keys: before_story, after_story, key_benefits (array), success_metrics (array)"""
186
 
187
- response = generate_with_claude(system_prompt, user_prompt)
188
- try:
189
- data = json.loads(response)
190
- return json.dumps(data, indent=2)
191
- except:
192
- return response
 
 
 
 
193
 
194
  def generate_journey_story(stage: str, product: str, persona_name: str, persona_override: str):
195
  """Generate a journey map narrative."""
196
- _, persona_context = build_persona_profile(persona_name, persona_override)
197
 
198
  system_prompt = """You are an expert in customer journey mapping and experience design.
199
  Generate a journey stage narrative in JSON format with:
@@ -206,18 +261,218 @@ Generate a journey stage narrative in JSON format with:
206
 
207
  Return ONLY valid JSON, no additional text."""
208
 
209
- user_prompt = f"""{persona_context}
 
 
210
 
211
  Describe this persona's experience at the '{stage}' stage of their journey with {product}.
212
 
213
  Format as JSON with keys: stage, narrative, touchpoints (array), emotions (array), breakdowns (array), opportunities (array)"""
214
 
215
- response = generate_with_claude(system_prompt, user_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  try:
217
- data = json.loads(response)
218
- return json.dumps(data, indent=2)
219
- except:
220
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  # Build the Gradio interface
223
  with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
@@ -230,7 +485,88 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
230
  """)
231
 
232
  with gr.Tabs():
233
- # Tab 1: User Story
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  with gr.Tab("User Story"):
235
  gr.Markdown("### Generate Structured User Stories")
236
  with gr.Row():
@@ -242,8 +578,9 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
242
  value=default_persona_choice,
243
  )
244
  persona_custom = gr.Textbox(
245
- label="Or Custom Persona",
246
- placeholder="Describe your custom persona",
 
247
  visible=False,
248
  )
249
  persona_name.change(
@@ -259,7 +596,7 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
259
  outputs=user_story_output
260
  )
261
 
262
- # Tab 2: Experience Tale
263
  with gr.Tab("Experience Tale"):
264
  gr.Markdown("### Generate Customer Experience Narratives")
265
  with gr.Row():
@@ -271,8 +608,9 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
271
  value=default_persona_choice,
272
  )
273
  persona_custom_2 = gr.Textbox(
274
- label="Or Custom Persona",
275
- placeholder="Describe your custom persona",
 
276
  visible=False,
277
  )
278
  persona_name_2.change(
@@ -290,7 +628,7 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
290
  outputs=tale_output
291
  )
292
 
293
- # Tab 3: Feature Impact
294
  with gr.Tab("Feature Impact"):
295
  gr.Markdown("### Generate Before/After Feature Impact Stories")
296
  with gr.Row():
@@ -302,8 +640,9 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
302
  value=default_persona_choice,
303
  )
304
  persona_custom_3 = gr.Textbox(
305
- label="Or Custom Persona",
306
- placeholder="Describe your custom persona",
 
307
  visible=False,
308
  )
309
  persona_name_3.change(
@@ -321,7 +660,7 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
321
  outputs=impact_output
322
  )
323
 
324
- # Tab 4: Journey Map
325
  with gr.Tab("Journey Map"):
326
  gr.Markdown("### Generate Journey Map Narratives")
327
  with gr.Row():
@@ -334,8 +673,9 @@ with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
334
  value=default_persona_choice,
335
  )
336
  persona_custom_4 = gr.Textbox(
337
- label="Or Custom Persona",
338
- placeholder="Describe your custom persona",
 
339
  visible=False,
340
  )
341
  persona_name_4.change(
 
72
  return "Persona: Default user"
73
 
74
 
75
+ def format_persona_from_json(persona_obj: dict) -> str:
76
+ """Format a persona object (from generated personas) into context string."""
77
+ job_title = persona_obj.get("job", {}).get("title", "Unknown")
78
+ industry = persona_obj.get("job", {}).get("industry", "Unknown")
79
+ age = persona_obj.get("age", "Unknown")
80
+ location_city = persona_obj.get("location", {}).get("city", "Unknown")
81
+ goals = persona_obj.get("goals", {}).get("primary_goals", [])
82
+ pain_points = persona_obj.get("pain_points", [])
83
+ motivations = persona_obj.get("motivations", [])
84
+
85
+ return f"""Persona: {persona_obj.get("name", "Unknown")}
86
+ Age: {age}
87
+ Location: {location_city}
88
+ Title: {job_title}
89
+ Industry: {industry}
90
+ Background: {persona_obj.get("background", "")}
91
+ Goals: {', '.join(goals) if goals else "N/A"}
92
+ Pain Points: {', '.join(pain_points) if pain_points else "N/A"}
93
+ Motivations: {', '.join(motivations) if motivations else "N/A"}
94
+ User Story: {persona_obj.get("user_story", "")}"""
95
+
96
+
97
  def build_persona_profile(persona_name: str, persona_override: str):
98
+ """Return the persona label for display and the context payload. Returns (label, contexts_list) where contexts_list is a list of persona contexts."""
99
+ # Check if persona_override contains JSON (generated persona)
100
+ if persona_override and persona_override.strip():
101
+ stripped = persona_override.strip()
102
+ if stripped.startswith('{') or stripped.startswith('['):
103
+ try:
104
+ persona_json = json.loads(stripped)
105
+ # If it's an array, return all personas
106
+ if isinstance(persona_json, list) and len(persona_json) > 0:
107
+ contexts = [format_persona_from_json(p) for p in persona_json if isinstance(p, dict)]
108
+ return f"{len(contexts)} Generated Personas", contexts
109
+ # If it's a single persona object, use it directly
110
+ elif isinstance(persona_json, dict) and 'id' in persona_json:
111
+ return persona_json.get('name', 'Generated Persona'), [format_persona_from_json(persona_json)]
112
+ except json.JSONDecodeError:
113
+ pass # Fall through to custom persona handling
114
+
115
  if persona_name == CUSTOM_PERSONA_LABEL:
116
  custom_label = persona_override.strip() or "Custom Persona"
117
  context = get_persona_context("", custom_label)
118
+ return custom_label, [context]
119
 
120
  persona_id = persona_options.get(persona_name, "busy_parent")
121
  context = get_persona_context(persona_id)
122
+ return persona_name or "Persona", [context]
123
 
124
 
125
  def toggle_custom_persona(selected_value: str):
 
152
  }, indent=2)
153
 
154
  def generate_user_story(product: str, persona_name: str, persona_override: str):
155
+ """Generate a user story for the given product and persona(s)."""
156
+ _, persona_contexts = build_persona_profile(persona_name, persona_override)
157
 
158
  system_prompt = """You are an expert UX writer and product strategist.
159
  Generate a structured user story in JSON format with the following fields:
 
163
 
164
  Return ONLY valid JSON, no additional text."""
165
 
166
+ results = []
167
+ for idx, persona_context in enumerate(persona_contexts):
168
+ user_prompt = f"""{persona_context}
169
 
170
  Generate a user story for this persona using the product: {product}
171
 
172
  Format as JSON with the keys: story, acceptance_criteria (array), risks (array)"""
173
 
174
+ response = generate_with_claude(system_prompt, user_prompt)
175
+ try:
176
+ data = json.loads(response)
177
+ if len(persona_contexts) > 1:
178
+ data['persona_index'] = idx + 1
179
+ results.append(data)
180
+ except:
181
+ results.append({"error": "Failed to parse response", "raw": response})
182
+
183
+ return json.dumps(results if len(results) > 1 else results[0], indent=2)
184
 
185
  def generate_experience_tale(problem: str, persona_name: str, persona_override: str):
186
  """Generate a customer experience tale."""
187
+ _, persona_contexts = build_persona_profile(persona_name, persona_override)
188
 
189
  system_prompt = """You are an expert in customer experience and user research.
190
  Generate a narrative about a persona experiencing a problem in JSON format with:
 
195
 
196
  Return ONLY valid JSON, no additional text."""
197
 
198
+ results = []
199
+ for idx, persona_context in enumerate(persona_contexts):
200
+ user_prompt = f"""{persona_context}
201
 
202
  Describe how this persona experiences this problem: {problem}
203
 
204
  Format as JSON with keys: title, narrative, pain_points (array), opportunities (array)"""
205
 
206
+ response = generate_with_claude(system_prompt, user_prompt)
207
+ try:
208
+ data = json.loads(response)
209
+ if len(persona_contexts) > 1:
210
+ data['persona_index'] = idx + 1
211
+ results.append(data)
212
+ except:
213
+ results.append({"error": "Failed to parse response", "raw": response})
214
+
215
+ return json.dumps(results if len(results) > 1 else results[0], indent=2)
216
 
217
  def generate_feature_impact(feature: str, persona_name: str, persona_override: str):
218
  """Generate a before/after feature impact story."""
219
+ _, persona_contexts = build_persona_profile(persona_name, persona_override)
220
 
221
  system_prompt = """You are an expert at showing product impact through user narratives.
222
  Generate a before/after story in JSON format with:
 
227
 
228
  Return ONLY valid JSON, no additional text."""
229
 
230
+ results = []
231
+ for idx, persona_context in enumerate(persona_contexts):
232
+ user_prompt = f"""{persona_context}
233
 
234
  Show the before/after impact of this feature on the persona's experience: {feature}
235
 
236
  Format as JSON with keys: before_story, after_story, key_benefits (array), success_metrics (array)"""
237
 
238
+ response = generate_with_claude(system_prompt, user_prompt)
239
+ try:
240
+ data = json.loads(response)
241
+ if len(persona_contexts) > 1:
242
+ data['persona_index'] = idx + 1
243
+ results.append(data)
244
+ except:
245
+ results.append({"error": "Failed to parse response", "raw": response})
246
+
247
+ return json.dumps(results if len(results) > 1 else results[0], indent=2)
248
 
249
  def generate_journey_story(stage: str, product: str, persona_name: str, persona_override: str):
250
  """Generate a journey map narrative."""
251
+ _, persona_contexts = build_persona_profile(persona_name, persona_override)
252
 
253
  system_prompt = """You are an expert in customer journey mapping and experience design.
254
  Generate a journey stage narrative in JSON format with:
 
261
 
262
  Return ONLY valid JSON, no additional text."""
263
 
264
+ results = []
265
+ for idx, persona_context in enumerate(persona_contexts):
266
+ user_prompt = f"""{persona_context}
267
 
268
  Describe this persona's experience at the '{stage}' stage of their journey with {product}.
269
 
270
  Format as JSON with keys: stage, narrative, touchpoints (array), emotions (array), breakdowns (array), opportunities (array)"""
271
 
272
+ response = generate_with_claude(system_prompt, user_prompt)
273
+ try:
274
+ data = json.loads(response)
275
+ if len(persona_contexts) > 1:
276
+ data['persona_index'] = idx + 1
277
+ results.append(data)
278
+ except:
279
+ results.append({"error": "Failed to parse response", "raw": response})
280
+
281
+ return json.dumps(results if len(results) > 1 else results[0], indent=2)
282
+
283
+ # Persona browser functions
284
+ def search_personas_func(role: str, industry: str, age_range: str, tech_literacy: str, location: str):
285
+ """Search personas based on filters."""
286
+ filtered = personas
287
+
288
+ # Apply filters
289
+ if role and role.strip():
290
+ filtered = [p for p in filtered if role.lower() in p.get("job", {}).get("title", "").lower()]
291
+
292
+ if industry and industry.strip():
293
+ filtered = [p for p in filtered if industry.lower() in p.get("job", {}).get("industry", "").lower()]
294
+
295
+ if age_range and age_range.strip():
296
+ try:
297
+ min_age, max_age = map(int, age_range.split("-"))
298
+ filtered = [p for p in filtered if min_age <= p.get("age", 0) <= max_age]
299
+ except:
300
+ pass
301
+
302
+ if tech_literacy and tech_literacy.strip():
303
+ filtered = [p for p in filtered if tech_literacy.lower() in p.get("technology", {}).get("tech_literacy", "").lower()]
304
+
305
+ if location and location.strip():
306
+ filtered = [p for p in filtered
307
+ if location.lower() in p.get("location", {}).get("city", "").lower() or
308
+ location.lower() in p.get("location", {}).get("state", "").lower() or
309
+ location.lower() in p.get("location", {}).get("country", "").lower()]
310
+
311
+ # Format results as a table
312
+ if not filtered:
313
+ return "No personas found matching the criteria.", ""
314
+
315
+ result_lines = [f"**Found {len(filtered)} persona(s):**\n"]
316
+
317
+ for p in filtered[:20]: # Limit to 20 results
318
+ name = p.get("name", "Unknown")
319
+ age = p.get("age", "?")
320
+ title = p.get("job", {}).get("title", "Unknown")
321
+ industry_val = p.get("job", {}).get("industry", "Unknown")
322
+ city = p.get("location", {}).get("city", "Unknown")
323
+ tech_lit = p.get("technology", {}).get("tech_literacy", "Unknown")
324
+
325
+ result_lines.append(f"• **{name}** ({age}) - {title} @ {industry_val} | {city} | Tech: {tech_lit}")
326
+
327
+ return "\n".join(result_lines), json.dumps(filtered[0] if filtered else {}, indent=2)
328
+
329
+ def list_all_personas():
330
+ """List all personas with summary info."""
331
+ result_lines = [f"**Total Personas: {len(personas)}**\n"]
332
+
333
+ # Group by industry
334
+ by_industry = {}
335
+ for p in personas:
336
+ industry = p.get("job", {}).get("industry", "Other")
337
+ if industry not in by_industry:
338
+ by_industry[industry] = []
339
+ by_industry[industry].append(p)
340
+
341
+ result_lines.append("**By Industry:**")
342
+ for industry, persona_list in sorted(by_industry.items()):
343
+ result_lines.append(f"• {industry}: {len(persona_list)} personas")
344
+
345
+ result_lines.append("\n**All Personas:**")
346
+ for p in personas[:20]: # Show first 20
347
+ name = p.get("name", "Unknown")
348
+ age = p.get("age", "?")
349
+ title = p.get("job", {}).get("title", "Unknown")
350
+ city = p.get("location", {}).get("city", "Unknown")
351
+ result_lines.append(f"• **{name}** ({age}) - {title} | {city}")
352
+
353
+ if len(personas) > 20:
354
+ result_lines.append(f"\n_... and {len(personas) - 20} more personas_")
355
+
356
+ return "\n".join(result_lines)
357
+
358
+ def show_persona_details(persona_index: int):
359
+ """Show detailed information about a specific persona."""
360
+ if 0 <= persona_index < len(personas):
361
+ p = personas[persona_index]
362
+ return json.dumps(p, indent=2)
363
+ return "Persona not found"
364
+
365
+ def generate_personas_func(description: str, count: int):
366
+ """Generate random personas based on description using Claude AI."""
367
+ if not description or not description.strip():
368
+ return json.dumps({"error": "Please provide a description"}, indent=2)
369
+
370
+ if count < 1 or count > 5:
371
+ return json.dumps({"error": "Count must be between 1 and 5"}, indent=2)
372
+
373
+ if not client:
374
+ return json.dumps({"error": "Anthropic API key not configured"}, indent=2)
375
+
376
+ system_prompt = """You are a persona generation expert. Generate realistic, diverse user personas based on the provided description.
377
+
378
+ CRITICAL REQUIREMENTS:
379
+ 1. Return ONLY valid JSON - no markdown, no code blocks, no explanations
380
+ 2. Return a JSON array of persona objects
381
+ 3. Each persona must follow the exact structure provided in the example
382
+ 4. Ensure diversity in all randomized fields (location, income, education, tech literacy, devices, etc.)
383
+ 5. All personas must match the core description provided by the user
384
+ 6. Generate exactly the number of personas requested
385
+
386
+ PERSONA STRUCTURE (follow this exactly):
387
+ {
388
+ "id": "UUID (e.g., 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')",
389
+ "name": "Full Name",
390
+ "age": number,
391
+ "gender": "Male/Female/Nonbinary",
392
+ "location": {
393
+ "address": "street address",
394
+ "city": "city",
395
+ "state": "state/province",
396
+ "country": "country"
397
+ },
398
+ "demographics": {
399
+ "income": "$XX,000/year",
400
+ "education_level": "education level",
401
+ "marital_status": "status",
402
+ "household_size": number
403
+ },
404
+ "job": {
405
+ "title": "job title",
406
+ "industry": "industry",
407
+ "experience_years": number,
408
+ "employment_type": "Full-time/Part-time/Freelance/etc"
409
+ },
410
+ "background": "brief background description",
411
+ "interests": ["interest1", "interest2", "interest3"],
412
+ "purchasing_habits": {
413
+ "online_shopping_frequency": "frequency",
414
+ "preferred_platforms": ["platform1", "platform2"],
415
+ "average_spend_per_month": "$XXX",
416
+ "brand_loyalty_level": "Low/Medium/High/Very High"
417
+ },
418
+ "technology": {
419
+ "tech_literacy": "Low/Medium/High/Very High",
420
+ "devices_used": ["device1", "device2"],
421
+ "favorite_apps": ["app1", "app2", "app3"]
422
+ },
423
+ "goals": {
424
+ "primary_goals": ["goal1", "goal2"],
425
+ "secondary_goals": ["goal1", "goal2"]
426
+ },
427
+ "pain_points": ["pain1", "pain2"],
428
+ "motivations": ["motivation1", "motivation2"],
429
+ "personality": {
430
+ "traits": ["trait1", "trait2"],
431
+ "communication_style": "style description"
432
+ },
433
+ "user_story": "As a [role], I want [goal] so [benefit].",
434
+ "acceptance_criteria": ["criteria1", "criteria2"]
435
+ }"""
436
+
437
+ user_prompt = f"""Generate {count} diverse user persona{'s' if count > 1 else ''} that match this description:
438
+
439
+ "{description}"
440
+
441
+ Requirements:
442
+ - Generate exactly {count} persona{'s' if count > 1 else ''}
443
+ - Each persona MUST match the core description: "{description}"
444
+ - Randomize other attributes for diversity (locations worldwide, various incomes, different tech literacy levels, diverse jobs/industries, etc.)
445
+ - Ensure realistic consistency within each persona
446
+ - Use diverse names from various cultures
447
+ - Return ONLY the JSON array, no other text
448
+
449
+ Return format: [persona1, persona2, ...]"""
450
+
451
  try:
452
+ response = client.messages.create(
453
+ model=model,
454
+ max_tokens=4096,
455
+ messages=[{"role": "user", "content": f"{system_prompt}\n\n{user_prompt}"}]
456
+ )
457
+
458
+ response_text = response.content[0].text if response.content else ""
459
+
460
+ # Clean up response (remove markdown code blocks if present)
461
+ cleaned_response = response_text.strip()
462
+ if cleaned_response.startswith("```json"):
463
+ cleaned_response = cleaned_response.replace("```json", "").replace("```", "").strip()
464
+ elif cleaned_response.startswith("```"):
465
+ cleaned_response = cleaned_response.replace("```", "").strip()
466
+
467
+ # Validate JSON
468
+ personas_data = json.loads(cleaned_response)
469
+ if not isinstance(personas_data, list):
470
+ return json.dumps({"error": "Response is not an array"}, indent=2)
471
+
472
+ return json.dumps(personas_data, indent=2)
473
+
474
+ except Exception as e:
475
+ return json.dumps({"error": f"Failed to generate personas: {str(e)}"}, indent=2)
476
 
477
  # Build the Gradio interface
478
  with gr.Blocks(title="HealixPath - AI Persona Storytelling") as demo:
 
485
  """)
486
 
487
  with gr.Tabs():
488
+ # Tab 0: Persona Browser (NEW!)
489
+ with gr.Tab("🔍 Persona Browser"):
490
+ gr.Markdown("""
491
+ ### Discover & Search Personas
492
+ **New Feature:** Browse all 30 personas and filter by multiple criteria
493
+ """)
494
+
495
+ with gr.Row():
496
+ with gr.Column(scale=1):
497
+ gr.Markdown("#### Filters")
498
+ role_filter = gr.Textbox(label="Role/Job Title", placeholder="e.g., manager, developer, director")
499
+ industry_filter = gr.Textbox(label="Industry", placeholder="e.g., healthcare, tech, e-commerce")
500
+ age_filter = gr.Textbox(label="Age Range", placeholder="e.g., 25-35, 40-50")
501
+ tech_filter = gr.Textbox(label="Tech Literacy", placeholder="e.g., high, moderate, low")
502
+ location_filter = gr.Textbox(label="Location", placeholder="e.g., San Diego, Toronto, USA")
503
+
504
+ search_btn = gr.Button("🔍 Search Personas", variant="primary")
505
+ list_all_btn = gr.Button("📋 List All Personas")
506
+
507
+ with gr.Column(scale=2):
508
+ gr.Markdown("#### Search Results")
509
+ search_results = gr.Markdown(value="Click 'List All Personas' to see all available personas.")
510
+
511
+ gr.Markdown("#### Persona Details (JSON)")
512
+ persona_details = gr.Textbox(label="Full Persona Data", lines=20, interactive=False)
513
+
514
+ search_btn.click(
515
+ search_personas_func,
516
+ inputs=[role_filter, industry_filter, age_filter, tech_filter, location_filter],
517
+ outputs=[search_results, persona_details]
518
+ )
519
+
520
+ list_all_btn.click(
521
+ list_all_personas,
522
+ outputs=search_results
523
+ )
524
+
525
+ # Tab 1: Generate Personas (NEW!)
526
+ with gr.Tab("✨ Generate Personas"):
527
+ gr.Markdown("""
528
+ ### AI-Powered Persona Generator
529
+ **New Feature:** Generate random, realistic personas based on any description
530
+
531
+ Examples:
532
+ - "people who love to cook but never have time to in their 20-30s"
533
+ - "sandwich enthusiasts who are 50+ years old"
534
+ - "tech-savvy students interested in sustainable fashion"
535
+ """)
536
+
537
+ with gr.Row():
538
+ with gr.Column(scale=2):
539
+ persona_description = gr.Textbox(
540
+ label="Persona Description",
541
+ placeholder="e.g., people who love to cook but never have time to in their 20-30s",
542
+ lines=3
543
+ )
544
+ with gr.Column(scale=1):
545
+ persona_count = gr.Slider(
546
+ label="Number of Personas",
547
+ minimum=1,
548
+ maximum=5,
549
+ value=3,
550
+ step=1
551
+ )
552
+
553
+ generate_personas_btn = gr.Button("✨ Generate Personas", variant="primary", size="lg")
554
+
555
+ gr.Markdown("#### Generated Personas (JSON)")
556
+ generated_personas_output = gr.Textbox(
557
+ label="Personas",
558
+ lines=25,
559
+ interactive=False,
560
+ placeholder="Generated personas will appear here..."
561
+ )
562
+
563
+ generate_personas_btn.click(
564
+ generate_personas_func,
565
+ inputs=[persona_description, persona_count],
566
+ outputs=generated_personas_output
567
+ )
568
+
569
+ # Tab 2: User Story
570
  with gr.Tab("User Story"):
571
  gr.Markdown("### Generate Structured User Stories")
572
  with gr.Row():
 
578
  value=default_persona_choice,
579
  )
580
  persona_custom = gr.Textbox(
581
+ label="Or Custom Persona / Generated Persona JSON",
582
+ placeholder="Paste generated persona JSON here or describe your custom persona",
583
+ lines=3,
584
  visible=False,
585
  )
586
  persona_name.change(
 
596
  outputs=user_story_output
597
  )
598
 
599
+ # Tab 3: Experience Tale
600
  with gr.Tab("Experience Tale"):
601
  gr.Markdown("### Generate Customer Experience Narratives")
602
  with gr.Row():
 
608
  value=default_persona_choice,
609
  )
610
  persona_custom_2 = gr.Textbox(
611
+ label="Or Custom Persona / Generated Persona JSON",
612
+ placeholder="Paste generated persona JSON here or describe your custom persona",
613
+ lines=3,
614
  visible=False,
615
  )
616
  persona_name_2.change(
 
628
  outputs=tale_output
629
  )
630
 
631
+ # Tab 4: Feature Impact
632
  with gr.Tab("Feature Impact"):
633
  gr.Markdown("### Generate Before/After Feature Impact Stories")
634
  with gr.Row():
 
640
  value=default_persona_choice,
641
  )
642
  persona_custom_3 = gr.Textbox(
643
+ label="Or Custom Persona / Generated Persona JSON",
644
+ placeholder="Paste generated persona JSON here or describe your custom persona",
645
+ lines=3,
646
  visible=False,
647
  )
648
  persona_name_3.change(
 
660
  outputs=impact_output
661
  )
662
 
663
+ # Tab 5: Journey Map
664
  with gr.Tab("Journey Map"):
665
  gr.Markdown("### Generate Journey Map Narratives")
666
  with gr.Row():
 
673
  value=default_persona_choice,
674
  )
675
  persona_custom_4 = gr.Textbox(
676
+ label="Or Custom Persona / Generated Persona JSON",
677
+ placeholder="Paste generated persona JSON here or describe your custom persona",
678
+ lines=3,
679
  visible=False,
680
  )
681
  persona_name_4.change(
src/index.ts CHANGED
@@ -2,6 +2,8 @@ import "dotenv/config";
2
  import {
3
  loadPersonas,
4
  getPersona,
 
 
5
  type Persona,
6
  } from "./utils/persona_loader.js";
7
  import { generateUserStory, type UserStoryInput } from "./tools/user_story.js";
@@ -17,6 +19,7 @@ import {
17
  generateJourneyMapStory,
18
  type JourneyMapStoryInput,
19
  } from "./tools/journey_map_story.js";
 
20
 
21
  interface ToolSchema {
22
  name: string;
@@ -28,6 +31,13 @@ interface ToolSchema {
28
  };
29
  }
30
 
 
 
 
 
 
 
 
31
  class HealixPathServer {
32
  private personas: Map<string, Persona>;
33
 
@@ -37,6 +47,52 @@ class HealixPathServer {
37
 
38
  getTools(): ToolSchema[] {
39
  return [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  {
41
  name: "user_story",
42
  description:
@@ -51,7 +107,7 @@ class HealixPathServer {
51
  persona_id: {
52
  type: "string",
53
  description:
54
- "ID of the persona to use (busy_parent, ops_engineer, product_manager)",
55
  },
56
  persona_override: {
57
  type: "string",
@@ -139,13 +195,106 @@ class HealixPathServer {
139
  required: ["stage", "product"],
140
  },
141
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  ];
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  async processTool(
146
  toolName: string,
147
  toolInput: Record<string, unknown>
148
  ): Promise<string> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  let persona: Persona | undefined;
150
 
151
  // Load persona if specified
@@ -185,6 +334,18 @@ class HealixPathServer {
185
  return JSON.stringify(journeyStory);
186
  }
187
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  default:
189
  return JSON.stringify({ error: `Unknown tool: ${toolName}` });
190
  }
@@ -208,28 +369,46 @@ async function main(): Promise<void> {
208
  console.log(` ${tool.description}`);
209
  });
210
 
211
- console.log("\n\nAvailable Personas:");
212
- console.log(
213
- "- busy_parent: Working parent juggling multiple responsibilities"
214
- );
215
- console.log("- ops_engineer: On-call SRE managing infrastructure");
216
- console.log("- product_manager: PM at a healthcare startup");
217
 
218
  console.log("\n\n📝 Example Tool Calls:");
219
  console.log("---");
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  // Example 1: User Story
222
  const userStoryResult = await server.processTool("user_story", {
223
  product: "Healthcare appointment scheduling app",
224
- persona_id: "busy_parent",
225
  });
226
- console.log("\n1. User Story for Busy Parent:");
227
  console.log(userStoryResult);
228
 
229
  // Example 2: Customer Experience Tale
230
  const taleResult = await server.processTool("customer_experience_tale", {
231
  problem: "Mobile app crashes during checkout",
232
- persona_id: "busy_parent",
233
  });
234
  console.log("\n2. Customer Experience Tale:");
235
  console.log(taleResult);
@@ -237,7 +416,7 @@ async function main(): Promise<void> {
237
  // Example 3: Feature Impact Story
238
  const impactResult = await server.processTool("feature_impact_story", {
239
  feature_description: "One-click scheduling with automatic reminders",
240
- persona_id: "busy_parent",
241
  });
242
  console.log("\n3. Feature Impact Story:");
243
  console.log(impactResult);
@@ -246,11 +425,19 @@ async function main(): Promise<void> {
246
  const journeyResult = await server.processTool("journey_map_story", {
247
  stage: "adoption",
248
  product: "Healthcare platform",
249
- persona_id: "ops_engineer",
250
  });
251
  console.log("\n4. Journey Map Story:");
252
  console.log(journeyResult);
253
 
 
 
 
 
 
 
 
 
254
  console.log("\n\n✅ HealixPath Server is ready to serve MCP clients!");
255
  }
256
 
 
2
  import {
3
  loadPersonas,
4
  getPersona,
5
+ formatPersonaSummary,
6
+ searchPersonas,
7
  type Persona,
8
  } from "./utils/persona_loader.js";
9
  import { generateUserStory, type UserStoryInput } from "./tools/user_story.js";
 
19
  generateJourneyMapStory,
20
  type JourneyMapStoryInput,
21
  } from "./tools/journey_map_story.js";
22
+ import { generatePersonas } from "./tools/generate_personas.js";
23
 
24
  interface ToolSchema {
25
  name: string;
 
31
  };
32
  }
33
 
34
+ interface ResourceSchema {
35
+ uri: string;
36
+ name: string;
37
+ description: string;
38
+ mimeType: string;
39
+ }
40
+
41
  class HealixPathServer {
42
  private personas: Map<string, Persona>;
43
 
 
47
 
48
  getTools(): ToolSchema[] {
49
  return [
50
+ {
51
+ name: "list_personas",
52
+ description:
53
+ "List all available personas with their key attributes (id, name, age, location, job title, industry, tech literacy, goals, pain points)",
54
+ input_schema: {
55
+ type: "object",
56
+ properties: {
57
+ limit: {
58
+ type: "number",
59
+ description: "Optional: Maximum number of personas to return (default: all)",
60
+ },
61
+ },
62
+ required: [],
63
+ },
64
+ },
65
+ {
66
+ name: "search_personas",
67
+ description:
68
+ "Search and filter personas by role/job title, industry, age range, tech literacy, or location",
69
+ input_schema: {
70
+ type: "object",
71
+ properties: {
72
+ role: {
73
+ type: "string",
74
+ description: "Filter by job title or role (partial match, case-insensitive)",
75
+ },
76
+ industry: {
77
+ type: "string",
78
+ description: "Filter by industry (partial match, case-insensitive)",
79
+ },
80
+ age_range: {
81
+ type: "string",
82
+ description: "Filter by age range (format: 'min-max', e.g., '25-35')",
83
+ },
84
+ tech_literacy: {
85
+ type: "string",
86
+ description: "Filter by tech literacy level (e.g., 'high', 'moderate', 'low')",
87
+ },
88
+ location: {
89
+ type: "string",
90
+ description: "Filter by location - city, state, or country (partial match)",
91
+ },
92
+ },
93
+ required: [],
94
+ },
95
+ },
96
  {
97
  name: "user_story",
98
  description:
 
107
  persona_id: {
108
  type: "string",
109
  description:
110
+ "ID of the persona to use (use list_personas to see available IDs)",
111
  },
112
  persona_override: {
113
  type: "string",
 
195
  required: ["stage", "product"],
196
  },
197
  },
198
+ {
199
+ name: "generate_personas",
200
+ description:
201
+ "Generate random user personas based on a natural language description. Creates realistic, diverse personas matching the specified criteria with randomized attributes for variety.",
202
+ input_schema: {
203
+ type: "object",
204
+ properties: {
205
+ description: {
206
+ type: "string",
207
+ description:
208
+ "Natural language description of the personas to generate (e.g., 'people who love to cook but never have time to in their 20-30s', 'sandwich enthusiasts who are 50+ years old')",
209
+ },
210
+ count: {
211
+ type: "number",
212
+ description:
213
+ "Number of personas to generate (minimum: 1, maximum: 5, default: 1)",
214
+ },
215
+ },
216
+ required: ["description", "count"],
217
+ },
218
+ },
219
  ];
220
  }
221
 
222
+ getResources(): ResourceSchema[] {
223
+ const resources: ResourceSchema[] = [
224
+ {
225
+ uri: "personas://all",
226
+ name: "All Personas",
227
+ description: "Complete list of all available personas with their full details",
228
+ mimeType: "application/json",
229
+ },
230
+ ];
231
+
232
+ // Add individual persona resources
233
+ for (const [id, persona] of this.personas.entries()) {
234
+ resources.push({
235
+ uri: `personas://${id}`,
236
+ name: `${persona.name} - ${persona.job?.title || persona.role || "User"}`,
237
+ description: `Detailed information about ${persona.name}`,
238
+ mimeType: "application/json",
239
+ });
240
+ }
241
+
242
+ return resources;
243
+ }
244
+
245
+ async readResource(uri: string): Promise<string> {
246
+ if (uri === "personas://all") {
247
+ const allPersonas = Array.from(this.personas.values()).map(formatPersonaSummary);
248
+ return JSON.stringify(allPersonas, null, 2);
249
+ }
250
+
251
+ // Handle individual persona URIs
252
+ if (uri.startsWith("personas://")) {
253
+ const personaId = uri.replace("personas://", "");
254
+ const persona = this.personas.get(personaId);
255
+
256
+ if (!persona) {
257
+ return JSON.stringify({ error: `Persona not found: ${personaId}` });
258
+ }
259
+
260
+ return JSON.stringify(persona, null, 2);
261
+ }
262
+
263
+ return JSON.stringify({ error: `Unknown resource URI: ${uri}` });
264
+ }
265
+
266
  async processTool(
267
  toolName: string,
268
  toolInput: Record<string, unknown>
269
  ): Promise<string> {
270
+ // Handle list_personas tool
271
+ if (toolName === "list_personas") {
272
+ const limit = typeof toolInput.limit === "number" ? toolInput.limit : undefined;
273
+ const allPersonas = Array.from(this.personas.values()).map(formatPersonaSummary);
274
+ const personas = limit ? allPersonas.slice(0, limit) : allPersonas;
275
+ return JSON.stringify({ personas, total: this.personas.size }, null, 2);
276
+ }
277
+
278
+ // Handle search_personas tool
279
+ if (toolName === "search_personas") {
280
+ const filters = {
281
+ role: typeof toolInput.role === "string" ? toolInput.role : undefined,
282
+ industry: typeof toolInput.industry === "string" ? toolInput.industry : undefined,
283
+ age_range: typeof toolInput.age_range === "string" ? toolInput.age_range : undefined,
284
+ tech_literacy: typeof toolInput.tech_literacy === "string" ? toolInput.tech_literacy : undefined,
285
+ location: typeof toolInput.location === "string" ? toolInput.location : undefined,
286
+ };
287
+
288
+ const results = searchPersonas(this.personas, filters);
289
+ const summaries = results.map(formatPersonaSummary);
290
+
291
+ return JSON.stringify({
292
+ personas: summaries,
293
+ count: summaries.length,
294
+ filters: Object.fromEntries(Object.entries(filters).filter(([_, v]) => v !== undefined))
295
+ }, null, 2);
296
+ }
297
+
298
  let persona: Persona | undefined;
299
 
300
  // Load persona if specified
 
334
  return JSON.stringify(journeyStory);
335
  }
336
 
337
+ case "generate_personas": {
338
+ const description = typeof toolInput.description === "string" ? toolInput.description : "";
339
+ const count = typeof toolInput.count === "number" ? toolInput.count : 1;
340
+
341
+ if (!description) {
342
+ return JSON.stringify({ error: "Description is required" });
343
+ }
344
+
345
+ const personas = await generatePersonas({ description, count });
346
+ return personas; // Already JSON stringified
347
+ }
348
+
349
  default:
350
  return JSON.stringify({ error: `Unknown tool: ${toolName}` });
351
  }
 
369
  console.log(` ${tool.description}`);
370
  });
371
 
372
+ console.log("\n\nAvailable Resources:");
373
+ server.getResources().slice(0, 5).forEach((resource) => {
374
+ console.log(`\n- ${resource.uri}`);
375
+ console.log(` ${resource.description}`);
376
+ });
377
+ console.log(`\n... and ${server.getResources().length - 5} more persona resources`);
378
 
379
  console.log("\n\n📝 Example Tool Calls:");
380
  console.log("---");
381
 
382
+ // Example 0: List Personas
383
+ const listResult = await server.processTool("list_personas", { limit: 3 });
384
+ console.log("\n0. List Personas (first 3):");
385
+ console.log(listResult);
386
+
387
+ // Example 0b: Search Personas
388
+ const searchResult = await server.processTool("search_personas", {
389
+ industry: "healthcare",
390
+ tech_literacy: "moderate"
391
+ });
392
+ console.log("\n0b. Search Personas (Healthcare + Moderate Tech):");
393
+ console.log(searchResult);
394
+
395
+ // Example 0c: Read Resource
396
+ const resourceResult = await server.readResource("personas://all");
397
+ console.log("\n0c. Read Resource (all personas summary - truncated):");
398
+ console.log(resourceResult.substring(0, 500) + "...");
399
+
400
  // Example 1: User Story
401
  const userStoryResult = await server.processTool("user_story", {
402
  product: "Healthcare appointment scheduling app",
403
+ persona_id: "persona-001",
404
  });
405
+ console.log("\n1. User Story:");
406
  console.log(userStoryResult);
407
 
408
  // Example 2: Customer Experience Tale
409
  const taleResult = await server.processTool("customer_experience_tale", {
410
  problem: "Mobile app crashes during checkout",
411
+ persona_id: "persona-001",
412
  });
413
  console.log("\n2. Customer Experience Tale:");
414
  console.log(taleResult);
 
416
  // Example 3: Feature Impact Story
417
  const impactResult = await server.processTool("feature_impact_story", {
418
  feature_description: "One-click scheduling with automatic reminders",
419
+ persona_id: "persona-001",
420
  });
421
  console.log("\n3. Feature Impact Story:");
422
  console.log(impactResult);
 
425
  const journeyResult = await server.processTool("journey_map_story", {
426
  stage: "adoption",
427
  product: "Healthcare platform",
428
+ persona_id: "persona-003",
429
  });
430
  console.log("\n4. Journey Map Story:");
431
  console.log(journeyResult);
432
 
433
+ // Example 5: Generate Personas
434
+ const generateResult = await server.processTool("generate_personas", {
435
+ description: "people who love to cook but never have time to in their 20-30s",
436
+ count: 2,
437
+ });
438
+ console.log("\n5. Generate Personas (cooking enthusiasts, 20-30s, time-constrained):");
439
+ console.log(generateResult);
440
+
441
  console.log("\n\n✅ HealixPath Server is ready to serve MCP clients!");
442
  }
443
 
src/tools/generate_personas.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+
3
+ const client = new Anthropic({
4
+ apiKey: process.env.ANTHROPIC_API_KEY,
5
+ });
6
+
7
+ interface GeneratePersonasParams {
8
+ description: string;
9
+ count: number;
10
+ }
11
+
12
+ export async function generatePersonas(params: GeneratePersonasParams): Promise<string> {
13
+ const { description, count } = params;
14
+
15
+ // Validate count
16
+ if (count < 1 || count > 5) {
17
+ return "Error: Count must be between 1 and 5 personas.";
18
+ }
19
+
20
+ const systemPrompt = `You are a persona generation expert. Generate realistic, diverse user personas based on the provided description.
21
+
22
+ CRITICAL REQUIREMENTS:
23
+ 1. Return ONLY valid JSON - no markdown, no code blocks, no explanations
24
+ 2. Return a JSON array of persona objects
25
+ 3. Each persona must follow the exact structure provided in the example
26
+ 4. Ensure diversity in all randomized fields (location, income, education, tech literacy, devices, etc.)
27
+ 5. All personas must match the core description provided by the user
28
+ 6. Generate exactly the number of personas requested
29
+
30
+ PERSONA STRUCTURE (follow this exactly):
31
+ {
32
+ "id": "UUID (e.g., 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')",
33
+ "name": "Full Name",
34
+ "age": number,
35
+ "gender": "Male/Female/Nonbinary",
36
+ "location": {
37
+ "address": "street address",
38
+ "city": "city",
39
+ "state": "state/province",
40
+ "country": "country"
41
+ },
42
+ "demographics": {
43
+ "income": "$XX,000/year",
44
+ "education_level": "education level",
45
+ "marital_status": "status",
46
+ "household_size": number
47
+ },
48
+ "job": {
49
+ "title": "job title",
50
+ "industry": "industry",
51
+ "experience_years": number,
52
+ "employment_type": "Full-time/Part-time/Freelance/etc"
53
+ },
54
+ "background": "brief background description",
55
+ "interests": ["interest1", "interest2", "interest3"],
56
+ "purchasing_habits": {
57
+ "online_shopping_frequency": "frequency",
58
+ "preferred_platforms": ["platform1", "platform2"],
59
+ "average_spend_per_month": "$XXX",
60
+ "brand_loyalty_level": "Low/Medium/High/Very High"
61
+ },
62
+ "technology": {
63
+ "tech_literacy": "Low/Medium/High/Very High",
64
+ "devices_used": ["device1", "device2"],
65
+ "favorite_apps": ["app1", "app2", "app3"]
66
+ },
67
+ "goals": {
68
+ "primary_goals": ["goal1", "goal2"],
69
+ "secondary_goals": ["goal1", "goal2"]
70
+ },
71
+ "pain_points": ["pain1", "pain2"],
72
+ "motivations": ["motivation1", "motivation2"],
73
+ "personality": {
74
+ "traits": ["trait1", "trait2"],
75
+ "communication_style": "style description"
76
+ },
77
+ "user_story": "As a [role], I want [goal] so [benefit].",
78
+ "acceptance_criteria": ["criteria1", "criteria2"]
79
+ }`;
80
+
81
+ const userPrompt = `Generate ${count} diverse user persona${count > 1 ? 's' : ''} that match this description:
82
+
83
+ "${description}"
84
+
85
+ Requirements:
86
+ - Generate exactly ${count} persona${count > 1 ? 's' : ''}
87
+ - Each persona MUST match the core description: "${description}"
88
+ - Randomize other attributes for diversity (locations worldwide, various incomes, different tech literacy levels, diverse jobs/industries, etc.)
89
+ - Ensure realistic consistency within each persona
90
+ - Use diverse names from various cultures
91
+ - Return ONLY the JSON array, no other text
92
+
93
+ Return format: [persona1, persona2, ...]`;
94
+
95
+ try {
96
+ const message = await client.messages.create({
97
+ model: "claude-3-5-sonnet-20241022",
98
+ max_tokens: 8000,
99
+ messages: [
100
+ {
101
+ role: "user",
102
+ content: `${systemPrompt}\n\n${userPrompt}`,
103
+ },
104
+ ],
105
+ });
106
+
107
+ const responseText = message.content[0].type === "text" ? message.content[0].text : "";
108
+
109
+ // Clean up response (remove markdown code blocks if present)
110
+ let cleanedResponse = responseText.trim();
111
+ if (cleanedResponse.startsWith("```json")) {
112
+ cleanedResponse = cleanedResponse.replace(/^```json\s*/, "").replace(/```\s*$/, "").trim();
113
+ } else if (cleanedResponse.startsWith("```")) {
114
+ cleanedResponse = cleanedResponse.replace(/^```\s*/, "").replace(/```\s*$/, "").trim();
115
+ }
116
+
117
+ // Validate JSON
118
+ try {
119
+ const personas = JSON.parse(cleanedResponse);
120
+ if (!Array.isArray(personas)) {
121
+ throw new Error("Response is not an array");
122
+ }
123
+
124
+ // Return formatted JSON
125
+ return JSON.stringify(personas, null, 2);
126
+ } catch (parseError) {
127
+ console.error("JSON Parse Error:", parseError);
128
+ console.error("Response:", cleanedResponse);
129
+ return `Error: Failed to parse generated personas. Raw response:\n${cleanedResponse}`;
130
+ }
131
+
132
+ } catch (error: any) {
133
+ console.error("Error generating personas:", error);
134
+
135
+ // Fallback response
136
+ return JSON.stringify([
137
+ {
138
+ error: "Failed to generate personas with AI",
139
+ message: error.message || "Unknown error",
140
+ fallback_note: "The persona generation tool requires a valid Anthropic API key and internet connection."
141
+ }
142
+ ], null, 2);
143
+ }
144
+ }
src/utils/persona_loader.ts CHANGED
@@ -4,12 +4,57 @@ import { join } from "path";
4
  export interface Persona {
5
  id: string;
6
  name: string;
7
- role: string;
8
- goals: string[];
9
- frustrations: string[];
10
- tech_comfort: string;
11
- quote: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  pain_points?: string[];
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  needs?: string[];
14
  }
15
 
@@ -36,14 +81,105 @@ export function getPersona(personaId: string, personas: Map<string, Persona>): P
36
  }
37
 
38
  export function formatPersonaContext(persona: Persona): string {
 
 
 
 
 
 
 
 
 
39
  return `
40
  Persona: ${persona.name}
41
- Role: ${persona.role}
42
- Goals: ${persona.goals.join(", ")}
43
- Frustrations: ${persona.frustrations.join(", ")}
44
- Tech Comfort Level: ${persona.tech_comfort}
45
- Quote: "${persona.quote}"
46
- ${persona.pain_points ? `Pain Points: ${persona.pain_points.join(", ")}` : ""}
47
- ${persona.needs ? `Needs: ${persona.needs.join(", ")}` : ""}
 
 
 
 
48
  `.trim();
49
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  export interface Persona {
5
  id: string;
6
  name: string;
7
+ age?: number;
8
+ gender?: string;
9
+ location?: {
10
+ address?: string;
11
+ city?: string;
12
+ state?: string;
13
+ country?: string;
14
+ };
15
+ demographics?: {
16
+ income?: string;
17
+ education_level?: string;
18
+ marital_status?: string;
19
+ household_size?: number;
20
+ };
21
+ job?: {
22
+ title?: string;
23
+ industry?: string;
24
+ experience_years?: number;
25
+ employment_type?: string;
26
+ };
27
+ background?: string;
28
+ interests?: string[];
29
+ purchasing_habits?: {
30
+ online_shopping_frequency?: string;
31
+ preferred_platforms?: string[];
32
+ average_spend_per_month?: string;
33
+ brand_loyalty_level?: string;
34
+ };
35
+ technology?: {
36
+ tech_literacy?: string;
37
+ devices_used?: string[];
38
+ favorite_apps?: string[];
39
+ };
40
+ goals?: {
41
+ primary_goals?: string[];
42
+ secondary_goals?: string[];
43
+ };
44
  pain_points?: string[];
45
+ motivations?: string[];
46
+ personality?: {
47
+ traits?: string[];
48
+ communication_style?: string;
49
+ };
50
+ user_story?: string;
51
+ acceptance_criteria?: string[];
52
+
53
+ // Legacy fields for backward compatibility
54
+ role?: string;
55
+ frustrations?: string[];
56
+ tech_comfort?: string;
57
+ quote?: string;
58
  needs?: string[];
59
  }
60
 
 
81
  }
82
 
83
  export function formatPersonaContext(persona: Persona): string {
84
+ const jobTitle = persona.job?.title || persona.role || "Unknown";
85
+ const industry = persona.job?.industry || "Unknown";
86
+ const age = persona.age || "Unknown";
87
+ const city = persona.location?.city || "Unknown";
88
+ const primaryGoals = persona.goals?.primary_goals || persona.goals as any as string[] || [];
89
+ const painPoints = persona.pain_points || persona.frustrations || [];
90
+ const motivations = persona.motivations || [];
91
+ const techLiteracy = persona.technology?.tech_literacy || persona.tech_comfort || "Unknown";
92
+
93
  return `
94
  Persona: ${persona.name}
95
+ Age: ${age}
96
+ Location: ${city}
97
+ Title: ${jobTitle}
98
+ Industry: ${industry}
99
+ Background: ${persona.background || "N/A"}
100
+ Goals: ${Array.isArray(primaryGoals) ? primaryGoals.join(", ") : "N/A"}
101
+ Pain Points: ${Array.isArray(painPoints) ? painPoints.join(", ") : "N/A"}
102
+ Motivations: ${Array.isArray(motivations) ? motivations.join(", ") : "N/A"}
103
+ Tech Literacy: ${techLiteracy}
104
+ ${persona.user_story ? `User Story: ${persona.user_story}` : ""}
105
+ ${persona.quote ? `Quote: "${persona.quote}"` : ""}
106
  `.trim();
107
  }
108
+
109
+ export function formatPersonaSummary(persona: Persona): any {
110
+ return {
111
+ id: persona.id,
112
+ name: persona.name,
113
+ age: persona.age,
114
+ location: persona.location?.city ? `${persona.location.city}, ${persona.location.state || persona.location.country}` : "Unknown",
115
+ job_title: persona.job?.title || persona.role || "Unknown",
116
+ industry: persona.job?.industry || "Unknown",
117
+ tech_literacy: persona.technology?.tech_literacy || persona.tech_comfort || "Unknown",
118
+ primary_goals: persona.goals?.primary_goals || [],
119
+ pain_points: persona.pain_points || persona.frustrations || [],
120
+ user_story: persona.user_story || "",
121
+ };
122
+ }
123
+
124
+ export function searchPersonas(
125
+ personas: Map<string, Persona>,
126
+ filters: {
127
+ role?: string;
128
+ industry?: string;
129
+ age_range?: string;
130
+ tech_literacy?: string;
131
+ location?: string;
132
+ }
133
+ ): Persona[] {
134
+ const results: Persona[] = [];
135
+
136
+ for (const persona of personas.values()) {
137
+ let matches = true;
138
+
139
+ // Filter by role/job title
140
+ if (filters.role) {
141
+ const personaRole = (persona.job?.title || persona.role || "").toLowerCase();
142
+ if (!personaRole.includes(filters.role.toLowerCase())) {
143
+ matches = false;
144
+ }
145
+ }
146
+
147
+ // Filter by industry
148
+ if (filters.industry && matches) {
149
+ const personaIndustry = (persona.job?.industry || "").toLowerCase();
150
+ if (!personaIndustry.includes(filters.industry.toLowerCase())) {
151
+ matches = false;
152
+ }
153
+ }
154
+
155
+ // Filter by age range (e.g., "20-30", "30-40")
156
+ if (filters.age_range && persona.age && matches) {
157
+ const [minAge, maxAge] = filters.age_range.split("-").map(Number);
158
+ if (persona.age < minAge || persona.age > maxAge) {
159
+ matches = false;
160
+ }
161
+ }
162
+
163
+ // Filter by tech literacy
164
+ if (filters.tech_literacy && matches) {
165
+ const personaTech = (persona.technology?.tech_literacy || persona.tech_comfort || "").toLowerCase();
166
+ if (!personaTech.includes(filters.tech_literacy.toLowerCase())) {
167
+ matches = false;
168
+ }
169
+ }
170
+
171
+ // Filter by location
172
+ if (filters.location && matches) {
173
+ const personaLocation = `${persona.location?.city || ""} ${persona.location?.state || ""} ${persona.location?.country || ""}`.toLowerCase();
174
+ if (!personaLocation.includes(filters.location.toLowerCase())) {
175
+ matches = false;
176
+ }
177
+ }
178
+
179
+ if (matches) {
180
+ results.push(persona);
181
+ }
182
+ }
183
+
184
+ return results;
185
+ }