import os import re, csv import shutil import time, random import gradio as gr import pandas as pd from partial_json_parser import loads from openai import OpenAI from jinja2 import Template import logging from datetime import datetime seed = 42 timestamp = datetime.now().strftime('%Y%m%d%H%M') if not os.path.exists('output'): os.mkdir('output') log_file = f'output/output_{seed}_{timestamp}.log' # Set up logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler(log_file)]) prompt_system = ''' You are an AI assistant specialized in user behavior analysis and prediction. Your task is to create a detailed user persona based on provided survey responses, then predict how this persona would respond to a new question. Provide your analysis and prediction in a structured JSON format. ''' question_prompt = Template(''' Your task is to analyze survey responses, create a user persona, and predict a response to a new question. Follow these steps: 1. Review the provided survey questions and responses: {{survey_responses}} 2. Based on these responses, construct or update a comprehensive user persona. Consider: - Demographics (age, gender, occupation, location, etc.) - Psychographics (values, attitudes, interests, lifestyle) - Behavioral patterns and preferences - Potential pain points or motivations - Any other relevant characteristics that can be inferred {{user_persona}} 3. Analyze the new question and its options: {{new_question}} {{options}} 4. Consider how your constructed persona would approach this new question: - Which aspects of the question would resonate most with them? - How do their established traits and preferences influence their perspective? - What factors would be most important in their decision-making process? 5. Predict the most likely response the persona would give, ensuring consistency with their established profile and previous answers. If the survey responses are empty, generate a random answer for the question. The final answer shoule be only one digit chosen from provided options. 6. Provide your full analysis and prediction in the following JSON format: ```json { "user_persona": "A paragraph providing a comprehensive, concrete description of the imagined user persona, including all relevant demographic info, personality traits, shopping preferences and habits that can be gleaned from their provided answers. Avoid relying on implicit assumptions and aim to create a detailed, fleshed-out user profile.", "reasoning": "A paragraph outlining the step-by-step logical reasoning and thought process of how this specific user persona would approach the target question based on their established traits and preferences. Explain how these factors would influence them to ultimately select a particular answer from the given options.", "answer": "A single digit representing their selected answer, ensuring it is consistent with both their persona and their answers to the other questions, and falls within the scope of the provided options." } ``` Ensure that your analysis is well-reasoned, detailed, and consistently aligned with the information provided in the survey responses. If there's ambiguity or lack of information in certain areas, acknowledge this and explain how it affects your prediction. ''') API_BASE_URL = "https://openrouter.ai/api/v1" MODEL_CHOICES = [ "openai/gpt-3.5-turbo", "openai/gpt-4o", "meta-llama/llama-3.1-70b-instruct", "anthropic/claude-3.5-sonnet" ] DEFAULT_MODEL = "openai/gpt-4o" json_pattern = re.compile(r'```json\n(.*?)```', re.DOTALL) answer_pattern = re.compile(r'answer[\'"\s]*[:=][\s\'"]*(\d+)') csv_columns = ['contract_id', 'name', 'question_id', 'question', 'options'] def get_response_text(questions, answered): text = [] for question in answered: answer = answered[question] group_name, options = questions[question]['name'], questions[question]['options'] content = f'group: {group_name}\nquestion: {question} \noptions: {options} \t response: {answer}\n' text.append(content) return '\n'.join(text) def validate_csv(filename): required_columns = csv_columns df = pd.read_csv(filename) if not all(col in df.columns for col in required_columns): raise ValueError("CSV file is missing required columns: contract_id, name, question_id, question, options") return df def ask_llm(client, model, content, temperature=0.7): try: completion = client.chat.completions.create( model=model, temperature=float(temperature), seed=seed, response_format={ "type": "json_object" }, messages=[ {"role": "system", "content": prompt_system}, {"role": "user", "content": content} ] ) return completion.choices[0].message.content except Exception as e: print(f"Error in API call: {str(e)}") return None def prepare_question_prompt(all_questions, answered, new_question, user_persona): txt_response = get_response_text(all_questions, answered) message = question_prompt.render(survey_responses=txt_response, user_persona=user_persona, new_question=new_question['question'], options=new_question['options']) prompt = f'[INST]<>{prompt_system}<>\n{message} [/INST]' log = f"\n--> Processing question: {new_question['contract_id']} - {new_question['question_id']} {new_question['question']}\n" return prompt, log def parse_response(answered, new_question, response): log, answer_value = '', '' user_persona = None if response is None: log = f"No response received for question {new_question['question_id']}\n" return user_persona, log try: json_text = response match_json = json_pattern.search(response) if match_json: json_text = match_json.group(1) print(f'matched: {json_text}') response_json = loads(json_text) user_persona = response_json.get('user_persona', '') answer_value = response_json.get('answer', '') except: match = answer_pattern.search(response) if match: answer_value = match.group(1) print(f'failed to parse: {response}') answered[new_question['question']] = answer_value log += f"Answer generated: {answer_value}\n" if user_persona is not None: log += f"User persona: {user_persona}\n" # else: # log += f"Full response: {response}\n" return user_persona, log def process_csv(filename, api_key, model, temperature, total_numbers): if api_key is None or api_key.strip() == '': print("empty api_key, return") return 'OpenRouter API key is required', "result.csv" client = OpenAI(base_url=API_BASE_URL, api_key=api_key) logging.info(f"Processing with model: {model}, temperature: {temperature}\n") logging.info(f"Generate {total_numbers} cases") df = validate_csv(filename) questions_list = df.to_dict('records') qid_ordered = [q['question_id'] for q in questions_list] timestamp = datetime.now().strftime('%Y%m%d%H%M') output_filename = f"output/results_{timestamp}_0.csv" # Create the empty CSV file empty_df = pd.DataFrame(columns=qid_ordered) empty_df.to_csv(output_filename, index=False) # Group questions by contract_id grouped_questions = {} all_questions = {} for question in questions_list: all_questions[question['question']] = question contract_id = question['contract_id'] if contract_id not in grouped_questions: grouped_questions[contract_id] = [] grouped_questions[contract_id].append(question) # Process groups in random order group_order = list(grouped_questions.keys()) random.shuffle(group_order) logging.info(f"Ordered groups: {group_order}") log = '' for i in range(total_numbers): logging.info(f"--> sample: {i}") answered = {} user_persona = {} for contract_id in group_order: group = grouped_questions[contract_id] yield f"Processing group: {contract_id}", None # Randomly shuffle questions within the group random.shuffle(group) for new_question in group: # # first question will be answered randomly # if len(answered) < 1: # answer = random.choice(range(1, 7)) # answered[new_question['question']] = answer # continue prompt, msg = prepare_question_prompt(all_questions, answered, new_question, user_persona) log += msg yield log, output_filename logging.info(f"prompt: {prompt}\n") response = ask_llm(client, model, prompt, temperature) logging.info(f"response: {response}") new_persona, msg = parse_response(answered, new_question, response) if new_persona is not None: user_persona = new_persona log += msg yield log, output_filename time.sleep(0.1) logging.info(f"** answered **: {answered}") log += "Processing complete.\n" report = {} # convert answered to a matrix for q in answered: question = all_questions[q] report[question['question_id']] = answered[q] new_output_file = f"output/results_{timestamp}_{i+1}.csv" shutil.copy(output_filename, new_output_file) with open(new_output_file, 'a', newline='\n') as csvfile: writer = csv.writer(csvfile) writer.writerow([str(report[qid]) for qid in qid_ordered]) output_filename = new_output_file yield log, new_output_file # return log, output_filename iface = gr.Interface( fn=process_csv, inputs=[ gr.File(label="Upload CSV file"), gr.Textbox(label="API Key (OpenRouter)", type="password"), gr.Dropdown( label="LLM Model", choices=MODEL_CHOICES, value=DEFAULT_MODEL ), gr.Slider(label="Temperature", minimum=0, maximum=1, step=0.1, value=0), gr.Slider(label="Total cases", minimum=1, maximum=500, step=1, value=1) ], outputs=[ gr.Textbox(label="Execution Log", lines=30), gr.File(label="Download Results in CSV") ], examples = [ ["data/questions.csv", "", "openai/gpt-4o", 0.1, 3] ], cache_examples=False, title="User Survey Generator", description="Upload a CSV file with questions (contract_id, name, question_id, question). Provide API key and model settings. The AI will generate persona-based responses for each question." ) iface.queue() iface.launch(share=True)