erokou commited on
Commit
2139cb3
·
verified ·
1 Parent(s): 61cb8cf

Upload 8 files

Browse files

fixing requirements issue but loading newer version fingers crossed

Files changed (3) hide show
  1. app.py +362 -24
  2. pythia.py +1 -1
  3. requirements.txt +3 -6
app.py CHANGED
@@ -1,4 +1,10 @@
 
1
  import os
 
 
 
 
 
2
  from dotenv import load_dotenv
3
  from langchain_openai import OpenAIEmbeddings, ChatOpenAI
4
  from langchain_community.vectorstores import FAISS
@@ -10,7 +16,10 @@ from langchain.tools.retriever import create_retriever_tool
10
  from langchain.agents import create_tool_calling_agent, AgentExecutor
11
  import gradio as gr
12
  import numpy as np
13
- import logging
 
 
 
14
  from pythia import scenario_simulator
15
 
16
  # Set up logging
@@ -18,15 +27,35 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(
18
  logger = logging.getLogger(__name__)
19
 
20
  # Load environment variables
21
- load_dotenv()
22
- # os.environ['USER_AGENT'] = os.getenv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # # Verify API keys
25
- # openai_api_key = os.getenv("OPENAI_API_KEY")
26
- # tavily_api_key = os.getenv("TAVILY_API_KEY")
27
- # if not openai_api_key or not tavily_api_key:
28
- # logger.error("Missing API keys: OPENAI_API_KEY or TAVILY_API_KEY")
29
- # raise ValueError("API keys not set in environment")
30
 
31
  def load_web_resources(file='websites.txt'):
32
  urls = ['https://steveblank.com/']
@@ -42,7 +71,88 @@ def load_web_resources(file='websites.txt'):
42
  except Exception as e:
43
  logger.error(f"Failed to load {url}: {e}")
44
  return web_docs
 
 
 
 
 
 
 
 
 
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def build_vectorstore(pdf_dir="ConvertedFiles"):
47
  try:
48
  loader = PyPDFDirectoryLoader(pdf_dir)
@@ -62,6 +172,134 @@ def build_vectorstore(pdf_dir="ConvertedFiles"):
62
  logger.error(f"Failed to build vectorstore: {e}")
63
  raise
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def setup_agent(vectorstore):
66
  retriever_tool = create_retriever_tool(
67
  vectorstore.as_retriever(search_kwargs={"k": 5}),
@@ -72,7 +310,10 @@ def setup_agent(vectorstore):
72
  tools = [retriever_tool, web_tool, scenario_simulator]
73
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
74
  prompt = ChatPromptTemplate.from_messages([
75
- ("system", "You are Dr. Sue Cohen, a passionate professor of Commercializing New Technologies. Answer in a witty, professional, detailed style (100-200 words for course content), using main points from course materials without citing file names in the main response. Optionally include PDF titles as references at the end (e.g., 'Reference: Lecture_5.pdf'). Always use Tavily/web for recent updates in separate paragraphs, citing URLs. Don’t mix course and web content in the same paragraph. Run scenario sims for adoption/scalability with clear params. Redirect off-topic queries, prompt next steps with a sharp question."),
 
 
 
76
  ("human", "{input}"),
77
  MessagesPlaceholder("agent_scratchpad")
78
  ])
@@ -81,8 +322,72 @@ def setup_agent(vectorstore):
81
  return executor
82
 
83
  def chat(message, history):
84
- # Parse query for scenario_simulator
85
- if "initial_users" in message:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  params = {}
87
  try:
88
  for pair in message.split(","):
@@ -95,27 +400,54 @@ def chat(message, history):
95
  except Exception as e:
96
  logger.error(f"Failed to parse scenario params: {e}")
97
  return f"Error parsing params: {e}. Format: initial_users:1000, growth_rate:5%, time_months:12, uncertainty:10%"
98
-
99
- # Retrieve documents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
101
- docs = retriever.get_relevant_documents(message)
102
  context = "\n\n".join([doc.page_content for doc in docs])
103
  titles = [os.path.basename(doc.metadata.get('source', 'Unknown')) for doc in docs]
104
  logger.info(f"Retrieved {len(docs)} documents for query: {message}")
105
 
106
- # Always invoke Tavily for updates
107
  try:
108
- web_results = TavilySearch(max_results=3).invoke(message)
109
- web_context = "\n".join([f"Web: {res.get('title', 'Untitled')} - {res.get('content', '')[:150]}... (URL: {res.get('url', 'Unknown')})" for res in web_results])
110
- logger.info(f"Retrieved {len(web_results)} web results from Tavily")
 
 
 
111
  except Exception as e:
112
  web_context = ""
113
  logger.error(f"Tavily search failed: {e}")
114
 
115
- # Combine contexts
116
  input_with_context = f"{message}\n\nCourse Materials:\n{context}\n\nWeb Updates:\n{web_context}\n\nPDF References:\n{', '.join(titles)}"
117
- logger.info(f"Processing query: {message}")
118
- return agent.invoke({"input": input_with_context})["output"]
119
 
120
  if not os.path.exists("faiss_index"):
121
  logger.warning("FAISS index not found, rebuilding...")
@@ -131,9 +463,15 @@ demo = gr.ChatInterface(
131
  fn=chat,
132
  chatbot=gr.Chatbot(type='messages'),
133
  title="COMMERCIALIZING NEW TECHNOLOGIES RAG Bot",
134
- description="Query course materials with web augmentation"
135
  )
136
 
137
  if __name__ == "__main__":
138
  logger.info("Starting Gradio app...")
139
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
1
+
2
  import os
3
+ import json
4
+ import logging
5
+ import random
6
+ import re
7
+ from langchain_core.messages import SystemMessage, HumanMessage #
8
  from dotenv import load_dotenv
9
  from langchain_openai import OpenAIEmbeddings, ChatOpenAI
10
  from langchain_community.vectorstores import FAISS
 
16
  from langchain.agents import create_tool_calling_agent, AgentExecutor
17
  import gradio as gr
18
  import numpy as np
19
+ from dataclasses import dataclass
20
+ from typing import List, Dict
21
+ import uuid
22
+ import socket
23
  from pythia import scenario_simulator
24
 
25
  # Set up logging
 
27
  logger = logging.getLogger(__name__)
28
 
29
  # Load environment variables
30
+ os.environ['USER_AGENT'] = os.getenv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
31
+
32
+ # load_dotenv()
33
+ # Verify API keys
34
+ openai_api_key = os.getenv("OPENAI_API_KEY")
35
+ tavily_api_key = os.getenv("TAVILY_API_KEY")
36
+ if not openai_api_key or not tavily_api_key:
37
+ logger.error("Missing API keys: OPENAI_API_KEY or TAVILY_API_KEY")
38
+ raise ValueError("API keys not set in environment")
39
+
40
+ # Persona-related definitions
41
+ @dataclass
42
+ class Persona:
43
+ id: str
44
+ name: str
45
+ age: int
46
+ gender: str
47
+ occupation: str
48
+ adoption_stage: str
49
+ pain_points: List[str]
50
+ motivations: List[str]
51
+ demographics: Dict
52
+
53
+ # Store active persona and conversation state
54
+ active_persona = None
55
+ conversation_count = 0
56
+ MAX_QUESTIONS = 20
57
+
58
 
 
 
 
 
 
 
59
 
60
  def load_web_resources(file='websites.txt'):
61
  urls = ['https://steveblank.com/']
 
71
  except Exception as e:
72
  logger.error(f"Failed to load {url}: {e}")
73
  return web_docs
74
+ def persona_generator(user_input: str) -> List[Dict]:
75
+ """Generates five UX personas based on user input and adoption stages."""
76
+ try:
77
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
78
+
79
+ # Always pull Dr. Sue's material for context
80
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
81
+ docs = retriever.invoke(user_input)
82
+ dr_sue_context = "\n\n".join([doc.page_content for doc in docs])
83
+ logger.info(f"Incorporated Dr. Sue context: {dr_sue_context[:200]}...")
84
 
85
+ # Build messages directly
86
+ system_content = f"""
87
+ 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.
88
+
89
+ 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}
90
+
91
+ JSON template:
92
+ [
93
+ {{
94
+ "id": "UUID",
95
+ "name": "Unique Name",
96
+ "age": integer,
97
+ "gender": "Gender",
98
+ "occupation": "Occupation",
99
+ "adoption_stage": "Stage",
100
+ "pain_points": ["Phrase1", "Phrase2", "Phrase3"],
101
+ "motivations": ["Phrase1", "Phrase2", "Phrase3"],
102
+ "demographics": {{"location": "Place", "family_status": "Status", "workplace": "Place"}},
103
+ "scenarios": ["Narrative1", "Narrative2"]
104
+ }}
105
+ ]
106
+
107
+ Adoption stage traits:
108
+ - Innovators: Risk-takers, tech enthusiasts, embrace new tools.
109
+ - Early Adopters: Visionaries, influence others, accept moderate risk.
110
+ - Early Majority: Pragmatic, seek proven value, efficiency-focused.
111
+ - Late Majority: Skeptical, need social proof, risk-averse.
112
+ - Laggards: Resistant, distrust change, adopt last.
113
+ """.strip()
114
+
115
+ messages = [
116
+ SystemMessage(content=system_content),
117
+ HumanMessage(content=user_input)
118
+ ]
119
+
120
+ logger.info(f"Invoking LLM with input: {user_input}")
121
+ response = llm.invoke(messages)
122
+ logger.info(f"LLM response: {response.content[:200]}...")
123
+ personas = json.loads(response.content)
124
+
125
+ # Validation
126
+ if not isinstance(personas, list) or len(personas) != 5:
127
+ raise ValueError(f"Expected 5 personas, got {len(personas)}")
128
+ adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
129
+ for i, persona in enumerate(personas):
130
+ if "id" not in persona or not persona["id"]:
131
+ persona["id"] = str(uuid.uuid4())
132
+ if "adoption_stage" not in persona or persona["adoption_stage"] not in adoption_stages:
133
+ persona["adoption_stage"] = adoption_stages[i]
134
+ required_fields = ["name", "age", "gender", "occupation", "pain_points", "motivations", "demographics", "scenarios"]
135
+ for field in required_fields:
136
+ if field not in persona:
137
+ raise ValueError(f"Missing field {field} in persona {i}")
138
+ if not isinstance(persona["age"], int):
139
+ try:
140
+ persona["age"] = int(persona["age"])
141
+ except (ValueError, TypeError):
142
+ raise ValueError("Invalid age")
143
+ if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
144
+ raise ValueError(f"Invalid pain_points in persona {i}")
145
+ if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
146
+ raise ValueError(f"Invalid motivations in persona {i}")
147
+ if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
148
+ raise ValueError(f"Invalid scenarios in persona {i}")
149
+ if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
150
+ raise ValueError(f"Invalid demographics in persona {i}")
151
+ logger.info(f"Successfully generated {len(personas)} personas")
152
+ return personas
153
+ except Exception as e:
154
+ logger.error(f"Persona generation failed: {str(e)}")
155
+ raise ValueError(f"Failed to generate personas: {str(e)}. Use format: 'Persona Mode: Generate Persona: [description]'")
156
  def build_vectorstore(pdf_dir="ConvertedFiles"):
157
  try:
158
  loader = PyPDFDirectoryLoader(pdf_dir)
 
172
  logger.error(f"Failed to build vectorstore: {e}")
173
  raise
174
 
175
+
176
+ """Generates five UX personas based on user input and adoption stages."""
177
+ try:
178
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
179
+
180
+ # Always pull Dr. Sue's material for context
181
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
182
+ docs = retriever.invoke(user_input)
183
+ dr_sue_context = "\n\n".join([doc.page_content for doc in docs])
184
+ logger.info(f"Incorporated Dr. Sue context: {dr_sue_context[:200]}...")
185
+
186
+ # Build messages directly
187
+ system_content = f"""
188
+ 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.
189
+
190
+ 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}
191
+
192
+ JSON template:
193
+ [
194
+ {{
195
+ "id": "UUID",
196
+ "name": "Unique Name",
197
+ "age": integer,
198
+ "gender": "Gender",
199
+ "occupation": "Occupation",
200
+ "adoption_stage": "Stage",
201
+ "pain_points": ["Phrase1", "Phrase2", "Phrase3"],
202
+ "motivations": ["Phrase1", "Phrase2", "Phrase3"],
203
+ "demographics": {{"location": "Place", "family_status": "Status", "workplace": "Place"}},
204
+ "scenarios": ["Narrative1", "Narrative2"]
205
+ }}
206
+ ]
207
+
208
+ Adoption stage traits:
209
+ - Innovators: Risk-takers, tech enthusiasts, embrace new tools.
210
+ - Early Adopters: Visionaries, influence others, accept moderate risk.
211
+ - Early Majority: Pragmatic, seek proven value, efficiency-focused.
212
+ - Late Majority: Skeptical, need social proof, risk-averse.
213
+ - Laggards: Resistant, distrust change, adopt last.
214
+ """.strip()
215
+
216
+ messages = [
217
+ SystemMessage(content=system_content),
218
+ HumanMessage(content=user_input)
219
+ ]
220
+
221
+ logger.info(f"Invoking LLM with input: {user_input}")
222
+ response = llm.invoke(messages)
223
+ logger.info(f"LLM response: {response.content[:200]}...")
224
+ personas = json.loads(response.content)
225
+
226
+ # Validation
227
+ if not isinstance(personas, list) or len(personas) != 5:
228
+ raise ValueError(f"Expected 5 personas, got {len(personas)}")
229
+ adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
230
+ for i, persona in enumerate(personas):
231
+ if "id" not in persona or not persona["id"]:
232
+ persona["id"] = str(uuid.uuid4())
233
+ if "adoption_stage" not in persona or persona["adoption_stage"] not in adoption_stages:
234
+ persona["adoption_stage"] = adoption_stages[i]
235
+ required_fields = ["name", "age", "gender", "occupation", "pain_points", "motivations", "demographics", "scenarios"]
236
+ for field in required_fields:
237
+ if field not in persona:
238
+ raise ValueError(f"Missing field {field} in persona {i}")
239
+ if not isinstance(persona["age"], int):
240
+ try:
241
+ persona["age"] = int(persona["age"])
242
+ except (ValueError, TypeError):
243
+ raise ValueError("Invalid age")
244
+ if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
245
+ raise ValueError(f"Invalid pain_points in persona {i}")
246
+ if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
247
+ raise ValueError(f"Invalid motivations in persona {i}")
248
+ if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
249
+ raise ValueError(f"Invalid scenarios in persona {i}")
250
+ if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
251
+ raise ValueError(f"Invalid demographics in persona {i}")
252
+ logger.info(f"Successfully generated {len(personas)} personas")
253
+ return personas
254
+ except Exception as e:
255
+ logger.error(f"Persona generation failed: {str(e)}")
256
+ raise ValueError(f"Failed to generate personas: {str(e)}. Use format: 'Persona Mode: Generate Persona: [description]'")
257
+ def validate_persona_json(persona_json: str) -> Dict:
258
+ """Validates and parses a persona JSON string."""
259
+ try:
260
+ # Clean JSON input to handle whitespace and line breaks
261
+ persona_json = ' '.join(persona_json.strip().split())
262
+ persona = json.loads(persona_json)
263
+ required_fields = ["id", "name", "age", "gender", "occupation", "adoption_stage", "pain_points", "motivations", "demographics", "scenarios"]
264
+ for field in required_fields:
265
+ if field not in persona:
266
+ raise ValueError(f"Missing field: {field}")
267
+ if not isinstance(persona["age"], int):
268
+ try:
269
+ persona["age"] = int(persona["age"])
270
+ except (ValueError, TypeError):
271
+ raise ValueError("age must be an integer")
272
+ if not isinstance(persona["pain_points"], list) or not all(isinstance(p, str) for p in persona["pain_points"]):
273
+ raise ValueError("pain_points must be a list of strings")
274
+ if not isinstance(persona["motivations"], list) or not all(isinstance(p, str) for p in persona["motivations"]):
275
+ raise ValueError("motivations must be a list of strings")
276
+ if not isinstance(persona["scenarios"], list) or not all(isinstance(s, str) for s in persona["scenarios"]):
277
+ raise ValueError("scenarios must be a list of strings")
278
+ if not isinstance(persona["demographics"], dict) or not all(k in persona["demographics"] for k in ["location", "family_status", "workplace"]):
279
+ raise ValueError("demographics must be a dict with location, family_status, workplace")
280
+ # Normalize adoption_stage
281
+ adoption_stages = ["Innovator", "Early Adopter", "Early Majority", "Late Majority", "Laggard"]
282
+ adoption_stage = persona["adoption_stage"].title()
283
+ if adoption_stage not in adoption_stages:
284
+ raise ValueError(f"Invalid adoption stage: {persona['adoption_stage']}")
285
+ persona["adoption_stage"] = adoption_stage
286
+ # Fix gender inconsistencies
287
+ family_status = persona["demographics"]["family_status"].lower()
288
+ if "mother" in family_status and persona["gender"].lower() != "female":
289
+ persona["gender"] = "Female"
290
+ elif "father" in family_status and persona["gender"].lower() != "male":
291
+ persona["gender"] = "Male"
292
+ # Clean family_status
293
+ persona["demographics"]["family_status"] = re.sub(r'^(she|he|they)\s+', '', family_status, flags=re.IGNORECASE).strip().title()
294
+ logger.info(f"Validated persona: {persona['name']} ({persona['adoption_stage']})")
295
+ return persona
296
+ except json.JSONDecodeError as e:
297
+ logger.error(f"JSON parsing error: {str(e)}")
298
+ return None
299
+ except Exception as e:
300
+ logger.error(f"Invalid persona JSON: {str(e)}")
301
+ return None
302
+
303
  def setup_agent(vectorstore):
304
  retriever_tool = create_retriever_tool(
305
  vectorstore.as_retriever(search_kwargs={"k": 5}),
 
310
  tools = [retriever_tool, web_tool, scenario_simulator]
311
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
312
  prompt = ChatPromptTemplate.from_messages([
313
+ ("system", """
314
+ 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.
315
+ Active Persona (if any): {active_persona}
316
+ """),
317
  ("human", "{input}"),
318
  MessagesPlaceholder("agent_scratchpad")
319
  ])
 
322
  return executor
323
 
324
  def chat(message, history):
325
+ global active_persona, conversation_count
326
+ logger.info(f"Received message: {message}")
327
+
328
+ # Parse Persona Mode commands
329
+ if message.lower().startswith("persona mode:"):
330
+ command = message[len("persona mode:"):].strip().lower()
331
+ logger.info(f"Processing persona command: {command}")
332
+
333
+ if command.startswith("generate persona:"):
334
+ try:
335
+ user_input = command[len("generate persona:"):].strip()
336
+ logger.info(f"Processing generate persona with input: {user_input}")
337
+ personas = persona_generator(user_input)
338
+ logger.info(f"Generated personas: {[p['id'] + ': ' + p['name'] for p in personas]}")
339
+ 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."
340
+ except Exception as e:
341
+ logger.error(f"Persona generation failed in chat: {e}")
342
+ return f"Error: {e}. Use format: 'Persona Mode: Generate Persona: [description]'"
343
+
344
+ elif command.startswith("activate persona:"):
345
+ try:
346
+ persona_json = command[len("activate persona:"):].strip()
347
+ persona = validate_persona_json(persona_json)
348
+ if persona:
349
+ active_persona = persona
350
+ conversation_count = 0
351
+ # Set pronouns based on gender
352
+ pronouns = {
353
+ "Male": {"subject": "he", "possessive": "his"},
354
+ "Female": {"subject": "she", "possessive": "her"},
355
+ "Non-binary": {"subject": "they", "possessive": "their"}
356
+ }.get(persona["gender"].title(), {"subject": "they", "possessive": "their"})
357
+ # Clean fields for summary
358
+ pain_point_1 = re.sub(r'^(she|he|they)\s+', '', persona['pain_points'][0], flags=re.IGNORECASE).strip().lower()
359
+ 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"
360
+ motivation_1 = re.sub(r'^(she|he|they)\s+', '', persona['motivations'][0], flags=re.IGNORECASE).strip().lower()
361
+ 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"
362
+ scenario_1 = re.sub(r'^(she|he|they)\s+', '', persona['scenarios'][0], flags=re.IGNORECASE).strip()
363
+ 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}."
364
+ # Generate summary in Dr. Sue's voice
365
+ summary = f"""
366
+ {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?
367
+ """
368
+ logger.info(f"Activated persona: {persona['name']} ({persona['adoption_stage']})")
369
+ return f"Activated persona: {persona['name']} ({persona['adoption_stage']}). Ask up to {MAX_QUESTIONS} questions.\n\n{summary.strip()}"
370
+ 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']}}"
371
+ except Exception as e:
372
+ logger.error(f"Persona activation failed: {e}")
373
+ return f"Error: {e}. Use format: 'Persona Mode: Activate Persona: [JSON]'"
374
+
375
+ elif command == "end conversation":
376
+ if active_persona:
377
+ name = active_persona['name']
378
+ active_persona = None
379
+ conversation_count = 0
380
+ logger.info(f"Ended conversation with {name}")
381
+ return f"Ended conversation with {name}. Now in Dr. Sue Cohen's TA mode. What's your next question?"
382
+ logger.warning("No active persona conversation to end")
383
+ return "No active persona conversation to end."
384
+
385
+ else:
386
+ logger.error(f"Invalid persona command: {command}")
387
+ return f"Invalid Persona Mode command: {command}. Use 'Generate Persona: [description]', 'Activate Persona: [JSON]', or 'End Conversation'."
388
+
389
+ # Parse scenario simulator query
390
+ if "initial_users" in message.lower():
391
  params = {}
392
  try:
393
  for pair in message.split(","):
 
400
  except Exception as e:
401
  logger.error(f"Failed to parse scenario params: {e}")
402
  return f"Error parsing params: {e}. Format: initial_users:1000, growth_rate:5%, time_months:12, uncertainty:10%"
403
+
404
+ # Handle persona interaction
405
+ if active_persona and conversation_count < MAX_QUESTIONS:
406
+ try:
407
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
408
+ messages = [
409
+ ("system", f"""
410
+ You are {active_persona['name']}, a {active_persona['age']}-year-old {active_persona['gender']} {active_persona['occupation']}.
411
+ Adoption stage: {active_persona['adoption_stage']}.
412
+ 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).
413
+ Pain points: {', '.join(active_persona['pain_points'])}.
414
+ Motivations: {', '.join(active_persona['motivations'])}.
415
+ Demographics: {json.dumps(active_persona['demographics'])}.
416
+ 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.
417
+ Question: {message}
418
+ """),
419
+ ("human", message)
420
+ ]
421
+ response = llm.invoke(messages)
422
+ conversation_count += 1
423
+ remaining = MAX_QUESTIONS - conversation_count
424
+ logger.info(f"Persona response generated for {active_persona['name']}, {remaining} questions left")
425
+ return f"{response.content}\n\n({remaining} questions left. Use 'Persona Mode: End Conversation' to stop.)"
426
+ except Exception as e:
427
+ logger.error(f"Persona interaction failed: {e}")
428
+ return f"Error: {e}. Try again or end conversation."
429
+
430
+ # Handle regular RAG queries
431
  retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
432
+ docs = retriever.invoke(message)
433
  context = "\n\n".join([doc.page_content for doc in docs])
434
  titles = [os.path.basename(doc.metadata.get('source', 'Unknown')) for doc in docs]
435
  logger.info(f"Retrieved {len(docs)} documents for query: {message}")
436
 
 
437
  try:
438
+ web_results = TavilySearch(max_results=5).invoke(message)
439
+ if isinstance(web_results, str):
440
+ web_context = f"Web: {web_results[:150]}..."
441
+ else:
442
+ web_context = "\n".join([f"Web: {res.get('title', 'Untitled')} - {res.get('content', '')[:150]}... (URL: {res.get('url', 'Unknown')})" for res in web_results])
443
+ logger.info(f"Retrieved {len(web_results) if isinstance(web_results, list) else 1} web results from Tavily")
444
  except Exception as e:
445
  web_context = ""
446
  logger.error(f"Tavily search failed: {e}")
447
 
 
448
  input_with_context = f"{message}\n\nCourse Materials:\n{context}\n\nWeb Updates:\n{web_context}\n\nPDF References:\n{', '.join(titles)}"
449
+ logger.info(f"Processing RAG query: {message}")
450
+ return agent.invoke({"input": input_with_context, "active_persona": active_persona or "None"})["output"]
451
 
452
  if not os.path.exists("faiss_index"):
453
  logger.warning("FAISS index not found, rebuilding...")
 
463
  fn=chat,
464
  chatbot=gr.Chatbot(type='messages'),
465
  title="COMMERCIALIZING NEW TECHNOLOGIES RAG Bot",
466
+ description="Query course materials, run scenario simulations, or use Persona Mode: 'Generate Persona: [description]', 'Activate Persona: [JSON]', 'End Conversation'"
467
  )
468
 
469
  if __name__ == "__main__":
470
  logger.info("Starting Gradio app...")
471
+ try:
472
+
473
+ logger.info(f"Using port: {7860}")
474
+ demo.launch(server_name="0.0.0.0", server_port=7860)
475
+ except Exception as e:
476
+ logger.error(f"Failed to start Gradio app: {e}")
477
+ raise
pythia.py CHANGED
@@ -2,7 +2,7 @@ import numpy as np
2
  from langchain_core.tools import tool
3
 
4
  @tool
5
- def scenario_simulator(query: str, params: dict) -> str:
6
  """Simulates product adoption scenarios using Monte Carlo. Params: initial_users, growth_rate, time_months, uncertainty."""
7
  try:
8
  initial_users = params.get("initial_users", 1000)
 
2
  from langchain_core.tools import tool
3
 
4
  @tool
5
+ def scenario_simulator(params: dict) -> str:
6
  """Simulates product adoption scenarios using Monte Carlo. Params: initial_users, growth_rate, time_months, uncertainty."""
7
  try:
8
  initial_users = params.get("initial_users", 1000)
requirements.txt CHANGED
@@ -7,10 +7,7 @@ openai>=1.35.0
7
  python-dotenv>=1.0.1
8
  langchain-text-splitters>=0.3.0
9
  tavily-python>=0.4.0
10
- numpy>=1.26.4
 
11
  beautifulsoup4>=4.12.0
12
- docx2pdf>=0.2.2
13
- pywin32>=306
14
- comtypes>=1.4.7
15
- pypdf>=5.0.0
16
- comtypes>=1.4.7
 
7
  python-dotenv>=1.0.1
8
  langchain-text-splitters>=0.3.0
9
  tavily-python>=0.4.0
10
+ langchain-tavily>=0.1.0
11
+ numpy>=2.0.0
12
  beautifulsoup4>=4.12.0
13
+ pypdf>=5.0.0