Spaces:
Sleeping
Sleeping
File size: 29,001 Bytes
2139cb3 dbb3f80 2139cb3 dbb3f80 61cb8cf dbb3f80 2139cb3 61cb8cf dbb3f80 61cb8cf 2139cb3 61cb8cf dbb3f80 61cb8cf dbb3f80 61cb8cf dbb3f80 2139cb3 dbb3f80 2139cb3 dbb3f80 61cb8cf dbb3f80 61cb8cf dbb3f80 2139cb3 dbb3f80 61cb8cf dbb3f80 61cb8cf dbb3f80 2139cb3 dbb3f80 2139cb3 dbb3f80 61cb8cf 2139cb3 61cb8cf 2139cb3 61cb8cf 2139cb3 61cb8cf 2139cb3 dbb3f80 61cb8cf dbb3f80 61cb8cf dbb3f80 61cb8cf 7eaf569 2139cb3 dbb3f80 61cb8cf 2139cb3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 |
import os
import json
import logging
import random
import re
from langchain_core.messages import SystemMessage, HumanMessage #
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import PyPDFDirectoryLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_tavily import TavilySearch
from langchain.tools.retriever import create_retriever_tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
import gradio as gr
import numpy as np
from dataclasses import dataclass
from typing import List, Dict
import uuid
import socket
from pythia import scenario_simulator
# Set up logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Load environment variables
os.environ['USER_AGENT'] = os.getenv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
# load_dotenv()
# Verify API keys
openai_api_key = os.getenv("OPENAI_API_KEY")
tavily_api_key = os.getenv("TAVILY_API_KEY")
if not openai_api_key or not tavily_api_key:
logger.error("Missing API keys: OPENAI_API_KEY or TAVILY_API_KEY")
raise ValueError("API keys not set in environment")
# Persona-related definitions
@dataclass
class Persona:
id: str
name: str
age: int
gender: str
occupation: str
adoption_stage: str
pain_points: List[str]
motivations: List[str]
demographics: Dict
# Store active persona and conversation state
active_persona = None
conversation_count = 0
MAX_QUESTIONS = 20
def load_web_resources(file='websites.txt'):
urls = ['https://steveblank.com/']
if os.path.exists(file):
with open(file, 'r') as f:
urls.extend(line.strip() for line in f if line.strip())
web_docs = []
for url in urls:
try:
loader = WebBaseLoader(url)
web_docs.extend(loader.load())
logger.info(f"Loaded web resources from {url}")
except Exception as e:
logger.error(f"Failed to load {url}: {e}")
return web_docs
def persona_generator(user_input: str) -> List[Dict]:
"""Generates five UX personas based on user input and adoption stages."""
try:
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# Always pull Dr. Sue's material for context
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
docs = retriever.invoke(user_input)
dr_sue_context = "\n\n".join([doc.page_content for doc in docs])
logger.info(f"Incorporated Dr. Sue context: {dr_sue_context[:200]}...")
# Build messages directly
system_content = f"""
You are a UX persona generator for Dr. Sue Cohen's Commercializing New Technologies class. Parse the user input (which may be narrative, e.g., 'Single mother, tired') into a JSON list of exactly 5 personas, one for each adoption stage: Innovator, Early Adopter, Early Majority, Late Majority, Laggard. Output ONLY valid JSON matching the template below—do NOT add extra text or prose.
Incorporate all input details (e.g., occupation, age, gender, family status, pain points). Convert narrative phrases into concise pain_points and motivations (e.g., 'She is tired' → 'Shift-induced exhaustion'). Use input pain points if provided (e.g., 'perpetually tired' → 'Shift-induced exhaustion'); otherwise, generate stage-specific ones consistent with the input context. Generate unique motivations and scenarios per persona, reflecting their adoption stage and healthcare technology adoption (e.g., AI tools, telehealth). Scenarios show tech use, tied to stage, pain points, and motivations. Include demographics (location, family_status, workplace). Generate unique UUID for 'id' and unique realistic names. Use this course context for depth (e.g., regulatory barriers, scaling): {dr_sue_context}
JSON template:
[
{{
"id": "UUID",
"name": "Unique Name",
"age": integer,
"gender": "Gender",
"occupation": "Occupation",
"adoption_stage": "Stage",
"pain_points": ["Phrase1", "Phrase2", "Phrase3"],
"motivations": ["Phrase1", "Phrase2", "Phrase3"],
"demographics": {{"location": "Place", "family_status": "Status", "workplace": "Place"}},
"scenarios": ["Narrative1", "Narrative2"]
}}
]
Adoption stage traits:
- Innovators: Risk-takers, tech enthusiasts, embrace new tools.
- Early Adopters: Visionaries, influence others, accept moderate risk.
- Early Majority: Pragmatic, seek proven value, efficiency-focused.
- Late Majority: Skeptical, need social proof, risk-averse.
- Laggards: Resistant, distrust change, adopt last.
""".strip()
messages = [
SystemMessage(content=system_content),
HumanMessage(content=user_input)
]
logger.info(f"Invoking LLM with input: {user_input}")
response = llm.invoke(messages)
logger.info(f"LLM response: {response.content[:200]}...")
personas = json.loads(response.content)
# Validation
if not isinstance(personas, list) or len(personas) != 5:
raise ValueError(f"Expected 5 personas, got {len(personas)}")
adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
for i, persona in enumerate(personas):
if "id" not in persona or not persona["id"]:
persona["id"] = str(uuid.uuid4())
if "adoption_stage" not in persona or persona["adoption_stage"] not in adoption_stages:
persona["adoption_stage"] = adoption_stages[i]
required_fields = ["name", "age", "gender", "occupation", "pain_points", "motivations", "demographics", "scenarios"]
for field in required_fields:
if field not in persona:
raise ValueError(f"Missing field {field} in persona {i}")
if not isinstance(persona["age"], int):
try:
persona["age"] = int(persona["age"])
except (ValueError, TypeError):
raise ValueError("Invalid age")
if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
raise ValueError(f"Invalid pain_points in persona {i}")
if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
raise ValueError(f"Invalid motivations in persona {i}")
if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
raise ValueError(f"Invalid scenarios in persona {i}")
if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
raise ValueError(f"Invalid demographics in persona {i}")
logger.info(f"Successfully generated {len(personas)} personas")
return personas
except Exception as e:
logger.error(f"Persona generation failed: {str(e)}")
raise ValueError(f"Failed to generate personas: {str(e)}. Use format: 'Persona Mode: Generate Persona: [description]'")
def build_vectorstore(pdf_dir="ConvertedFiles"):
try:
loader = PyPDFDirectoryLoader(pdf_dir)
docs = loader.load()
logger.info(f"Loaded {len(docs)} documents from {pdf_dir}")
web_docs = load_web_resources()
all_docs = docs + web_docs
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = splitter.split_documents(all_docs)
logger.info(f"Split into {len(splits)} document chunks")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(splits, embeddings)
vectorstore.save_local("faiss_index")
logger.info("Saved FAISS index to faiss_index/")
return vectorstore
except Exception as e:
logger.error(f"Failed to build vectorstore: {e}")
raise
"""Generates five UX personas based on user input and adoption stages."""
try:
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# Always pull Dr. Sue's material for context
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
docs = retriever.invoke(user_input)
dr_sue_context = "\n\n".join([doc.page_content for doc in docs])
logger.info(f"Incorporated Dr. Sue context: {dr_sue_context[:200]}...")
# Build messages directly
system_content = f"""
You are a UX persona generator for Dr. Sue Cohen's Commercializing New Technologies class. Parse the user input (which may be narrative, e.g., 'Single mother, tired') into a JSON list of exactly 5 personas, one for each adoption stage: Innovator, Early Adopter, Early Majority, Late Majority, Laggard. Output ONLY valid JSON matching the template below—do NOT add extra text or prose.
Incorporate all input details (e.g., occupation, age, gender, family status, pain points). Convert narrative phrases into concise pain_points and motivations (e.g., 'She is tired' → 'Shift-induced exhaustion'). Use input pain points if provided; otherwise, generate stage-specific ones consistent with the input context. Generate unique motivations and scenarios per persona, reflecting their adoption stage and healthcare technology adoption (e.g., AI tools, telehealth). Scenarios show tech use, tied to stage, pain points, and motivations. Include demographics (location, family_status, workplace). Generate unique UUID for 'id' and unique realistic names. Use this course context for depth (e.g., regulatory barriers, scaling): {dr_sue_context}
JSON template:
[
{{
"id": "UUID",
"name": "Unique Name",
"age": integer,
"gender": "Gender",
"occupation": "Occupation",
"adoption_stage": "Stage",
"pain_points": ["Phrase1", "Phrase2", "Phrase3"],
"motivations": ["Phrase1", "Phrase2", "Phrase3"],
"demographics": {{"location": "Place", "family_status": "Status", "workplace": "Place"}},
"scenarios": ["Narrative1", "Narrative2"]
}}
]
Adoption stage traits:
- Innovators: Risk-takers, tech enthusiasts, embrace new tools.
- Early Adopters: Visionaries, influence others, accept moderate risk.
- Early Majority: Pragmatic, seek proven value, efficiency-focused.
- Late Majority: Skeptical, need social proof, risk-averse.
- Laggards: Resistant, distrust change, adopt last.
""".strip()
messages = [
SystemMessage(content=system_content),
HumanMessage(content=user_input)
]
logger.info(f"Invoking LLM with input: {user_input}")
response = llm.invoke(messages)
logger.info(f"LLM response: {response.content[:200]}...")
personas = json.loads(response.content)
# Validation
if not isinstance(personas, list) or len(personas) != 5:
raise ValueError(f"Expected 5 personas, got {len(personas)}")
adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
for i, persona in enumerate(personas):
if "id" not in persona or not persona["id"]:
persona["id"] = str(uuid.uuid4())
if "adoption_stage" not in persona or persona["adoption_stage"] not in adoption_stages:
persona["adoption_stage"] = adoption_stages[i]
required_fields = ["name", "age", "gender", "occupation", "pain_points", "motivations", "demographics", "scenarios"]
for field in required_fields:
if field not in persona:
raise ValueError(f"Missing field {field} in persona {i}")
if not isinstance(persona["age"], int):
try:
persona["age"] = int(persona["age"])
except (ValueError, TypeError):
raise ValueError("Invalid age")
if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
raise ValueError(f"Invalid pain_points in persona {i}")
if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
raise ValueError(f"Invalid motivations in persona {i}")
if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
raise ValueError(f"Invalid scenarios in persona {i}")
if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
raise ValueError(f"Invalid demographics in persona {i}")
logger.info(f"Successfully generated {len(personas)} personas")
return personas
except Exception as e:
logger.error(f"Persona generation failed: {str(e)}")
raise ValueError(f"Failed to generate personas: {str(e)}. Use format: 'Persona Mode: Generate Persona: [description]'")
def validate_persona_json(persona_json: str) -> Dict:
"""Validates and parses a persona JSON string."""
try:
# Clean JSON input to handle whitespace and line breaks
persona_json = ' '.join(persona_json.strip().split())
persona = json.loads(persona_json)
required_fields = ["id", "name", "age", "gender", "occupation", "adoption_stage", "pain_points", "motivations", "demographics", "scenarios"]
for field in required_fields:
if field not in persona:
raise ValueError(f"Missing field: {field}")
if not isinstance(persona["age"], int):
try:
persona["age"] = int(persona["age"])
except (ValueError, TypeError):
raise ValueError("age must be an integer")
if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
raise ValueError("pain_points must be a list of strings")
if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
raise ValueError("motivations must be a list of strings")
if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
raise ValueError("scenarios must be a list of strings")
if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
raise ValueError("demographics must be a dict with location, family_status, workplace")
# Normalize adoption_stage
adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
adoption_stage = persona["adoption_stage"].title()
if adoption_stage not in adoption_stages:
raise ValueError(f"Invalid adoption stage: {persona['adoption_stage']}")
persona["adoption_stage"] = adoption_stage
# Fix gender inconsistencies
family_status = persona["demographics"]["family_status"].lower()
if "mother" in family_status and persona["gender"].lower() != "female":
persona["gender"] = "Female"
elif "father" in family_status and persona["gender"].lower() != "male":
persona["gender"] = "Male"
# Clean family_status
persona["demographics"]["family_status"] = re.sub(r'^(she|he|they)\s+', '', family_status, flags=re.IGNORECASE).strip().title()
logger.info(f"Validated persona: {persona['name']} ({persona['adoption_stage']})")
return persona
except json.JSONDecodeError as e:
logger.error(f"JSON parsing error: {str(e)}")
return None
except Exception as e:
logger.error(f"Invalid persona JSON: {str(e)}")
return None
def setup_agent(vectorstore):
retriever_tool = create_retriever_tool(
vectorstore.as_retriever(search_kwargs={"k": 5}),
"course_materials_retriever",
"Searches course materials on optimization, simulation, decision tech, project management, and lean startup."
)
web_tool = TavilySearch(max_results=3)
tools = [retriever_tool, web_tool, scenario_simulator]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
prompt = ChatPromptTemplate.from_messages([
("system", """
You are Dr. Sue Cohen, a passionate professor of Commercializing New Technologies, unless a persona is active, in which case you respond fully in-character as the persona. Answer in a witty, professional, detailed style (150-250 words for course content to ensure depth with examples and in-depth analysis), using main points from diverse course materials without citing file names in the main response. Avoid over-relying on any single lecture (e.g., Lecture 5). Use Tavily/web for recent updates in separate paragraphs with up to 5 URLs only if relevant and available; if not, omit the web section. Don’t mix course and web content. Run scenario sims for adoption/scalability with clear params. If a persona is active, respond only as that persona, staying true to their profile. Redirect off-topic queries, prompt next steps with a sharp question.
Active Persona (if any): {active_persona}
"""),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad")
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
return executor
def chat(message, history):
global active_persona, conversation_count
logger.info(f"Received message: {message}")
# Parse Persona Mode commands
if message.lower().startswith("persona mode:"):
command = message[len("persona mode:"):].strip().lower()
logger.info(f"Processing persona command: {command}")
if command.startswith("generate persona:"):
try:
user_input = command[len("generate persona:"):].strip()
logger.info(f"Processing generate persona with input: {user_input}")
personas = persona_generator(user_input)
logger.info(f"Generated personas: {[p['id'] + ': ' + p['name'] for p in personas]}")
return json.dumps(personas, indent=2) + "\n\nCopy the JSON above and save to a .json file for reuse. Use 'Persona Mode: Activate Persona: [JSON]' to interact with a persona."
except Exception as e:
logger.error(f"Persona generation failed in chat: {e}")
return f"Error: {e}. Use format: 'Persona Mode: Generate Persona: [description]'"
elif command.startswith("activate persona:"):
try:
persona_json = command[len("activate persona:"):].strip()
persona = validate_persona_json(persona_json)
if persona:
active_persona = persona
conversation_count = 0
# Set pronouns based on gender
pronouns = {
"Male": {"subject": "he", "possessive": "his"},
"Female": {"subject": "she", "possessive": "her"},
"Non-binary": {"subject": "they", "possessive": "their"}
}.get(persona["gender"].title(), {"subject": "they", "possessive": "their"})
# Clean fields for summary
pain_point_1 = re.sub(r'^(she|he|they)\s+', '', persona['pain_points'][0], flags=re.IGNORECASE).strip().lower()
pain_point_2 = re.sub(r'^(she|he|they)\s+', '', persona['pain_points'][1], flags=re.IGNORECASE).strip().lower() if len(persona['pain_points']) > 1 else "workplace challenges"
motivation_1 = re.sub(r'^(she|he|they)\s+', '', persona['motivations'][0], flags=re.IGNORECASE).strip().lower()
motivation_2 = re.sub(r'^(she|he|they)\s+', '', persona['motivations'][1], flags=re.IGNORECASE).strip().lower() if len(persona['motivations']) > 1 else "professional growth"
scenario_1 = re.sub(r'^(she|he|they)\s+', '', persona['scenarios'][0], flags=re.IGNORECASE).strip()
scenario_2 = re.sub(r'^(she|he|they)\s+', '', persona['scenarios'][1], flags=re.IGNORECASE).strip() if len(persona['scenarios']) > 1 else f"Evaluates tech to support {motivation_1}."
# Generate summary in Dr. Sue's voice
summary = f"""
{persona['name']} is a quintessential {persona['adoption_stage'].lower()} in the wild world of healthcare commercialization, tackling innovation at {persona['demographics']['workplace'].lower()}. {pronouns['subject'].title()}'s passion for {motivation_1} drives {pronouns['subject']} to size up healthcare technologies, but {pain_point_1} keeps {pronouns['subject']} on edge. {scenario_1} Meanwhile, {pronouns['subject']} grapples with {pain_point_2}, a stark reminder of the stakes in {persona['demographics']['location'].lower()}. Yet, {pronouns['subject']}'s commitment to {motivation_2} fuels {pronouns['possessive']} resolve, juggling {persona['demographics']['family_status'].lower()} alongside {pronouns['possessive']} career. In our course's spirit, {pronouns['subject']}'s a case study in navigating adoption barriers—think regulatory hoops or staff pushback—demanding a pitch sharper than a scalpel. To win {pronouns['subject']} over, you'll need rock-solid data and a plan to scale without derailing {pronouns['possessive']} hectic life. What's your strategy to crack {pronouns['possessive']} skepticism?
"""
logger.info(f"Activated persona: {persona['name']} ({persona['adoption_stage']})")
return f"Activated persona: {persona['name']} ({persona['adoption_stage']}). Ask up to {MAX_QUESTIONS} questions.\n\n{summary.strip()}"
return f"Error: Invalid persona JSON. Ensure all required fields are included and JSON is valid. Example: {{'id': 'uuid', 'name': 'Name', 'age': 35, 'gender': 'Male', 'occupation': 'Doctor', 'adoption_stage': 'Innovator', 'pain_points': ['issue1', 'issue2'], 'motivations': ['goal1', 'goal2'], 'demographics': {{'location': 'City', 'family_status': 'Status', 'workplace': 'Place'}}, 'scenarios': ['Story1', 'Story2']}}"
except Exception as e:
logger.error(f"Persona activation failed: {e}")
return f"Error: {e}. Use format: 'Persona Mode: Activate Persona: [JSON]'"
elif command == "end conversation":
if active_persona:
name = active_persona['name']
active_persona = None
conversation_count = 0
logger.info(f"Ended conversation with {name}")
return f"Ended conversation with {name}. Now in Dr. Sue Cohen's TA mode. What's your next question?"
logger.warning("No active persona conversation to end")
return "No active persona conversation to end."
else:
logger.error(f"Invalid persona command: {command}")
return f"Invalid Persona Mode command: {command}. Use 'Generate Persona: [description]', 'Activate Persona: [JSON]', or 'End Conversation'."
# Parse scenario simulator query
if "initial_users" in message.lower():
params = {}
try:
for pair in message.split(","):
key, value = pair.split(":")
key = key.strip()
value = float(value.strip().replace("%", "")) / 100 if "%" in value else int(value.strip())
params[key] = value
logger.info(f"Running scenario simulator with params: {params}")
return scenario_simulator(params)
except Exception as e:
logger.error(f"Failed to parse scenario params: {e}")
return f"Error parsing params: {e}. Format: initial_users:1000, growth_rate:5%, time_months:12, uncertainty:10%"
# Handle persona interaction
if active_persona and conversation_count < MAX_QUESTIONS:
try:
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
messages = [
("system", f"""
You are {active_persona['name']}, a {active_persona['age']}-year-old {active_persona['gender']} {active_persona['occupation']}.
Adoption stage: {active_persona['adoption_stage']}.
Personality traits: {active_persona['adoption_stage'].lower()} (trailblazers for Innovators, visionaries for Early Adopters, pragmatic for Early Majority, skeptical for Late Majority, change-resistant for Laggards).
Pain points: {', '.join(active_persona['pain_points'])}.
Motivations: {', '.join(active_persona['motivations'])}.
Demographics: {json.dumps(active_persona['demographics'])}.
You are being interviewed about a healthcare technology (e.g., AI patient monitoring tool) as a potential customer or stakeholder. Respond in character, using first-person voice, in 3-5 detailed sentences. Draw on your pain points (e.g., regulatory barriers, staff resistance from course materials), motivations (e.g., evidence-based validation for scaling), demographics (e.g., work-life balance in Pittsburgh), and scenarios (e.g., overcoming adoption hurdles like IT integration). Use conversational, realistic language with specific examples from healthcare commercialization (e.g., pilot studies, board approvals), avoiding medical jargon unless relevant to your role. Do NOT ask a question back to the interviewer; provide a complete, standalone response to simulate a realistic client interview for student training.
Question: {message}
"""),
("human", message)
]
response = llm.invoke(messages)
conversation_count += 1
remaining = MAX_QUESTIONS - conversation_count
logger.info(f"Persona response generated for {active_persona['name']}, {remaining} questions left")
return f"{response.content}\n\n({remaining} questions left. Use 'Persona Mode: End Conversation' to stop.)"
except Exception as e:
logger.error(f"Persona interaction failed: {e}")
return f"Error: {e}. Try again or end conversation."
# Handle regular RAG queries
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
docs = retriever.invoke(message)
context = "\n\n".join([doc.page_content for doc in docs])
titles = [os.path.basename(doc.metadata.get('source', 'Unknown')) for doc in docs]
logger.info(f"Retrieved {len(docs)} documents for query: {message}")
try:
web_results = TavilySearch(max_results=5).invoke(message)
if isinstance(web_results, str):
web_context = f"Web: {web_results[:150]}..."
else:
web_context = "\n".join([f"Web: {res.get('title', 'Untitled')} - {res.get('content', '')[:150]}... (URL: {res.get('url', 'Unknown')})" for res in web_results])
logger.info(f"Retrieved {len(web_results) if isinstance(web_results, list) else 1} web results from Tavily")
except Exception as e:
web_context = ""
logger.error(f"Tavily search failed: {e}")
input_with_context = f"{message}\n\nCourse Materials:\n{context}\n\nWeb Updates:\n{web_context}\n\nPDF References:\n{', '.join(titles)}"
logger.info(f"Processing RAG query: {message}")
return agent.invoke({"input": input_with_context, "active_persona": active_persona or "None"})["output"]
if not os.path.exists("faiss_index"):
logger.warning("FAISS index not found, rebuilding...")
vectorstore = build_vectorstore()
else:
logger.info("Loading prebuilt FAISS index...")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)
agent = setup_agent(vectorstore)
demo = gr.ChatInterface(
fn=chat,
chatbot=gr.Chatbot(type='messages'),
title="COMMERCIALIZING NEW TECHNOLOGIES RAG Bot",
description="Query course materials, run scenario simulations, or use Persona Mode: 'Generate Persona: [description]', 'Activate Persona: [JSON]', 'End Conversation'"
)
if __name__ == "__main__":
logger.info("Starting Gradio app...")
try:
logger.info(f"Using port: {7860}")
demo.launch(server_name="0.0.0.0", server_port=7860)
except Exception as e:
logger.error(f"Failed to start Gradio app: {e}")
raise
|