Spaces:
Sleeping
Sleeping
Upload 10 files
Browse files- README.md +81 -14
- ai_helpers.py +96 -0
- app.py +187 -0
- config.py +40 -0
- integration_deployment.md +249 -0
- personas.json +1 -0
- requirements.txt +3 -3
- simulation_algorithm_design.md +195 -0
- use_cases.md +55 -0
- utils.py +180 -0
README.md
CHANGED
|
@@ -1,19 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
| 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
|