britbrat0 commited on
Commit
ad3c52d
·
verified ·
1 Parent(s): 1b8132f

Upload 10 files

Browse files
Files changed (10) hide show
  1. README.md +81 -14
  2. ai_helpers.py +96 -0
  3. app.py +187 -0
  4. config.py +40 -0
  5. integration_deployment.md +249 -0
  6. personas.json +1 -0
  7. requirements.txt +3 -3
  8. simulation_algorithm_design.md +195 -0
  9. use_cases.md +55 -0
  10. utils.py +180 -0
README.md CHANGED
@@ -1,19 +1,86 @@
 
 
 
 
 
 
1
  ---
2
- title: Feedback Simulator
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: AI-powered persona feedback simulator
 
 
12
  ---
13
 
14
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
1
+
2
+ # Persona Feedback Simulation App
3
+
4
+ A **Streamlit-based app** for generating realistic persona feedback using the OpenAI API.
5
+ This version includes production-ready improvements such as modular design, authentication support, cloud storage, and monitoring hooks.
6
+
7
  ---
8
+
9
+ ## Features
10
+
11
+ - **Persona management**: Create, edit, and simulate personas.
12
+ - **OpenAI API integration**: Generate realistic feedback automatically.
13
+ - **Database support**: Optional Supabase or SQLite backend.
14
+ - **Secure API key handling**: Avoid exposing secrets.
15
+ - **Modular codebase**: Separate app, utilities, and database logic.
16
+ - **Deployment ready**: Works on Streamlit Cloud or GitHub Codespaces.
17
+ - **Monitoring hooks**: Metrics and logging for production use.
18
+
19
+
20
  ---
21
 
22
+ ## Setup & Installation
23
+
24
+ 1. **Clone the repository**
25
+
26
+ ```bash
27
+ git clone https://github.com/yourusername/persona-feedback-app.git
28
+ cd persona-feedback-app
29
+ ```
30
+
31
+ 2. **Create a virtual environment**
32
+
33
+ ```bash
34
+ python -m venv venv
35
+ source venv/bin/activate # macOS/Linux
36
+ venv\Scripts\activate # Windows
37
+ ```
38
+
39
+ 3. **Install dependencies**
40
+
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ 4. **Set your OpenAI API key**
46
+
47
+ ```bash
48
+ export OPENAI_API_KEY="your_api_key_here" # macOS/Linux
49
+ setx OPENAI_API_KEY "your_api_key_here" # Windows
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Running the App
55
+
56
+ ```bash
57
+ streamlit run app.py
58
+ ```
59
+
60
+ - Open the browser link provided by Streamlit.
61
+ - Create or select personas, input features, and generate feedback.
62
+
63
+ ---
64
+
65
+ ## Testing
66
+
67
+ Unit tests use **pytest** and can be run with:
68
+
69
+ ```bash
70
+ pytest
71
+ ```
72
+
73
+ - Tests cover:
74
+ - Response generation (small, empty, large input)
75
+ - Persona management
76
+ - Concurrency handling
77
+
78
+ **Tip:** You can mock OpenAI responses for offline testing.
79
+
80
+ ---
81
 
82
+ ## Deployment
83
 
84
+ - **Streamlit Cloud**: Push your repo and configure the secrets (API keys) in the app settings.
85
+ - **GitHub Codespaces**: Works in the dev container with all dependencies installed.
86
+ - **Optional database**: Use SQLite for local testing or Supabase for cloud storage.
ai_helpers.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+ import logging
3
+ import time
4
+ from typing import List, Dict, Optional
5
+ from config import OPENAI_DEFAULTS, REPORT_DEFAULTS
6
+ from utils import build_sentiment_summary, extract_persona_response # utils in same package
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ def build_prompt(personas: List[Dict], feature_inputs: Dict, conversation_history: str = "") -> str:
11
+ """Construct a compact prompt for the chat model."""
12
+ persona_block = "\n".join(
13
+ f"- {p['name']} ({p['occupation']}, {p.get('location','')}, Tech: {p.get('tech_proficiency','')})"
14
+ for p in personas
15
+ )
16
+ feature_block = ""
17
+ for k, v in feature_inputs.items():
18
+ vtxt = ", ".join(v) if isinstance(v, list) else (v or "")
19
+ feature_block += f"{k}:\n{vtxt}\n\n"
20
+
21
+ prompt = f"""
22
+ Personas:
23
+ {persona_block}
24
+
25
+ Features:
26
+ {feature_block}
27
+
28
+ Simulate a realistic persona conversation. Each persona should reply in 2-3 sentences.
29
+ Use this template for each persona:
30
+
31
+ [Persona Name]:
32
+ - Response: <what they say>
33
+ - Reasoning: <why>
34
+ - Confidence: <High|Medium|Low>
35
+ - Suggested follow-up: <question>
36
+
37
+ """
38
+ if conversation_history:
39
+ prompt += f"\nPrevious conversation:\n{conversation_history}\nContinue naturally."
40
+ return prompt.strip()
41
+
42
+ def generate_response(feature_inputs: Dict, personas: List[Dict], history: str, model: str) -> str:
43
+ """Single-shot OpenAI call (may raise exceptions)."""
44
+ prompt = build_prompt(personas, feature_inputs, history)
45
+ resp = openai.chat.completions.create(
46
+ model=model,
47
+ messages=[
48
+ {"role": "system", "content": "You are an AI facilitator for a virtual focus group."},
49
+ {"role": "user", "content": prompt}
50
+ ],
51
+ temperature=OPENAI_DEFAULTS.get("temperature", 0.8),
52
+ max_tokens=OPENAI_DEFAULTS.get("max_tokens", 1500)
53
+ )
54
+ return resp.choices[0].message.content.strip()
55
+
56
+ def generate_response_with_retry(feature_inputs: Dict, personas: List[Dict], history: str, model: str, retries: int = 3, backoff: float = 1.0) -> str:
57
+ """Call OpenAI with retries and exponential backoff."""
58
+ for attempt in range(retries):
59
+ try:
60
+ return generate_response(feature_inputs, personas, history, model)
61
+ except Exception as e:
62
+ log.exception("OpenAI call failed (attempt %s): %s", attempt + 1, e)
63
+ if attempt + 1 < retries:
64
+ time.sleep(backoff * (2 ** attempt))
65
+ else:
66
+ # final failure
67
+ raise
68
+
69
+ def generate_feedback_report(conversation: str, model: str) -> str:
70
+ """Generate a structured feedback report using OpenAI."""
71
+ prompt = f"""
72
+ Analyze the following conversation and create a structured feedback report.
73
+
74
+ Conversation:
75
+ {conversation}
76
+
77
+ Sections:
78
+ - Executive Summary
79
+ - Patterns & Themes
80
+ - Consensus Points
81
+ - Disagreements & Concerns
82
+ - Persona Insights
83
+ - Actionable Recommendations
84
+ - Quantitative Metrics (acceptance %, likelihood per persona, priority)
85
+ - Risk Assessment
86
+ """
87
+ resp = openai.chat.completions.create(
88
+ model=model,
89
+ messages=[
90
+ {"role": "system", "content": "You are an expert product analyst and UX researcher."},
91
+ {"role": "user", "content": prompt}
92
+ ],
93
+ temperature=REPORT_DEFAULTS.get("temperature", 0.7),
94
+ max_tokens=REPORT_DEFAULTS.get("max_tokens", 1500)
95
+ )
96
+ return resp.choices[0].message.content
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import json
3
+ from typing import List, Dict
4
+
5
+ from config import MODEL_CHOICES, DEFAULT_MODEL, DEFAULT_PERSONA_PATH
6
+ from utils import (
7
+ get_personas,
8
+ format_response_line,
9
+ detect_insight_or_concern,
10
+ extract_persona_response,
11
+ build_sentiment_summary,
12
+ build_heatmap_chart,
13
+ save_personas,
14
+ )
15
+ from ai_helpers import generate_response_with_retry, generate_feedback_report
16
+
17
+ # -------------------------
18
+ # Page config & state
19
+ # -------------------------
20
+ st.set_page_config(page_title="Persona Feedback Simulator", page_icon="💬", layout="wide")
21
+
22
+ if "conversation_history" not in st.session_state:
23
+ st.session_state.conversation_history = ""
24
+ if "api_key" not in st.session_state:
25
+ st.session_state.api_key = ""
26
+
27
+ # -------------------------
28
+ # Sidebar: API key, model, personas upload
29
+ # -------------------------
30
+ st.sidebar.header("🔑 API Configuration")
31
+ api_key_input = st.sidebar.text_input("OpenAI API Key", type="password", value=st.session_state.api_key)
32
+ if api_key_input:
33
+ st.session_state.api_key = api_key_input
34
+ import openai
35
+ openai.api_key = api_key_input
36
+ else:
37
+ st.sidebar.info("Enter OpenAI API key to enable generation.")
38
+
39
+ model_choice = st.sidebar.selectbox("Model", MODEL_CHOICES, index=MODEL_CHOICES.index(DEFAULT_MODEL))
40
+
41
+ st.sidebar.markdown("---")
42
+ st.sidebar.header("👥 Personas")
43
+ uploaded = st.sidebar.file_uploader("Upload personas.json", type=["json"])
44
+ personas = get_personas(uploaded, path=DEFAULT_PERSONA_PATH)
45
+ st.sidebar.metric("Total Personas", len(personas))
46
+
47
+ # -------------------------
48
+ # Main UI - Feature input
49
+ # -------------------------
50
+ st.title("💬 Persona Feedback Simulator")
51
+ st.header("📝 Feature Description")
52
+ tabs = st.tabs(["Text", "Files"])
53
+ with tabs[0]:
54
+ text_desc = st.text_area("Describe your feature", height=160)
55
+ with tabs[1]:
56
+ uploaded_files = st.file_uploader("Upload wireframes / mockups", accept_multiple_files=True, type=["png", "jpg", "jpeg", "pdf"])
57
+
58
+ feature_inputs = {
59
+ "Text": text_desc or "",
60
+ "Files": [f.name for f in (uploaded_files or [])]
61
+ }
62
+
63
+ st.markdown("---")
64
+
65
+ # -------------------------
66
+ # Persona selection
67
+ # -------------------------
68
+ st.header("👥 Choose Personas")
69
+ if not personas:
70
+ st.warning("No personas available.")
71
+ selected_personas: List[Dict] = []
72
+ else:
73
+ labels = [f"{p['name']} ({p.get('occupation','')})" for p in personas]
74
+ defaults = labels[:3]
75
+ selected_labels = st.multiselect("Select personas:", labels, default=defaults)
76
+ selected_personas = [p for p in personas if f"{p['name']} ({p.get('occupation','')})" in selected_labels]
77
+
78
+ # -------------------------
79
+ # Ask / Report / Clear controls
80
+ # -------------------------
81
+ st.header("💭 Ask Your Question")
82
+ question = st.text_input("Question to personas")
83
+ c1, c2, c3 = st.columns([2, 2, 1])
84
+ ask_btn = c1.button("🎯 Ask")
85
+ report_btn = c2.button("📊 Generate Report")
86
+ clear_btn = c3.button("🗑️ Clear")
87
+
88
+ if ask_btn:
89
+ if not st.session_state.api_key:
90
+ st.warning("Please set your OpenAI API key in the sidebar.")
91
+ elif not selected_personas:
92
+ st.warning("Please select at least one persona.")
93
+ elif not (question or feature_inputs["Text"]):
94
+ st.warning("Enter a question or feature description.")
95
+ else:
96
+ if question:
97
+ st.session_state.conversation_history += f"\n**User:** {question}\n"
98
+ with st.spinner("Generating persona responses..."):
99
+ try:
100
+ resp = generate_response_with_retry(feature_inputs, selected_personas, st.session_state.conversation_history, model_choice)
101
+ st.session_state.conversation_history += resp + "\n"
102
+ st.rerun()
103
+ except Exception as e:
104
+ st.error(f"Failed to generate response: {e}")
105
+
106
+ if report_btn:
107
+ if not st.session_state.conversation_history.strip():
108
+ st.warning("Nothing to analyze yet.")
109
+ else:
110
+ with st.spinner("Generating feedback report..."):
111
+ try:
112
+ report = generate_feedback_report(st.session_state.conversation_history, model_choice)
113
+ st.markdown("## 📊 Feedback Report")
114
+ st.markdown(report)
115
+ st.download_button("⬇️ Download Report", report, "persona_report.md")
116
+ except Exception as e:
117
+ st.error(f"Failed to generate report: {e}")
118
+
119
+ if clear_btn:
120
+ st.session_state.conversation_history = ""
121
+ st.rerun()
122
+
123
+ st.markdown("---")
124
+
125
+ # -------------------------
126
+ # Conversation display + heatmap
127
+ # -------------------------
128
+ st.header("💬 Conversation History")
129
+ if st.session_state.conversation_history.strip() and selected_personas:
130
+ lines = [ln for ln in st.session_state.conversation_history.split("\n") if ln.strip()]
131
+
132
+ # Display conversation lines with persona formatting
133
+ for line in lines:
134
+ matched = False
135
+ for p in selected_personas:
136
+ if line.startswith(p["name"]):
137
+ response_text = extract_persona_response(line)
138
+ hl = detect_insight_or_concern(response_text)
139
+ st.markdown(format_response_line(line, p["name"], hl), unsafe_allow_html=True)
140
+ matched = True
141
+ break
142
+ if not matched:
143
+ # user or neutral lines
144
+ st.markdown(line)
145
+
146
+ st.info("💡 Continue the discussion using the **question field above** to ask a follow-up question.")
147
+
148
+ # Build & show heatmap
149
+ df_summary = build_sentiment_summary(lines, selected_personas)
150
+ chart = build_heatmap_chart(df_summary)
151
+ st.markdown("## 🔥 Persona Sentiment Heatmap")
152
+ st.altair_chart(chart, use_container_width=True)
153
+ else:
154
+ st.info("No conversation yet. Ask your personas a question to get started!")
155
+
156
+ # -------------------------
157
+ # Sidebar persona creation (persist)
158
+ # -------------------------
159
+ st.sidebar.markdown("---")
160
+ st.sidebar.header("➕ Create Persona")
161
+ with st.sidebar.form("new_persona_form"):
162
+ name = st.text_input("Name*")
163
+ occupation = st.text_input("Occupation*")
164
+ location = st.text_input("Location")
165
+ tech = st.selectbox("Tech Proficiency", ["Low", "Medium", "High"])
166
+ traits = st.text_area("Behavioral traits (comma-separated)")
167
+ submit = st.form_submit_button("Add Persona")
168
+ if submit:
169
+ if not name or not occupation:
170
+ st.sidebar.error("Name and Occupation required.")
171
+ else:
172
+ new_p = {
173
+ "id": f"p{len(personas)+1}",
174
+ "name": name.strip(),
175
+ "occupation": occupation.strip(),
176
+ "location": location.strip() or "Unknown",
177
+ "tech_proficiency": tech,
178
+ "behavioral_traits": [t.strip() for t in traits.split(",") if t.strip()]
179
+ }
180
+ personas.append(new_p)
181
+ if save_personas(personas, path=DEFAULT_PERSONA_PATH):
182
+ st.sidebar.success("✅ Persona added and saved.")
183
+ else:
184
+ st.sidebar.error("❌ Persona added but failed to save.")
185
+ st.rerun()
186
+
187
+ st.sidebar.metric("Total Personas", len(personas))
config.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # -------------------------
4
+ # Model and API Config
5
+ # -------------------------
6
+
7
+ MODEL_CHOICES = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
8
+ DEFAULT_MODEL = "gpt-4o-mini"
9
+
10
+ OPENAI_DEFAULTS = {
11
+ "temperature": 0.8,
12
+ "max_tokens": 2000
13
+ }
14
+
15
+ REPORT_DEFAULTS = {
16
+ "temperature": 0.7,
17
+ "max_tokens": 2500
18
+ }
19
+
20
+ # -------------------------
21
+ # Persona Colors
22
+ # -------------------------
23
+ PERSONA_COLORS = {
24
+ "Sophia Martinez": "#E6194B",
25
+ "Jamal Robinson": "#3CB44B",
26
+ "Eleanor Chen": "#FFE119",
27
+ "Diego Alvarez": "#4363D8",
28
+ "Anita Patel": "#F58231",
29
+ "Robert Klein": "#911EB4",
30
+ "Nia Thompson": "#46F0F0",
31
+ "Marcus Green": "#F032E6",
32
+ "Aisha Mbatha": "#BCF60C",
33
+ "Owen Gallagher": "#FABEBE",
34
+ }
35
+
36
+ # -------------------------
37
+ # File Paths
38
+ # -------------------------
39
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
40
+ DEFAULT_PERSONA_PATH = os.path.join(BASE_DIR, "personas.json")
integration_deployment.md ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Persona Feedback Simulator – Integration & Deployment
2
+
3
+ ## Table of Contents
4
+ 1. [Overview](#overview)
5
+ 2. [System Architecture](#system-architecture)
6
+ 3. [Prerequisites](#prerequisites)
7
+ 4. [Installation and Setup](#installation-and-setup)
8
+ 5. [Configuration](#configuration)
9
+ 6. [Database / Data Storage](#database--data-storage)
10
+ 7. [API Specifications](#api-specifications)
11
+ 8. [Deployment Procedures](#deployment-procedures)
12
+ 9. [Monitoring and Metrics](#monitoring-and-metrics)
13
+ 10. [Maintenance and Update Procedures](#maintenance-and-update-procedures)
14
+ 11. [Troubleshooting](#troubleshooting)
15
+ 12. [Backup and Recovery](#backup-and-recovery)
16
+
17
+ ---
18
+
19
+ ## 1. Overview
20
+ The **Persona Feedback Simulator** is a Streamlit-based application that allows users to simulate conversations among virtual personas regarding product features. The system is designed to support:
21
+
22
+ - Multiple personas stored in `personas.json`
23
+ - AI-driven responses via OpenAI models (`gpt-4o-mini` recommended)
24
+ - Automatic backup and recovery of persona data
25
+ - Monitoring and observability via Prometheus metrics
26
+ - Scalable deployment with load balancing
27
+
28
+ ---
29
+
30
+ ## 2. System Architecture
31
+ **Components:**
32
+ 1. **Streamlit App** (`app.py`)
33
+ - Handles UI, persona selection, question submission, AI integration, and persona responses.
34
+ 2. **Prometheus Metrics** (`Counter`, `Histogram`)
35
+ - Exposes metrics on `/metrics` for request counts, response times, and errors.
36
+ 3. **Load Balancer** (NGINX)
37
+ - Balances traffic between multiple containerized app instances.
38
+ 4. **Persistent Storage** (`personas.json` + backup `personas_backup.json`)
39
+ - Stores persona definitions and allows safe recovery.
40
+ 5. **Optional Cloud Integration**
41
+ - Can use S3, GCS, or a database backend for production durability.
42
+
43
+ **Diagram:**
44
+
45
+ ```
46
+ [User Browser] --> [NGINX Load Balancer] --> [Streamlit App Containers]
47
+ \--> [Prometheus Metrics]
48
+ \--> [Persistent Storage / Backup]
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 3. Prerequisites
54
+ - **Python 3.11+** (tested with 3.13)
55
+ - **Docker & Docker Compose** for containerized deployment
56
+ - **OpenAI API key** for AI responses
57
+ - **Prometheus** for metrics collection (optional, but recommended)
58
+
59
+ **Python packages** (in `requirements.txt`):
60
+ ```
61
+ streamlit
62
+ openai
63
+ prometheus_client
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 4. Installation and Setup
69
+
70
+ 1. **Clone the repository:**
71
+ ```bash
72
+ git clone https://github.com/<your-username>/persona-feedback-simulator.git
73
+ cd persona-feedback-simulator
74
+ ```
75
+
76
+ 2. **Set OpenAI API Key:**
77
+ ```bash
78
+ export OPENAI_API_KEY="sk-..."
79
+ ```
80
+
81
+ 3. **Install dependencies locally (optional):**
82
+ ```bash
83
+ python -m venv venv
84
+ source venv/bin/activate
85
+ pip install -r requirements.txt
86
+ ```
87
+
88
+ 4. **Run the app locally:**
89
+ ```bash
90
+ streamlit run app.py
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 5. Configuration
96
+
97
+ ### Environment Variables
98
+ - `OPENAI_API_KEY` – OpenAI authentication
99
+ - `PROMETHEUS_PORT` – Optional, defaults to 8000
100
+ - `STREAMLIT_PORT` – Optional, defaults to 8501
101
+
102
+ ### Configuration Files
103
+ - `personas.json` – Primary persona definitions
104
+ - `personas_backup.json` – Auto-backup file
105
+ - `prometheus.yml` – Prometheus scrape configuration
106
+ - `nginx.conf` – Load balancer configuration
107
+
108
+ ---
109
+
110
+ ## 6. Database / Data Storage
111
+ Currently uses **JSON files**:
112
+
113
+ ### `personas.json`
114
+ | Field | Type | Description |
115
+ |-------|------|-------------|
116
+ | id | integer | Unique persona ID |
117
+ | name | string | Persona full name |
118
+ | occupation | string | Job title |
119
+ | location | string | Geographical location |
120
+ | tech_proficiency | string | "Low", "Medium", or "High" |
121
+ | behavioral_traits | array[string] | List of persona traits |
122
+
123
+ ### Backup Strategy
124
+ - Automatic backup to `personas_backup.json` whenever personas are added or modified.
125
+ - Restore automatically if `personas.json` is missing or corrupted.
126
+
127
+ ---
128
+
129
+ ## 7. API Specifications
130
+
131
+ ### 7.1 Internal App Functions
132
+ | Function | Endpoint/Usage | Description |
133
+ |----------|----------------|-------------|
134
+ | `generate_response` | Internal | Sends feature input + persona data to OpenAI and returns conversation |
135
+ | `generate_feedback_report` | Internal | Summarizes conversation with actionable insights |
136
+ | `instrumented` | Decorator | Wraps functions for Prometheus metrics (`REQUEST_COUNTER`, `RESPONSE_TIME`) |
137
+
138
+ ### 7.2 Prometheus Metrics
139
+ Exposed at `/metrics` (default port 8000):
140
+
141
+ | Metric | Type | Labels | Description |
142
+ |--------|------|--------|-------------|
143
+ | `app_requests_total` | Counter | `endpoint`, `status` | Counts total requests and success/error outcomes |
144
+ | `app_response_time_seconds` | Histogram | `endpoint` | Measures response duration per endpoint |
145
+ | `app_heartbeat` | Gauge | – | Optional: timestamp for health checks |
146
+
147
+ ### 7.3 Health Endpoint
148
+ Optional `/healthz` endpoint for load balancer or Kubernetes:
149
+
150
+ ```python
151
+ @app.route("/healthz")
152
+ def health():
153
+ return "OK", 200
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 8. Deployment Procedures
159
+
160
+ ### 8.1 Docker Deployment
161
+ **Dockerfile**
162
+ ```dockerfile
163
+ WORKDIR /app
164
+ COPY . /app
165
+ RUN pip install --no-cache-dir -r requirements.txt
166
+ EXPOSE 8501 8000
167
+ CMD ["streamlit", "run", "app.py", "--server.port=8501"]
168
+ ```
169
+
170
+ **Docker Compose**
171
+ ```yaml
172
+ version: '3'
173
+ services:
174
+ app1:
175
+ build: .
176
+ ports:
177
+ - "8501:8501"
178
+ environment:
179
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
180
+ nginx:
181
+ image: nginx:latest
182
+ ports:
183
+ - "80:80"
184
+ volumes:
185
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 9. Monitoring and Metrics
191
+
192
+ 1. **Prometheus Scraping**
193
+ ```yaml
194
+ scrape_configs:
195
+ - job_name: 'persona_app'
196
+ static_configs:
197
+ - targets: ['app1:8000', 'app2:8000', 'app3:8000']
198
+ ```
199
+
200
+ 2. **Metrics to Watch**
201
+ - Request counts per endpoint
202
+ - Error rate
203
+ - Response latency
204
+ - Heartbeat (optional)
205
+
206
+ 3. **Optional Visualization**
207
+ - Connect Prometheus to Grafana dashboards
208
+ - Create alerts for:
209
+ - High error rate
210
+ - Service unavailability
211
+ - Slow response times
212
+
213
+ ---
214
+
215
+ ## 10. Maintenance and Update Procedures
216
+
217
+ - **Update Python packages:** `pip install -r requirements.txt --upgrade`
218
+ - **Update app code:** Pull latest from GitHub and rebuild Docker containers
219
+ - **Persona updates:** Add through UI, then backup occurs automatically
220
+ - **Rolling restart:** Use Docker Compose `up -d --force-recreate` or Kubernetes Deployment rolling update
221
+ - **Configuration changes:** Update `nginx.conf`, `prometheus.yml`, or `.env` variables and reload respective services
222
+
223
+ ---
224
+
225
+ ## 11. Troubleshooting
226
+
227
+ | Issue | Solution |
228
+ |-------|---------|
229
+ | `personas.json` not found | Ensure it exists next to `app.py` or restore from backup |
230
+ | Duplicated Prometheus metrics | Use custom registry or `st.cache_resource` (see metrics setup) |
231
+ | OpenAI errors | Verify `OPENAI_API_KEY` and network connectivity |
232
+ | Streamlit reload issues | Clear browser cache or restart app container |
233
+
234
+ ---
235
+
236
+ ## 12. Backup and Recovery
237
+
238
+ 1. **Automatic Backup:** Occurs whenever personas are added or modified → saved to `personas_backup.json`.
239
+ 2. **Restore Backup:** If `personas.json` missing or corrupted, app restores from backup on startup.
240
+ 3. **Manual Backup Trigger:** Call `backup_personas()` in code after persona updates.
241
+ 4. **Optional Cloud Backup:** Sync `personas_backup.json` to S3/GCS for off-site durability.
242
+
243
+ ---
244
+
245
+ ### Notes for Developers
246
+ - Always maintain a **clean `personas.json`** in version control for initial deployments.
247
+ - Use the **backup/restore helpers** to prevent accidental persona data loss.
248
+ - Keep **metrics endpoints open** only for internal monitoring (don’t expose publicly without authentication).
249
+
personas.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"id":1,"name":"Sophia Martinez","age":34,"gender":"Female","occupation":"Marketing Manager","location":"Austin, Texas","tech_proficiency":"High","income_level":"Upper-Middle","interests":["data analytics","digital campaigns","branding","travel"],"personality":"Analytical, persuasive, pragmatic","communication_style":"Professional but approachable, uses marketing jargon","behavioral_traits":["Goal-oriented","Metrics-driven","Team collaborator"],"product_preferences":"Loves features that provide measurable ROI or improve productivity"},{"id":2,"name":"Jamal Robinson","age":22,"gender":"Male","occupation":"College Student (Computer Science)","location":"Chicago, Illinois","tech_proficiency":"Expert","income_level":"Low","interests":["gaming","AI","social media","open-source projects"],"personality":"Curious, outspoken, experimental","communication_style":"Casual and meme-friendly, uses tech slang","behavioral_traits":["Early adopter","Enjoys tinkering","Socially engaged online"],"product_preferences":"Values innovation, customization, and gamification"},{"id":3,"name":"Eleanor Chen","age":46,"gender":"Female","occupation":"High School Teacher","location":"San Francisco, California","tech_proficiency":"Moderate","income_level":"Middle","interests":["education","reading","community work","family time"],"personality":"Patient, empathetic, articulate","communication_style":"Clear and thoughtful, educational tone","behavioral_traits":["Cautious adopter","Prefers reliability","Community-minded"],"product_preferences":"Values simplicity, clarity, and ethical design"},{"id":4,"name":"Diego Alvarez","age":29,"gender":"Male","occupation":"UX Designer","location":"Barcelona, Spain","tech_proficiency":"High","income_level":"Upper-Middle","interests":["design thinking","photography","user research","cycling"],"personality":"Creative, detail-oriented, empathetic","communication_style":"Reflective, uses design terminology","behavioral_traits":["Empathy-driven","Iterative thinker","User advocate"],"product_preferences":"Values aesthetics, usability, and accessibility"},{"id":5,"name":"Anita Patel","age":39,"gender":"Female","occupation":"Small Business Owner (Online Boutique)","location":"Toronto, Canada","tech_proficiency":"Moderate","income_level":"Middle","interests":["fashion","entrepreneurship","social media marketing","family"],"personality":"Driven, pragmatic, social","communication_style":"Friendly and informal, with business undertones","behavioral_traits":["Risk-taker","Customer-focused","Budget-conscious"],"product_preferences":"Loves tools that automate tasks and boost sales"},{"id":6,"name":"Robert Klein","age":61,"gender":"Male","occupation":"Retired Engineer","location":"Munich, Germany","tech_proficiency":"Intermediate","income_level":"Upper-Middle","interests":["gardening","woodworking","technology trends","cycling"],"personality":"Logical, patient, skeptical","communication_style":"Methodical and concise","behavioral_traits":["Fact-driven","Early-morning routine","Skeptical of marketing hype"],"product_preferences":"Prefers durability, data transparency, and reliability"},{"id":7,"name":"Nia Thompson","age":27,"gender":"Female","occupation":"Social Media Influencer","location":"Los Angeles, California","tech_proficiency":"Expert","income_level":"High","interests":["photography","fashion","travel","wellness"],"personality":"Charismatic, spontaneous, expressive","communication_style":"Conversational, emotional, uses emojis","behavioral_traits":["Trend-sensitive","Highly visual","Collaborative"],"product_preferences":"Values aesthetics, social shareability, and novelty"},{"id":8,"name":"Marcus Green","age":51,"gender":"Male","occupation":"Construction Site Manager","location":"Birmingham, UK","tech_proficiency":"Low","income_level":"Middle","interests":["sports","DIY projects","family outings","local pubs"],"personality":"Down-to-earth, straightforward, humorous","communication_style":"Direct and practical","behavioral_traits":["Skeptical of new tech","Hands-on learner","Prefers face-to-face"],"product_preferences":"Wants clear value, reliability, and low maintenance"},{"id":9,"name":"Aisha Mbatha","age":31,"gender":"Female","occupation":"Public Health Researcher","location":"Nairobi, Kenya","tech_proficiency":"High","income_level":"Middle","interests":["data science","policy analysis","volunteering","fitness"],"personality":"Curious, data-driven, empathetic","communication_style":"Balanced between analytical and conversational","behavioral_traits":["Research-oriented","Socially conscious","Detail-focused"],"product_preferences":"Values privacy, inclusivity, and evidence-based features"},{"id":10,"name":"Owen Gallagher","age":42,"gender":"Male","occupation":"Truck Driver","location":"Dublin, Ireland","tech_proficiency":"Low","income_level":"Working Class","interests":["road trips","radio shows","sports","family"],"personality":"Friendly, patient, no-nonsense","communication_style":"Informal, storytelling tone","behavioral_traits":["Routine-oriented","Practical","Prefers stability"],"product_preferences":"Wants ease of use, low cost, and safety features"}]
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- altair
2
- pandas
3
- streamlit
 
1
+ streamlit>=1.20
2
+ openai>=0.27.0
3
+ prometheus-client>=0.16.0
simulation_algorithm_design.md ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Simulation Algorithm Design
2
+
3
+ This is a detailed overview of the simulation
4
+ algorithm that powers the persona-based feedback system. It explains
5
+ how personas are modeled, how user feature descriptions are processed,
6
+ how conversations are generated, and how the system synthesizes feedback
7
+ into structured insights. It also outlines the underlying AI
8
+ architecture and decision-making processes.
9
+
10
+ ------------------------------------------------------------------------
11
+
12
+ ## Persona Modeling
13
+
14
+ Personas represent simulated users with distinct
15
+ characteristics, demographics, and communication styles. Each persona is
16
+ defined as a structured JSON object with the following schema:
17
+
18
+ ``` json
19
+ {
20
+ "id":1,
21
+ "name":"Sophia Martinez"
22
+ "age":34
23
+ "gender":"Female"
24
+ "occupation":"Marketing Manager"
25
+ "location":"Austin, Texas"
26
+ "tech_proficiency":"High"
27
+ "income_level":"Upper-Middle"
28
+ "interests":["data analytics","digital campaigns","branding","travel"]
29
+ "personality":"Analytical, persuasive, pragmatic"
30
+ "communication_style":"Professional but approachable, uses marketing jargon"
31
+ "behavioral_traits":["Goal-oriented","Metrics-driven","Team collaborator"]
32
+ "product_preferences":"Loves features that provide measurable ROI or improve productivity"
33
+ }
34
+ ```
35
+
36
+ ### Key Persona Attributes
37
+
38
+ - **Occupation:** Defines the professional role and informs domain-specific vocabulary or reasoning.
39
+ - **Tech Proficiency:** A categorical indicator (“Low”, “Medium”, “High”) determining familiarity with digital tools or technical language.
40
+ - **Personality:** Descriptive traits that guide tone and emotional expression (e.g., analytical, creative, empathetic).
41
+ - **Behavioral Traits:** A list of tendencies shaping decision-making and feedback approach (e.g., goal-oriented, cautious).
42
+ - **Product Preferences:** Describes what the persona values in products or features — such as measurable ROI, innovation, or usability.
43
+
44
+ ### Behavioral Logic
45
+
46
+ Each persona follows a set of rule-based and probabilistic logic for
47
+ generating feedback. The simulation ensures diverse, realistic outputs
48
+ by adjusting response tone and focus based on both persona attributes
49
+ and conversation history.
50
+
51
+ ------------------------------------------------------------------------
52
+
53
+ ## Feature Description Processing
54
+
55
+ Feature descriptions --- e.g., project summaries, data reports, or
56
+ design specifications --- are preprocessed before being fed into
57
+ personas.
58
+
59
+ ### Steps:
60
+
61
+ 1. **Text Cleaning:** Remove formatting artifacts, redundant
62
+ whitespace, and HTML tags.
63
+ 2. **Semantic Parsing:** Break down input into feature units (e.g.,
64
+ "target users," "goals," "methodology").
65
+ 3. **Embedding Generation:** Convert text into dense vector embeddings
66
+ via an OpenAI text embedding model.
67
+ 4. **Contextual Weighting:** Assign higher weights to sections
68
+ containing keywords like *impact*, *risk*, or *improvement
69
+ opportunity*.
70
+ 5. **Persona-Specific Filtering:** Each persona receives a subset of
71
+ features relevant to their domain.
72
+
73
+ This ensures that feedback remains context-aware and targeted to
74
+ persona expertise.
75
+
76
+ ------------------------------------------------------------------------
77
+
78
+ ## Conversation Generation
79
+
80
+ Conversations simulate an interactive critique between the user and
81
+ multiple personas. The process follows a turn-based design:
82
+
83
+ ### Workflow
84
+
85
+ 1. **User Prompt Ingestion:** The user provides a question or
86
+ description.
87
+ 2. **Persona Response Loop:**
88
+ - Each persona analyzes the prompt through its embedding and
89
+ memory context.
90
+ - The persona's decision engine generates a structured response
91
+ using an OpenAI language model (e.g., GPT-5).
92
+ - Responses are tagged with persona metadata and colorized for
93
+ display.
94
+ 3. **History Integration:** Previous interactions are appended to the
95
+ persona's memory for continuity.
96
+ 4. **Adaptive Tone Adjustment:** The persona adjusts its verbosity or
97
+ formality based on conversation depth and user sentiment.
98
+
99
+ ### Pseudo-code Example
100
+
101
+ ``` python
102
+ for persona in personas:
103
+ context = build_context(user_input, persona.memory)
104
+ response = generate_response(model="gpt-5", role=persona.role, tone=persona.tone, context=context)
105
+ update_history(persona, response)
106
+ ```
107
+
108
+ ------------------------------------------------------------------------
109
+
110
+ ## Feedback Synthesis
111
+
112
+ After generating individual persona responses, the system produces a
113
+ summary report highlighting key insights and concerns.
114
+
115
+ ### Steps:
116
+
117
+ 1. **Response Extraction:** Parse persona outputs into structured units
118
+ (feedback statements).
119
+ 2. **Topic Clustering:** Group feedback by themes (clarity, engagement,
120
+ usability, etc.).
121
+ 3. **Sentiment Weighting:** Use semantic analysis to determine whether
122
+ comments are positive, neutral, or critical.
123
+ 4. **Insight-Concern Classification:** Mark feedback as *insight*
124
+ (constructive) or *concern* (problematic).
125
+ 5. **Visualization Layer:** Generate bar charts, sentiment histograms,
126
+ and keyword clouds for report display.
127
+
128
+ The synthesized report provides **multi-perspective feedback** combining
129
+ all personas' evaluations.
130
+
131
+ ------------------------------------------------------------------------
132
+
133
+ ## Underlying AI Architecture
134
+
135
+ The simulation architecture combines symbolic reasoning (rules,
136
+ goals, and persona metadata) with neural generation (GPT-5
137
+ responses).
138
+
139
+ ### Core Components
140
+
141
+ - **Persona Memory:** Tracks each persona's previous statements and
142
+ context.
143
+ - **Embedding Engine:** Converts text into vectorized meaning for
144
+ cross-referencing and relevance scoring.
145
+ - **Response Generator:** A large language model (LLM) that interprets
146
+ both persona context and user input.
147
+ - **Feedback Synthesizer:** Aggregates persona responses into
148
+ structured insights.
149
+
150
+ ### Decision-Making Flow
151
+
152
+ ``` mermaid
153
+ flowchart TD
154
+ A[User Input] --> B[Feature Preprocessing]
155
+ B --> C[Persona Loop]
156
+ C -->|Contextual Prompt| D[GPT-5 Response Generator]
157
+ D --> E[Persona Response Memory]
158
+ E --> F[Feedback Synthesis Module]
159
+ F --> G[Visualization + Summary Report]
160
+ ```
161
+
162
+ This hybrid architecture ensures responses are contextually
163
+ consistent, behaviorally diverse, and analytically useful.
164
+
165
+ ------------------------------------------------------------------------
166
+
167
+ ## Decision-Making Processes
168
+
169
+ Each persona's decision-making combines deterministic and probabilistic
170
+ components:
171
+
172
+ **Goal Alignment:** Checks how closely input aligns with persona's goals.
173
+
174
+ **Contextual Memory Recall:** Retrieves relevant parts of previous interactions.
175
+
176
+ **Response Scoring:** Evaluates multiple candidate responses using an internal reward model (clarity, tone, novelty).
177
+
178
+ **Tone Calibration:** Adjusts phrasing style (formal, critical, supportive).
179
+
180
+ **Adaptive Weighting:** Balances between deterministic persona behavior and stochastic creativity from GPT generation.
181
+
182
+ -----------------------------------------------------------------------
183
+
184
+ ------------------------------------------------------------------------
185
+
186
+ ## Summary
187
+
188
+ This simulation framework enables multi-persona feedback generation
189
+ by integrating structured persona modeling, semantic text
190
+ processing, LLM-driven dialogue generation, Automated feedback
191
+ synthesis and visualization
192
+
193
+ Together, these elements create a robust, flexible system for
194
+ simulated evaluation and insight generation across creative,
195
+ technical, and analytical domains.
use_cases.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Cases
2
+
3
+ ---
4
+
5
+ ## 1. Early Concept Validation
6
+ **Industry:** Healthcare Technology
7
+ **Use:** Simulate patients, clinicians, and compliance officers to evaluate early UI sketches for a self-reporting app.
8
+ **Value:** Detects accessibility, workflow, and privacy concerns before prototyping.
9
+
10
+ ---
11
+
12
+ ## 2. UI & UX Refinement
13
+ **Industry:** Fintech
14
+ **Feature:** Interactive dashboards or visual elements.
15
+ **Use:** Personas with varying financial literacy assess clarity and customization.
16
+ **Value:** Reveals gaps between novice and expert users, guiding data simplification.
17
+
18
+ ---
19
+
20
+ ## 3. Workflow Optimization
21
+ **Industry:** Manufacturing / Industrial IoT
22
+ **Feature:** Alert systems and automation workflows.
23
+ **Use:** Simulated technicians and managers review alert logic and workload balance.
24
+ **Value:** Improves efficiency and reduces false alerts.
25
+
26
+ ---
27
+
28
+ ## 4. Content & Messaging Feedback
29
+ **Industry:** Consumer Marketing
30
+ **Feature:** Campaign tone and message framing.
31
+ **Use:** Personas test reactions to sustainability or brand claims.
32
+ **Value:** Refines balance between emotional appeal and factual credibility.
33
+
34
+ ---
35
+
36
+ ## 5. Detailed Design Feedback
37
+ **Industry:** SaaS / Productivity Tools
38
+ **Feature:** UI controls, filter logic, and data views.
39
+ **Use:** Simulated roles (manager, developer, executive) evaluate usability.
40
+ **Value:** Supports adaptive design for multiple user roles.
41
+
42
+ ---
43
+
44
+ ## 6. Pre-Launch Validation
45
+ **Industry:** eCommerce
46
+ **Feature:** Recommendation systems and personalization logic.
47
+ **Use:** Personas emulate shoppers with diverse goals and budgets.
48
+ **Value:** Identifies algorithm bias and improves relevance before release.
49
+
50
+ ---
51
+
52
+ ## 7. Cross-Functional Alignment
53
+ **Industry:** Enterprise Software
54
+ **Use:** Personas representing legal, technical, and user teams simulate feedback sessions.
55
+ **Value:** Accelerates stakeholder consensus through synthetic dialogue.
utils.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import altair as alt
6
+ import logging
7
+ from typing import List, Dict, Optional
8
+ from config import DEFAULT_PERSONA_PATH, PERSONA_COLORS as CONFIG_PERSONA_COLORS
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ # Local color cache that starts from config's colors (if provided)
13
+ PERSONA_COLORS = dict(CONFIG_PERSONA_COLORS) if isinstance(CONFIG_PERSONA_COLORS, dict) else {}
14
+
15
+ # -------------------------
16
+ # Personas I/O & validation
17
+ # -------------------------
18
+ def load_personas_from_file(path: str = DEFAULT_PERSONA_PATH) -> List[Dict]:
19
+ """Load personas from JSON file. Returns [] on errors."""
20
+ try:
21
+ with open(path, "r", encoding="utf-8") as f:
22
+ data = json.load(f)
23
+ if not isinstance(data, list):
24
+ st.warning(f"⚠️ {path} does not contain a list. Returning empty personas.")
25
+ return []
26
+ return data
27
+ except FileNotFoundError:
28
+ log.info("personas file not found: %s", path)
29
+ return []
30
+ except json.JSONDecodeError as e:
31
+ st.error(f"❌ Malformed JSON in {path}: {e}")
32
+ return []
33
+ except Exception as e:
34
+ st.error(f"❌ Unexpected error loading {path}: {e}")
35
+ return []
36
+
37
+ def get_personas(uploaded_file=None, path: str = DEFAULT_PERSONA_PATH) -> List[Dict]:
38
+ """
39
+ Return personas. If uploaded_file is provided (streamlit UploadedFile),
40
+ attempt to load and replace saved personas.
41
+ """
42
+ personas = load_personas_from_file(path)
43
+
44
+ if uploaded_file:
45
+ try:
46
+ imported = json.load(uploaded_file)
47
+ if not isinstance(imported, list):
48
+ st.error("Uploaded persona file must be a JSON list.")
49
+ else:
50
+ personas = imported
51
+ # try to persist
52
+ try:
53
+ with open(path, "w", encoding="utf-8") as f:
54
+ json.dump(personas, f, indent=2)
55
+ st.success("✅ Personas imported and saved successfully!")
56
+ except Exception as e:
57
+ st.error(f"❌ Could not save uploaded personas to disk: {e}")
58
+ except json.JSONDecodeError:
59
+ st.error("❌ Uploaded file contains invalid JSON.")
60
+ except Exception as e:
61
+ st.error(f"❌ Error reading uploaded file: {e}")
62
+
63
+ return personas
64
+
65
+ def validate_persona(persona: Dict) -> bool:
66
+ """Basic validation for persona structure."""
67
+ required = ["name", "occupation", "tech_proficiency", "behavioral_traits"]
68
+ for r in required:
69
+ if r not in persona or persona[r] in (None, "", []):
70
+ return False
71
+ if not isinstance(persona.get("behavioral_traits", []), list):
72
+ return False
73
+ return True
74
+
75
+ def save_personas(personas: List[Dict], path: str = DEFAULT_PERSONA_PATH) -> bool:
76
+ """Persist personas to disk. Returns True on success."""
77
+ try:
78
+ with open(path, "w", encoding="utf-8") as f:
79
+ json.dump(personas, f, indent=2)
80
+ return True
81
+ except Exception as e:
82
+ st.error(f"❌ Could not save personas: {e}")
83
+ log.exception("save_personas failed")
84
+ return False
85
+
86
+ # -------------------------
87
+ # Display & formatting
88
+ # -------------------------
89
+ def get_color_for_persona(name: str) -> str:
90
+ """Return or generate a stable hex color for a persona name."""
91
+ if name not in PERSONA_COLORS:
92
+ PERSONA_COLORS[name] = f"#{(hash(name) & 0xFFFFFF):06x}"
93
+ return PERSONA_COLORS[name]
94
+
95
+ def format_response_line(text: str, persona_name: str, highlight: Optional[str] = None) -> str:
96
+ """Return HTML string for styled persona line."""
97
+ color = get_color_for_persona(persona_name)
98
+ background = ""
99
+ if highlight == "insight":
100
+ background = "background-color: #d4edda;"
101
+ elif highlight == "concern":
102
+ background = "background-color: #f8d7da;"
103
+ return (
104
+ f"<div style='color:{color}; {background} padding:8px; margin:6px 0; "
105
+ f"border-left:4px solid {color}; border-radius:4px; white-space:pre-wrap;'>{text}</div>"
106
+ )
107
+
108
+ # -------------------------
109
+ # Text parsing & sentiment
110
+ # -------------------------
111
+ _INSIGHT_PATTERN = re.compile(r'\b(think|improve|great|helpful|excellent|love|benefit|useful|like)\b', re.I)
112
+ _CONCERN_PATTERN = re.compile(r'\b(worry|concern|problem|issue|difficult|hard|confused|frustrat|dislike)\b', re.I)
113
+
114
+ def detect_insight_or_concern(text: str) -> Optional[str]:
115
+ """Return 'insight' / 'concern' / None based on simple keyword matching."""
116
+ if not text:
117
+ return None
118
+ if _INSIGHT_PATTERN.search(text):
119
+ return "insight"
120
+ if _CONCERN_PATTERN.search(text):
121
+ return "concern"
122
+ return None
123
+
124
+ def extract_persona_response(line):
125
+ """
126
+ Remove persona name and metadata, return only the response text.
127
+ Example:
128
+ "John: - Response: I think this is great" -> "I think this is great"
129
+ """
130
+ parts = re.split(r":\s*-?\s*Response:?", line, maxsplit=1)
131
+ if len(parts) == 2:
132
+ return parts[1].strip()
133
+ else:
134
+ return line
135
+
136
+ def score_sentiment(text: str) -> int:
137
+ """Simple numeric score: insight=1, concern=-1, neutral=0"""
138
+ cat = detect_insight_or_concern(text)
139
+ return 1 if cat == "insight" else -1 if cat == "concern" else 0
140
+
141
+ # -------------------------
142
+ # Heatmap / Chart builder
143
+ # -------------------------
144
+ def build_sentiment_summary(lines: List[str], selected_personas: List[Dict]) -> pd.DataFrame:
145
+ """
146
+ Return a DataFrame with average sentiment score per persona.
147
+ Ensures each selected persona appears in the result.
148
+ """
149
+ rows = []
150
+ for line in lines:
151
+ for p in selected_personas:
152
+ if line.startswith(p["name"]):
153
+ text = extract_persona_response(line)
154
+ rows.append({"Persona": p["name"], "Sentiment": score_sentiment(text)})
155
+ df = pd.DataFrame(rows) if rows else pd.DataFrame(columns=["Persona", "Sentiment"])
156
+ # ensure order & include personas with no rows
157
+ names = [p["name"] for p in selected_personas]
158
+ if df.empty:
159
+ return pd.DataFrame({"Persona": names, "Sentiment": [0]*len(names)})
160
+ summary = df.groupby("Persona")["Sentiment"].mean().reindex(names, fill_value=0).reset_index()
161
+ return summary
162
+
163
+ def build_heatmap_chart(df_summary: pd.DataFrame, height: int = 220) -> alt.Chart:
164
+ """Return an Altair bar chart representing sentiment summary."""
165
+ chart = (
166
+ alt.Chart(df_summary)
167
+ .mark_bar()
168
+ .encode(
169
+ x=alt.X("Persona", sort="-y"),
170
+ y=alt.Y("Sentiment", title="Average Sentiment Score", scale=alt.Scale(domain=[-1, 1])),
171
+ color=alt.Color(
172
+ "Sentiment",
173
+ scale=alt.Scale(domain=[-1, 0, 1], range=["#F94144", "#FFC300", "#3CB44B"]),
174
+ legend=None
175
+ ),
176
+ tooltip=["Persona", "Sentiment"]
177
+ )
178
+ .properties(height=height)
179
+ )
180
+ return chart