diff --git a/ai_scientist/llm.py b/ai_scientist/llm.py index 27c9eee8..908c1db1 100644 --- a/ai_scientist/llm.py +++ b/ai_scientist/llm.py @@ -5,8 +5,8 @@ import anthropic import backoff import openai -import google.generativeai as genai -from google.generativeai.types import GenerationConfig +import google.genai as genai +from google.genai.types import GenerateContentConfig MAX_NUM_TOKENS = 4096 @@ -59,6 +59,8 @@ "gemini-2.0-flash-thinking-exp-01-21", "gemini-2.5-pro-preview-03-25", "gemini-2.5-pro-exp-03-25", + "gemini-2.5-flash", + "gemini-2.5-pro" ] @@ -148,6 +150,7 @@ def get_response_from_llm( print_debug=False, msg_history=None, temperature=0.75, + tools = [], ): if msg_history is None: msg_history = [] @@ -258,19 +261,17 @@ def get_response_from_llm( content = response.choices[0].message.content new_msg_history = new_msg_history + [{"role": "assistant", "content": content}] elif "gemini" in model: - new_msg_history = msg_history + [{"role": "user", "content": msg}] - response = client.chat.completions.create( - model=model, - messages=[ - {"role": "system", "content": system_message}, - *new_msg_history, - ], - temperature=temperature, - max_tokens=MAX_NUM_TOKENS, - n=1, + chat = client.chats.create(model=model, history=msg_history) + response = chat.send_message( + msg, + config=GenerateContentConfig( + system_instruction=system_message, + max_output_tokens=MAX_NUM_TOKENS, + temperature=temperature + ) ) - content = response.choices[0].message.content - new_msg_history = new_msg_history + [{"role": "assistant", "content": content}] + content = response.text + new_msg_history = chat.get_history() else: raise ValueError(f"Model {model} not supported.") @@ -342,10 +343,9 @@ def create_client(model): base_url="https://openrouter.ai/api/v1" ), "meta-llama/llama-3.1-405b-instruct" elif "gemini" in model: - print(f"Using OpenAI API with {model}.") - return openai.OpenAI( - api_key=os.environ["GEMINI_API_KEY"], - base_url="https://generativelanguage.googleapis.com/v1beta/openai/" + print(f"Using Google GenAI API with {model}.") + return genai.client.Client( + api_key=os.environ["GEMINI_API_KEY"] ), model else: raise ValueError(f"Model {model} not supported.") diff --git a/ai_scientist/perform_experiments.py b/ai_scientist/perform_experiments.py index bb8c248a..54bbcfaf 100644 --- a/ai_scientist/perform_experiments.py +++ b/ai_scientist/perform_experiments.py @@ -61,7 +61,9 @@ def run_experiment(folder_name, run_num, timeout=7200): else: with open(osp.join(cwd, f"run_{run_num}", "final_info.json"), "r") as f: results = json.load(f) - results = {k: v["means"] for k, v in results.items()} + if isinstance(results, dict) and \ + all([v.get("means") for v in results.values()]): + results = {k: v["means"] for k, v in results.items()} next_prompt = f"""Run {run_num} completed. Here are the results: {results} diff --git a/launch_scientist.py b/launch_scientist.py index 2fe7a49c..6d9ad61c 100644 --- a/launch_scientist.py +++ b/launch_scientist.py @@ -26,6 +26,11 @@ def print_time(): print(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) +def comma_separated_list(string): + if not string: + return [] + return [item.strip() for item in string.split(',')] + def parse_arguments(): parser = argparse.ArgumentParser(description="Run AI scientist experiments") parser.add_argument( @@ -89,6 +94,12 @@ def parse_arguments(): choices=["semanticscholar", "openalex"], help="Scholar engine to use.", ) + parser.add_argument( + "--per-experiment-files", + type=comma_separated_list, + default=[], + help="A list of files to be inlucded in addition to experiment.py", + ) return parser.parse_args() @@ -161,6 +172,7 @@ def do_idea( writeup, improvement, log_file=False, + per_experiment_files = [], ): ## CREATE PROJECT FOLDER timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -172,11 +184,13 @@ def do_idea( with open(osp.join(base_dir, "run_0", "final_info.json"), "r") as f: baseline_results = json.load(f) # Check if baseline_results is a dictionary before extracting means - if isinstance(baseline_results, dict): + if isinstance(baseline_results, dict) and \ + all([v.get("means") for v in baseline_results.values()]): baseline_results = {k: v["means"] for k, v in baseline_results.items()} exp_file = osp.join(folder_name, "experiment.py") vis_file = osp.join(folder_name, "plot.py") notes = osp.join(folder_name, "notes.txt") + per_experiment_files = [osp.join(folder_name, f) for f in per_experiment_files] with open(notes, "w") as f: f.write(f"# Title: {idea['Title']}\n") f.write(f"# Experiment description: {idea['Experiment']}\n") @@ -194,7 +208,7 @@ def do_idea( print_time() print(f"*Starting idea: {idea_name}*") ## PERFORM EXPERIMENTS - fnames = [exp_file, vis_file, notes] + fnames = [exp_file, vis_file, notes] + per_experiment_files io = InputOutput( yes=True, chat_history_file=f"{folder_name}/{idea_name}_aider.txt" ) @@ -259,6 +273,9 @@ def do_idea( else: raise ValueError(f"Writeup format {writeup} not supported.") + print("stop before reviewing") + sys.exit(1) + print_time() print(f"*Starting Review*") ## REVIEW PAPER @@ -411,6 +428,7 @@ def do_idea( client_model, args.writeup, args.improvement, + per_experiment_files=args.per_experiment_files ) print(f"Completed idea: {idea['Name']}, Success: {success}") except Exception as e: diff --git a/requirements.txt b/requirements.txt index 8971848d..3cd6751f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aider-chat backoff openai google-generativeai +google-genai # Viz matplotlib pypdf diff --git a/templates/psycology_survey/README.md b/templates/psycology_survey/README.md new file mode 100644 index 00000000..1efef8c3 --- /dev/null +++ b/templates/psycology_survey/README.md @@ -0,0 +1,25 @@ +# psychology Survey template + +The psychology survey template aims to explore using the AI-scientist +for simulating psychology studies using LLM personas, and then surveying them for analysis. + +[LLMs has been used](https://arxiv.org/pdf/2304.03442) to simulate human behaviours. There is potentially value in recreating historical psychology studies using LLM personas given that LLMs encode a large array of human behaviours. + +This template does so by templaziting survey based approaches in psychology studies. + +To use this template, we need to define 3 components. + +1) survey.json. This defines who are we surveying, what questions are we asking, and what analysis is to be done. +2) Personas. These are yaml files that describe virtual personas to be surveyed. +3) analysis.py. This file contains logic that maps analysis types in survey.json to python functions. + +As an example, ideas.json is populated with an idea that tries to reproduce the landmark psychology paper [Happiness and unhappiness in the East and West](https://www.researchgate.net/profile/Yukiko-Uchida/publication/26716010_Happiness_and_Unhappiness_in_East_and_West_Themes_and_Variations/links/0c960525e02a7d5940000000/Happiness-and-Unhappiness-in-East-and-West-Themes-and-Variations.pdf). This paper examined the differences in themes of happy and unhappy emotions in Japan and the United states. The template aims to reproduce the paper using LLM personas set in Japan and US. + + +## Happiness and unhappiness in the east and west example + +Run the following to reproduce this experiment. Example paper is included. Note that this experiment doesn't need GPUs to run. + +``` +python launch_scientist.py --model "gemini-2.5-pro" --experiment psychology_survey --skip-idea-generation --skip-novelty-check --per-experiment-files analyze.py,personas.py,survey.py,data/personas/jp.yaml,data/personas/us.yaml,data/survey.json +``` diff --git a/templates/psycology_survey/analyze.py b/templates/psycology_survey/analyze.py new file mode 100644 index 00000000..1324f8ef --- /dev/null +++ b/templates/psycology_survey/analyze.py @@ -0,0 +1,140 @@ +"""Helper functions that analysis survey results. + +An analysis is a python function that takes SurveyResult as an input, +and either mutates the input or stores an artifact in final_infos. + +Example Processor: PointScaleProcessor + +How it is used: Specify the following in survey.json: +{ + "question_id": "2", + "point_scale": [1,2,3,4,5], + "desc": "Change jp happiness desirability respone to 5 point scale" +} + +What it does: It mutates input SurveyResylt by turning answers into point scales. +""" +from typing import Literal, Union +import json +from collections import defaultdict + +from survey import SurveyResult +from llm import LLMInput, get_llm_response + +import pydantic +from sklearn.manifold import MDS +import numpy as np + +ANALYSIS_SYSTEM_INSTRUCTION = """ +You are a Phd student that is responible for analyzing survey results. +Try to represent as much opinions as possible in surveys. +""" + +class PointScaleProcessor(pydantic.BaseModel): + question_id: str + point_scale: list[int] + desc: str + + def process(self, survey_result: SurveyResult, artifacts: dict): + """Mutates literal answers to point scales""" + for respondent_id, resp in survey_result.responses.items(): + for question, answer in resp.items(): + if question.id != self.question_id: + continue + if len(list(answer.values())) != len(self.point_scale): + raise Exception("Point scale doesn't match the shape of survey result") + + original_choices = question.response_format.sub_type.choices + for k, v in answer.items(): + point_scale_val = self.point_scale[original_choices.index(v)] + survey_result.responses[respondent_id][question][k] = point_scale_val + +class MergeProcessor(pydantic.BaseModel): + """Pair responses for analysis purpose. + + Example: + question: "What is your favorite dessert?" A: "cake" + merge_with: "On a scale of 1 ~ 5, how much do you like this dessert?" A: 3 + + merged answer: ("cake", 3) + """ + question_id: str + merge_with: str + desc: str + + def process(self, survey_result: SurveyResult, artifacts: dict): + for respondent_id, resp in survey_result.responses.items(): + # Question was not tarted for this respondent + if self.question_id not in set({q.id for q in resp.keys()}): + continue + other_resp = None + for question, answer in resp.items(): + if question.id == self.merge_with: + other_resp = answer + if not other_resp: + raise Exception("Merge response failed.") + for question, answer in resp.items(): + if question.id != self.question_id: + continue + for k, v in answer.items(): + merged = [v, other_resp[k]] + survey_result.responses[respondent_id][question][k] = merged + + +class SimilarityMatrix(pydantic.BaseModel): + matrix: list[list[float]] + responses: list[str] + + +class SimilarityMatrixProcessor(pydantic.BaseModel): + """Given a list of statements, use LLM to create a similarity matrix. + + The similarity matrix is stored in final_infos, and will be used + for downstream analysis (ex: MDS analysis). + """ + question_id: str + artifact_id: str + processor: Literal["free_form_to_similarity_matrix"] + desc: str + + def process(self, survey_result: SurveyResult, final_infos: dict): + all_answers = [] + answer_has_multiple_items = False + for respondent_id, resp in survey_result.responses.items(): + for question, answer in resp.items(): + if question.id != self.question_id: + continue + for k, v in answer.items(): + if isinstance(v, list) or isinstance(v, tuple): + # For paired answers, assume first item is free form. + all_answers.append(v[0]) + else: + all_answers.append(v) + + answers_str = (",").join(all_answers) + prompt = f""" + Given a list of n statements, return a similarity matrix of n x n. + The similarity matrix should be symmetrical, and a greater value + at matrix[i][j] means that statements i and j are more similar. + All values should be between 0.0 and 1.0 inclusive. Before returning + the answer, ensure that the final matrix is symmetric. + + example input: ["I like apple", "I like pear", "I hate food"] + example output: [[1.0,0.8,0.1], [0.8,1.0,0.1], [0.1,0.1,1.0]] + + input: {answers_str} + """ + llm_input = LLMInput( + prompt=prompt, + max_output_tokens = 50000, + temperature = 0, + response_type = 'application/json', + system_instruction = ANALYSIS_SYSTEM_INSTRUCTION + ) + resp = get_llm_response(llm_input) + data = json.loads(resp) + final_infos[self.artifact_id] = \ + SimilarityMatrix(matrix=json.loads(resp), responses=all_answers).model_dump() + +class Processors(pydantic.BaseModel): + processor: Union[PointScaleProcessor, MergeProcessor, SimilarityMatrixProcessor] diff --git a/templates/psycology_survey/experiment.py b/templates/psycology_survey/experiment.py new file mode 100644 index 00000000..ee299a62 --- /dev/null +++ b/templates/psycology_survey/experiment.py @@ -0,0 +1,114 @@ +import json +from enum import Enum +import argparse +from collections import defaultdict +import os + +from analyze import Processors +from personas import Persona, load_personas +from survey import Survey, load_survey, SurveyResult, SURVEYER_SYSTEM_INSTRUCTION +from llm import get_llm_response_and_history, LLMInput + +def run_survey(survey, personas: list[Persona]) -> SurveyResult: + """Runs a virtual survey that answers a set of questions for a list of personas. + + Implementation is very simple: For each person, answer a list questions in series. + If LLM calls can be parallelized without quota issues, then consider parallelizing this. + """ + print("Running survey") + persona_to_results = {} + for persona in personas: + persona_to_results[persona.name] = {} + chat_history = [] # Chat history is per person. + for question in survey.questions: + should_answer = question.targets.issubset(persona.attrs) + if not should_answer: + continue + print(f"Survey parcitipant<{persona.name}> is answering question: {question.question}") + survey_llm_input = LLMInput( + prompt=question.get_prompt_for(persona.get_summary()), + max_output_tokens = 3000, + temperature = 0, + response_type = 'application/json', + response_schema = question.get_response_schema(), + system_instruction = SURVEYER_SYSTEM_INSTRUCTION + ) + resp, chat_history = get_llm_response_and_history(survey_llm_input, chat_history) + try: + resp = json.loads(resp) + except Exception as e: + print(f"failed to parse into json: {resp}") + + persona_to_results[persona.name][question] = resp + + survey_result = SurveyResult.model_validate({'responses': persona_to_results}) + return survey_result + +def analyze_survey_results( + final_infos: dict, survey: Survey, survey_result: SurveyResult): + """Runs analysis specified by the survey using the survey results as input. + + Saves analysis results into final_infos. + """ + for analysis in survey.analysis: + print(f"Analyzing: {analysis['desc']}") + processor = Processors.model_validate({'processor': analysis}) + processor.processor.process(survey_result, final_infos) + +def write_final_info(out_dir: str): + final_info = {} + with open( + os.path.join(out_dir, f"final_info.json"), "w" + ) as f: + json.dump(final_info, f) + pass + +def main(): + parser = argparse.ArgumentParser(description="Run experiment") + parser.add_argument("--out_dir", type=str, default="run_0", help="Output directory") + parser.add_argument( + "--survey", + type=str, + default="data/happiness_unhappiness/survey.json", + help="Path to a json formatted survey." + ) + parser.add_argument( + "--survey-result", + type=str, + help="Skip the survey part and load survey results from path." + ) + parser.add_argument( + "--personas", + type=str, + default="data/happiness_unhappiness/personas", + help="Path to a folder containing yaml formatted personas." + ) + args = parser.parse_args() + output_dir = args.out_dir + + survey = load_survey(args.survey) + print(f"Sucessfully loaded survey: {survey.title}") + personas = load_personas(args.personas) + print(f"Sucessfully loaded {len(personas)} personas") + for persona in personas: + print(f"Found persona: {persona.name}") + + final_infos = {} + + if args.survey_result: + survey_result = SurveyResult.load(args.survey_result, survey) + else: + survey_result = run_survey(survey, personas) + survey_results_path = os.path.join(output_dir, "survey_results") + survey_result.save(survey_results_path) + + final_infos["survey_result"] = survey_result.to_json() + + # Saves analysis into final_infos. + analyze_survey_results(final_infos, survey, survey_result) + with open(os.path.join(output_dir, "final_info.json"), "w") as f: + json.dump(final_infos, f, ensure_ascii=False, indent=4) + + +if __name__ == "__main__": + main() diff --git a/templates/psycology_survey/happiness_and_unhappiness_in_east_and_west_in_llms.pdf b/templates/psycology_survey/happiness_and_unhappiness_in_east_and_west_in_llms.pdf new file mode 100644 index 00000000..25a61a9d Binary files /dev/null and b/templates/psycology_survey/happiness_and_unhappiness_in_east_and_west_in_llms.pdf differ diff --git a/templates/psycology_survey/ideas.json b/templates/psycology_survey/ideas.json new file mode 100644 index 00000000..5583b380 --- /dev/null +++ b/templates/psycology_survey/ideas.json @@ -0,0 +1,11 @@ +[ + { + "Name": "happiness_and_unhappiness_in_east_and_west_in_llms", + "Title": "Happiness and Unhappiness in LLMs: Comparing LLM personas in Japan and the US.", + "Experiment": "In this experiment, we try to reproduce the landmark psychology study 'happiness and unhappiness in east and west themes and variatios' by Uchida and Kitayama, but using LLM in different languages instead of real people. We survey LLM personas in 2 languages. The goal is to compare themes in the 2 languages, and producing a MDS analysis like the original paper to contrast the LLM personas in the 2 different languages.", + "Interestingness": 5, + "Feasibility": 6, + "Novelty": 8, + "novel": true + } +] \ No newline at end of file diff --git a/templates/psycology_survey/latex/iclr2024_conference.bst b/templates/psycology_survey/latex/iclr2024_conference.bst new file mode 100644 index 00000000..a85a0087 --- /dev/null +++ b/templates/psycology_survey/latex/iclr2024_conference.bst @@ -0,0 +1,1440 @@ +%% File: `iclr2024.bst' +%% A copy of iclm2010.bst, which is a modification of `plainnl.bst' for use with natbib package +%% +%% Copyright 2010 Hal Daum\'e III +%% Modified by J. Fürnkranz +%% - Changed labels from (X and Y, 2000) to (X & Y, 2000) +%% +%% Copyright 1993-2007 Patrick W Daly +%% Max-Planck-Institut f\"ur Sonnensystemforschung +%% Max-Planck-Str. 2 +%% D-37191 Katlenburg-Lindau +%% Germany +%% E-mail: daly@mps.mpg.de +%% +%% This program can be redistributed and/or modified under the terms +%% of the LaTeX Project Public License Distributed from CTAN +%% archives in directory macros/latex/base/lppl.txt; either +%% version 1 of the License, or any later version. +%% + % Version and source file information: + % \ProvidesFile{icml2010.mbs}[2007/11/26 1.93 (PWD)] + % + % BibTeX `plainnat' family + % version 0.99b for BibTeX versions 0.99a or later, + % for LaTeX versions 2.09 and 2e. + % + % For use with the `natbib.sty' package; emulates the corresponding + % member of the `plain' family, but with author-year citations. + % + % With version 6.0 of `natbib.sty', it may also be used for numerical + % citations, while retaining the commands \citeauthor, \citefullauthor, + % and \citeyear to print the corresponding information. + % + % For version 7.0 of `natbib.sty', the KEY field replaces missing + % authors/editors, and the date is left blank in \bibitem. + % + % Includes field EID for the sequence/citation number of electronic journals + % which is used instead of page numbers. + % + % Includes fields ISBN and ISSN. + % + % Includes field URL for Internet addresses. + % + % Includes field DOI for Digital Object Idenfifiers. + % + % Works best with the url.sty package of Donald Arseneau. + % + % Works with identical authors and year are further sorted by + % citation key, to preserve any natural sequence. + % +ENTRY + { address + author + booktitle + chapter + doi + eid + edition + editor + howpublished + institution + isbn + issn + journal + key + month + note + number + organization + pages + publisher + school + series + title + type + url + volume + year + } + {} + { label extra.label sort.label short.list } + +INTEGERS { output.state before.all mid.sentence after.sentence after.block } + +FUNCTION {init.state.consts} +{ #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := +} + +STRINGS { s t } + +FUNCTION {output.nonnull} +{ 's := + output.state mid.sentence = + { ", " * write$ } + { output.state after.block = + { add.period$ write$ + newline$ + "\newblock " write$ + } + { output.state before.all = + 'write$ + { add.period$ " " * write$ } + if$ + } + if$ + mid.sentence 'output.state := + } + if$ + s +} + +FUNCTION {output} +{ duplicate$ empty$ + 'pop$ + 'output.nonnull + if$ +} + +FUNCTION {output.check} +{ 't := + duplicate$ empty$ + { pop$ "empty " t * " in " * cite$ * warning$ } + 'output.nonnull + if$ +} + +FUNCTION {fin.entry} +{ add.period$ + write$ + newline$ +} + +FUNCTION {new.block} +{ output.state before.all = + 'skip$ + { after.block 'output.state := } + if$ +} + +FUNCTION {new.sentence} +{ output.state after.block = + 'skip$ + { output.state before.all = + 'skip$ + { after.sentence 'output.state := } + if$ + } + if$ +} + +FUNCTION {not} +{ { #0 } + { #1 } + if$ +} + +FUNCTION {and} +{ 'skip$ + { pop$ #0 } + if$ +} + +FUNCTION {or} +{ { pop$ #1 } + 'skip$ + if$ +} + +FUNCTION {new.block.checka} +{ empty$ + 'skip$ + 'new.block + if$ +} + +FUNCTION {new.block.checkb} +{ empty$ + swap$ empty$ + and + 'skip$ + 'new.block + if$ +} + +FUNCTION {new.sentence.checka} +{ empty$ + 'skip$ + 'new.sentence + if$ +} + +FUNCTION {new.sentence.checkb} +{ empty$ + swap$ empty$ + and + 'skip$ + 'new.sentence + if$ +} + +FUNCTION {field.or.null} +{ duplicate$ empty$ + { pop$ "" } + 'skip$ + if$ +} + +FUNCTION {emphasize} +{ duplicate$ empty$ + { pop$ "" } + { "\emph{" swap$ * "}" * } + if$ +} + +INTEGERS { nameptr namesleft numnames } + +FUNCTION {format.names} +{ 's := + #1 'nameptr := + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { s nameptr "{ff~}{vv~}{ll}{, jj}" format.name$ 't := + nameptr #1 > + { namesleft #1 > + { ", " * t * } + { numnames #2 > + { "," * } + 'skip$ + if$ + t "others" = + { " et~al." * } + { " and " * t * } + if$ + } + if$ + } + 't + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ +} + +FUNCTION {format.key} +{ empty$ + { key field.or.null } + { "" } + if$ +} + +FUNCTION {format.authors} +{ author empty$ + { "" } + { author format.names } + if$ +} + +FUNCTION {format.editors} +{ editor empty$ + { "" } + { editor format.names + editor num.names$ #1 > + { " (eds.)" * } + { " (ed.)" * } + if$ + } + if$ +} + +FUNCTION {format.isbn} +{ isbn empty$ + { "" } + { new.block "ISBN " isbn * } + if$ +} + +FUNCTION {format.issn} +{ issn empty$ + { "" } + { new.block "ISSN " issn * } + if$ +} + +FUNCTION {format.url} +{ url empty$ + { "" } + { new.block "URL \url{" url * "}" * } + if$ +} + +FUNCTION {format.doi} +{ doi empty$ + { "" } + { new.block "\doi{" doi * "}" * } + if$ +} + +FUNCTION {format.title} +{ title empty$ + { "" } + { title "t" change.case$ } + if$ +} + +FUNCTION {format.full.names} +{'s := + #1 'nameptr := + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { s nameptr + "{vv~}{ll}" format.name$ 't := + nameptr #1 > + { + namesleft #1 > + { ", " * t * } + { + numnames #2 > + { "," * } + 'skip$ + if$ + t "others" = + { " et~al." * } + { " and " * t * } + if$ + } + if$ + } + 't + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ +} + +FUNCTION {author.editor.full} +{ author empty$ + { editor empty$ + { "" } + { editor format.full.names } + if$ + } + { author format.full.names } + if$ +} + +FUNCTION {author.full} +{ author empty$ + { "" } + { author format.full.names } + if$ +} + +FUNCTION {editor.full} +{ editor empty$ + { "" } + { editor format.full.names } + if$ +} + +FUNCTION {make.full.names} +{ type$ "book" = + type$ "inbook" = + or + 'author.editor.full + { type$ "proceedings" = + 'editor.full + 'author.full + if$ + } + if$ +} + +FUNCTION {output.bibitem} +{ newline$ + "\bibitem[" write$ + label write$ + ")" make.full.names duplicate$ short.list = + { pop$ } + { * } + if$ + "]{" * write$ + cite$ write$ + "}" write$ + newline$ + "" + before.all 'output.state := +} + +FUNCTION {n.dashify} +{ 't := + "" + { t empty$ not } + { t #1 #1 substring$ "-" = + { t #1 #2 substring$ "--" = not + { "--" * + t #2 global.max$ substring$ 't := + } + { { t #1 #1 substring$ "-" = } + { "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ +} + +FUNCTION {format.date} +{ year duplicate$ empty$ + { "empty year in " cite$ * warning$ + pop$ "" } + 'skip$ + if$ + month empty$ + 'skip$ + { month + " " * swap$ * + } + if$ + extra.label * +} + +FUNCTION {format.btitle} +{ title emphasize +} + +FUNCTION {tie.or.space.connect} +{ duplicate$ text.length$ #3 < + { "~" } + { " " } + if$ + swap$ * * +} + +FUNCTION {either.or.check} +{ empty$ + 'pop$ + { "can't use both " swap$ * " fields in " * cite$ * warning$ } + if$ +} + +FUNCTION {format.bvolume} +{ volume empty$ + { "" } + { "volume" volume tie.or.space.connect + series empty$ + 'skip$ + { " of " * series emphasize * } + if$ + "volume and number" number either.or.check + } + if$ +} + +FUNCTION {format.number.series} +{ volume empty$ + { number empty$ + { series field.or.null } + { output.state mid.sentence = + { "number" } + { "Number" } + if$ + number tie.or.space.connect + series empty$ + { "there's a number but no series in " cite$ * warning$ } + { " in " * series * } + if$ + } + if$ + } + { "" } + if$ +} + +FUNCTION {format.edition} +{ edition empty$ + { "" } + { output.state mid.sentence = + { edition "l" change.case$ " edition" * } + { edition "t" change.case$ " edition" * } + if$ + } + if$ +} + +INTEGERS { multiresult } + +FUNCTION {multi.page.check} +{ 't := + #0 'multiresult := + { multiresult not + t empty$ not + and + } + { t #1 #1 substring$ + duplicate$ "-" = + swap$ duplicate$ "," = + swap$ "+" = + or or + { #1 'multiresult := } + { t #2 global.max$ substring$ 't := } + if$ + } + while$ + multiresult +} + +FUNCTION {format.pages} +{ pages empty$ + { "" } + { pages multi.page.check + { "pp.\ " pages n.dashify tie.or.space.connect } + { "pp.\ " pages tie.or.space.connect } + if$ + } + if$ +} + +FUNCTION {format.eid} +{ eid empty$ + { "" } + { "art." eid tie.or.space.connect } + if$ +} + +FUNCTION {format.vol.num.pages} +{ volume field.or.null + number empty$ + 'skip$ + { "\penalty0 (" number * ")" * * + volume empty$ + { "there's a number but no volume in " cite$ * warning$ } + 'skip$ + if$ + } + if$ + pages empty$ + 'skip$ + { duplicate$ empty$ + { pop$ format.pages } + { ":\penalty0 " * pages n.dashify * } + if$ + } + if$ +} + +FUNCTION {format.vol.num.eid} +{ volume field.or.null + number empty$ + 'skip$ + { "\penalty0 (" number * ")" * * + volume empty$ + { "there's a number but no volume in " cite$ * warning$ } + 'skip$ + if$ + } + if$ + eid empty$ + 'skip$ + { duplicate$ empty$ + { pop$ format.eid } + { ":\penalty0 " * eid * } + if$ + } + if$ +} + +FUNCTION {format.chapter.pages} +{ chapter empty$ + 'format.pages + { type empty$ + { "chapter" } + { type "l" change.case$ } + if$ + chapter tie.or.space.connect + pages empty$ + 'skip$ + { ", " * format.pages * } + if$ + } + if$ +} + +FUNCTION {format.in.ed.booktitle} +{ booktitle empty$ + { "" } + { editor empty$ + { "In " booktitle emphasize * } + { "In " format.editors * ", " * booktitle emphasize * } + if$ + } + if$ +} + +FUNCTION {empty.misc.check} +{ author empty$ title empty$ howpublished empty$ + month empty$ year empty$ note empty$ + and and and and and + key empty$ not and + { "all relevant fields are empty in " cite$ * warning$ } + 'skip$ + if$ +} + +FUNCTION {format.thesis.type} +{ type empty$ + 'skip$ + { pop$ + type "t" change.case$ + } + if$ +} + +FUNCTION {format.tr.number} +{ type empty$ + { "Technical Report" } + 'type + if$ + number empty$ + { "t" change.case$ } + { number tie.or.space.connect } + if$ +} + +FUNCTION {format.article.crossref} +{ key empty$ + { journal empty$ + { "need key or journal for " cite$ * " to crossref " * crossref * + warning$ + "" + } + { "In \emph{" journal * "}" * } + if$ + } + { "In " } + if$ + " \citet{" * crossref * "}" * +} + +FUNCTION {format.book.crossref} +{ volume empty$ + { "empty volume in " cite$ * "'s crossref of " * crossref * warning$ + "In " + } + { "Volume" volume tie.or.space.connect + " of " * + } + if$ + editor empty$ + editor field.or.null author field.or.null = + or + { key empty$ + { series empty$ + { "need editor, key, or series for " cite$ * " to crossref " * + crossref * warning$ + "" * + } + { "\emph{" * series * "}" * } + if$ + } + 'skip$ + if$ + } + 'skip$ + if$ + " \citet{" * crossref * "}" * +} + +FUNCTION {format.incoll.inproc.crossref} +{ editor empty$ + editor field.or.null author field.or.null = + or + { key empty$ + { booktitle empty$ + { "need editor, key, or booktitle for " cite$ * " to crossref " * + crossref * warning$ + "" + } + { "In \emph{" booktitle * "}" * } + if$ + } + { "In " } + if$ + } + { "In " } + if$ + " \citet{" * crossref * "}" * +} + +FUNCTION {article} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + crossref missing$ + { journal emphasize "journal" output.check + eid empty$ + { format.vol.num.pages output } + { format.vol.num.eid output } + if$ + format.date "year" output.check + } + { format.article.crossref output.nonnull + eid empty$ + { format.pages output } + { format.eid output } + if$ + } + if$ + format.issn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {book} +{ output.bibitem + author empty$ + { format.editors "author and editor" output.check + editor format.key output + } + { format.authors output.nonnull + crossref missing$ + { "author and editor" editor either.or.check } + 'skip$ + if$ + } + if$ + new.block + format.btitle "title" output.check + crossref missing$ + { format.bvolume output + new.block + format.number.series output + new.sentence + publisher "publisher" output.check + address output + } + { new.block + format.book.crossref output.nonnull + } + if$ + format.edition output + format.date "year" output.check + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {booklet} +{ output.bibitem + format.authors output + author format.key output + new.block + format.title "title" output.check + howpublished address new.block.checkb + howpublished output + address output + format.date output + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {inbook} +{ output.bibitem + author empty$ + { format.editors "author and editor" output.check + editor format.key output + } + { format.authors output.nonnull + crossref missing$ + { "author and editor" editor either.or.check } + 'skip$ + if$ + } + if$ + new.block + format.btitle "title" output.check + crossref missing$ + { format.bvolume output + format.chapter.pages "chapter and pages" output.check + new.block + format.number.series output + new.sentence + publisher "publisher" output.check + address output + } + { format.chapter.pages "chapter and pages" output.check + new.block + format.book.crossref output.nonnull + } + if$ + format.edition output + format.date "year" output.check + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {incollection} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + crossref missing$ + { format.in.ed.booktitle "booktitle" output.check + format.bvolume output + format.number.series output + format.chapter.pages output + new.sentence + publisher "publisher" output.check + address output + format.edition output + format.date "year" output.check + } + { format.incoll.inproc.crossref output.nonnull + format.chapter.pages output + } + if$ + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {inproceedings} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + crossref missing$ + { format.in.ed.booktitle "booktitle" output.check + format.bvolume output + format.number.series output + format.pages output + address empty$ + { organization publisher new.sentence.checkb + organization output + publisher output + format.date "year" output.check + } + { address output.nonnull + format.date "year" output.check + new.sentence + organization output + publisher output + } + if$ + } + { format.incoll.inproc.crossref output.nonnull + format.pages output + } + if$ + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {conference} { inproceedings } + +FUNCTION {manual} +{ output.bibitem + format.authors output + author format.key output + new.block + format.btitle "title" output.check + organization address new.block.checkb + organization output + address output + format.edition output + format.date output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {mastersthesis} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + "Master's thesis" format.thesis.type output.nonnull + school "school" output.check + address output + format.date "year" output.check + format.url output + new.block + note output + fin.entry +} + +FUNCTION {misc} +{ output.bibitem + format.authors output + author format.key output + title howpublished new.block.checkb + format.title output + howpublished new.block.checka + howpublished output + format.date output + format.issn output + format.url output + new.block + note output + fin.entry + empty.misc.check +} + +FUNCTION {phdthesis} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.btitle "title" output.check + new.block + "PhD thesis" format.thesis.type output.nonnull + school "school" output.check + address output + format.date "year" output.check + format.url output + new.block + note output + fin.entry +} + +FUNCTION {proceedings} +{ output.bibitem + format.editors output + editor format.key output + new.block + format.btitle "title" output.check + format.bvolume output + format.number.series output + address output + format.date "year" output.check + new.sentence + organization output + publisher output + format.isbn output + format.doi output + format.url output + new.block + note output + fin.entry +} + +FUNCTION {techreport} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + format.tr.number output.nonnull + institution "institution" output.check + address output + format.date "year" output.check + format.url output + new.block + note output + fin.entry +} + +FUNCTION {unpublished} +{ output.bibitem + format.authors "author" output.check + author format.key output + new.block + format.title "title" output.check + new.block + note "note" output.check + format.date output + format.url output + fin.entry +} + +FUNCTION {default.type} { misc } + + +MACRO {jan} {"January"} + +MACRO {feb} {"February"} + +MACRO {mar} {"March"} + +MACRO {apr} {"April"} + +MACRO {may} {"May"} + +MACRO {jun} {"June"} + +MACRO {jul} {"July"} + +MACRO {aug} {"August"} + +MACRO {sep} {"September"} + +MACRO {oct} {"October"} + +MACRO {nov} {"November"} + +MACRO {dec} {"December"} + + + +MACRO {acmcs} {"ACM Computing Surveys"} + +MACRO {acta} {"Acta Informatica"} + +MACRO {cacm} {"Communications of the ACM"} + +MACRO {ibmjrd} {"IBM Journal of Research and Development"} + +MACRO {ibmsj} {"IBM Systems Journal"} + +MACRO {ieeese} {"IEEE Transactions on Software Engineering"} + +MACRO {ieeetc} {"IEEE Transactions on Computers"} + +MACRO {ieeetcad} + {"IEEE Transactions on Computer-Aided Design of Integrated Circuits"} + +MACRO {ipl} {"Information Processing Letters"} + +MACRO {jacm} {"Journal of the ACM"} + +MACRO {jcss} {"Journal of Computer and System Sciences"} + +MACRO {scp} {"Science of Computer Programming"} + +MACRO {sicomp} {"SIAM Journal on Computing"} + +MACRO {tocs} {"ACM Transactions on Computer Systems"} + +MACRO {tods} {"ACM Transactions on Database Systems"} + +MACRO {tog} {"ACM Transactions on Graphics"} + +MACRO {toms} {"ACM Transactions on Mathematical Software"} + +MACRO {toois} {"ACM Transactions on Office Information Systems"} + +MACRO {toplas} {"ACM Transactions on Programming Languages and Systems"} + +MACRO {tcs} {"Theoretical Computer Science"} + + +READ + +FUNCTION {sortify} +{ purify$ + "l" change.case$ +} + +INTEGERS { len } + +FUNCTION {chop.word} +{ 's := + 'len := + s #1 len substring$ = + { s len #1 + global.max$ substring$ } + 's + if$ +} + +FUNCTION {format.lab.names} +{ 's := + s #1 "{vv~}{ll}" format.name$ + s num.names$ duplicate$ + #2 > + { pop$ " et~al." * } + { #2 < + 'skip$ + { s #2 "{ff }{vv }{ll}{ jj}" format.name$ "others" = + { " et~al." * } + { " \& " * s #2 "{vv~}{ll}" format.name$ * } + if$ + } + if$ + } + if$ +} + +FUNCTION {author.key.label} +{ author empty$ + { key empty$ + { cite$ #1 #3 substring$ } + 'key + if$ + } + { author format.lab.names } + if$ +} + +FUNCTION {author.editor.key.label} +{ author empty$ + { editor empty$ + { key empty$ + { cite$ #1 #3 substring$ } + 'key + if$ + } + { editor format.lab.names } + if$ + } + { author format.lab.names } + if$ +} + +FUNCTION {author.key.organization.label} +{ author empty$ + { key empty$ + { organization empty$ + { cite$ #1 #3 substring$ } + { "The " #4 organization chop.word #3 text.prefix$ } + if$ + } + 'key + if$ + } + { author format.lab.names } + if$ +} + +FUNCTION {editor.key.organization.label} +{ editor empty$ + { key empty$ + { organization empty$ + { cite$ #1 #3 substring$ } + { "The " #4 organization chop.word #3 text.prefix$ } + if$ + } + 'key + if$ + } + { editor format.lab.names } + if$ +} + +FUNCTION {calc.short.authors} +{ type$ "book" = + type$ "inbook" = + or + 'author.editor.key.label + { type$ "proceedings" = + 'editor.key.organization.label + { type$ "manual" = + 'author.key.organization.label + 'author.key.label + if$ + } + if$ + } + if$ + 'short.list := +} + +FUNCTION {calc.label} +{ calc.short.authors + short.list + "(" + * + year duplicate$ empty$ + short.list key field.or.null = or + { pop$ "" } + 'skip$ + if$ + * + 'label := +} + +FUNCTION {sort.format.names} +{ 's := + #1 'nameptr := + "" + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { + s nameptr "{vv{ } }{ll{ }}{ ff{ }}{ jj{ }}" format.name$ 't := + nameptr #1 > + { + " " * + namesleft #1 = t "others" = and + { "zzzzz" * } + { numnames #2 > nameptr #2 = and + { "zz" * year field.or.null * " " * } + 'skip$ + if$ + t sortify * + } + if$ + } + { t sortify * } + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ +} + +FUNCTION {sort.format.title} +{ 't := + "A " #2 + "An " #3 + "The " #4 t chop.word + chop.word + chop.word + sortify + #1 global.max$ substring$ +} + +FUNCTION {author.sort} +{ author empty$ + { key empty$ + { "to sort, need author or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {author.editor.sort} +{ author empty$ + { editor empty$ + { key empty$ + { "to sort, need author, editor, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { editor sort.format.names } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {author.organization.sort} +{ author empty$ + { organization empty$ + { key empty$ + { "to sort, need author, organization, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { "The " #4 organization chop.word sortify } + if$ + } + { author sort.format.names } + if$ +} + +FUNCTION {editor.organization.sort} +{ editor empty$ + { organization empty$ + { key empty$ + { "to sort, need editor, organization, or key in " cite$ * warning$ + "" + } + { key sortify } + if$ + } + { "The " #4 organization chop.word sortify } + if$ + } + { editor sort.format.names } + if$ +} + + +FUNCTION {presort} +{ calc.label + label sortify + " " + * + type$ "book" = + type$ "inbook" = + or + 'author.editor.sort + { type$ "proceedings" = + 'editor.organization.sort + { type$ "manual" = + 'author.organization.sort + 'author.sort + if$ + } + if$ + } + if$ + " " + * + year field.or.null sortify + * + " " + * + cite$ + * + #1 entry.max$ substring$ + 'sort.label := + sort.label * + #1 entry.max$ substring$ + 'sort.key$ := +} + +ITERATE {presort} + +SORT + +STRINGS { longest.label last.label next.extra } + +INTEGERS { longest.label.width last.extra.num number.label } + +FUNCTION {initialize.longest.label} +{ "" 'longest.label := + #0 int.to.chr$ 'last.label := + "" 'next.extra := + #0 'longest.label.width := + #0 'last.extra.num := + #0 'number.label := +} + +FUNCTION {forward.pass} +{ last.label label = + { last.extra.num #1 + 'last.extra.num := + last.extra.num int.to.chr$ 'extra.label := + } + { "a" chr.to.int$ 'last.extra.num := + "" 'extra.label := + label 'last.label := + } + if$ + number.label #1 + 'number.label := +} + +FUNCTION {reverse.pass} +{ next.extra "b" = + { "a" 'extra.label := } + 'skip$ + if$ + extra.label 'next.extra := + extra.label + duplicate$ empty$ + 'skip$ + { "{\natexlab{" swap$ * "}}" * } + if$ + 'extra.label := + label extra.label * 'label := +} + +EXECUTE {initialize.longest.label} + +ITERATE {forward.pass} + +REVERSE {reverse.pass} + +FUNCTION {bib.sort.order} +{ sort.label 'sort.key$ := +} + +ITERATE {bib.sort.order} + +SORT + +FUNCTION {begin.bib} +{ preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\begin{thebibliography}{" number.label int.to.str$ * "}" * + write$ newline$ + "\providecommand{\natexlab}[1]{#1}" + write$ newline$ + "\providecommand{\url}[1]{\texttt{#1}}" + write$ newline$ + "\expandafter\ifx\csname urlstyle\endcsname\relax" + write$ newline$ + " \providecommand{\doi}[1]{doi: #1}\else" + write$ newline$ + " \providecommand{\doi}{doi: \begingroup \urlstyle{rm}\Url}\fi" + write$ newline$ +} + +EXECUTE {begin.bib} + +EXECUTE {init.state.consts} + +ITERATE {call.type$} + +FUNCTION {end.bib} +{ newline$ + "\end{thebibliography}" write$ newline$ +} + +EXECUTE {end.bib} diff --git a/templates/psycology_survey/latex/iclr2024_conference.sty b/templates/psycology_survey/latex/iclr2024_conference.sty new file mode 100644 index 00000000..a46370c7 --- /dev/null +++ b/templates/psycology_survey/latex/iclr2024_conference.sty @@ -0,0 +1,245 @@ +%%%% ICLR Macros (LaTex) +%%%% Adapted by Hugo Larochelle from the NIPS stylefile Macros +%%%% Style File +%%%% Dec 12, 1990 Rev Aug 14, 1991; Sept, 1995; April, 1997; April, 1999; October 2014 + +% This file can be used with Latex2e whether running in main mode, or +% 2.09 compatibility mode. +% +% If using main mode, you need to include the commands +% \documentclass{article} +% \usepackage{iclr14submit_e,times} +% + +% Change the overall width of the page. If these parameters are +% changed, they will require corresponding changes in the +% maketitle section. +% +\usepackage{eso-pic} % used by \AddToShipoutPicture +\RequirePackage{fancyhdr} +\RequirePackage{natbib} + +% modification to natbib citations +\setcitestyle{authoryear,round,citesep={;},aysep={,},yysep={;}} + +\renewcommand{\topfraction}{0.95} % let figure take up nearly whole page +\renewcommand{\textfraction}{0.05} % let figure take up nearly whole page + +% Define iclrfinal, set to true if iclrfinalcopy is defined +\newif\ificlrfinal +\iclrfinalfalse +\def\iclrfinalcopy{\iclrfinaltrue} +\font\iclrtenhv = phvb at 8pt + +% Specify the dimensions of each page + +\setlength{\paperheight}{11in} +\setlength{\paperwidth}{8.5in} + + +\oddsidemargin .5in % Note \oddsidemargin = \evensidemargin +\evensidemargin .5in +\marginparwidth 0.07 true in +%\marginparwidth 0.75 true in +%\topmargin 0 true pt % Nominal distance from top of page to top of +%\topmargin 0.125in +\topmargin -0.625in +\addtolength{\headsep}{0.25in} +\textheight 9.0 true in % Height of text (including footnotes & figures) +\textwidth 5.5 true in % Width of text line. +\widowpenalty=10000 +\clubpenalty=10000 + +% \thispagestyle{empty} \pagestyle{empty} +\flushbottom \sloppy + +% We're never going to need a table of contents, so just flush it to +% save space --- suggested by drstrip@sandia-2 +\def\addcontentsline#1#2#3{} + +% Title stuff, taken from deproc. +\def\maketitle{\par +\begingroup + \def\thefootnote{\fnsymbol{footnote}} + \def\@makefnmark{\hbox to 0pt{$^{\@thefnmark}$\hss}} % for perfect author + % name centering +% The footnote-mark was overlapping the footnote-text, +% added the following to fix this problem (MK) + \long\def\@makefntext##1{\parindent 1em\noindent + \hbox to1.8em{\hss $\m@th ^{\@thefnmark}$}##1} + \@maketitle \@thanks +\endgroup +\setcounter{footnote}{0} +\let\maketitle\relax \let\@maketitle\relax +\gdef\@thanks{}\gdef\@author{}\gdef\@title{}\let\thanks\relax} + +% The toptitlebar has been raised to top-justify the first page + +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhead{} + +% Title (includes both anonimized and non-anonimized versions) +\def\@maketitle{\vbox{\hsize\textwidth +%\linewidth\hsize \vskip 0.1in \toptitlebar \centering +{\LARGE\sc \@title\par} +%\bottomtitlebar % \vskip 0.1in % minus +\ificlrfinal + \lhead{Published as a conference paper at ICLR 2024} + \def\And{\end{tabular}\hfil\linebreak[0]\hfil + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}\ignorespaces}% + \def\AND{\end{tabular}\hfil\linebreak[4]\hfil + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}\ignorespaces}% + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}\@author\end{tabular}% +\else + \lhead{AI-Scientist Generated Preprint} + \def\And{\end{tabular}\hfil\linebreak[0]\hfil + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}\ignorespaces}% + \def\AND{\end{tabular}\hfil\linebreak[4]\hfil + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}\ignorespaces}% + \begin{tabular}[t]{l}\bf\rule{\z@}{24pt}Anonymous authors\\Paper under double-blind review\end{tabular}% +\fi +\vskip 0.3in minus 0.1in}} + +\renewenvironment{abstract}{\vskip.075in\centerline{\large\sc +Abstract}\vspace{0.5ex}\begin{quote}}{\par\end{quote}\vskip 1ex} + +% sections with less space +\def\section{\@startsection {section}{1}{\z@}{-2.0ex plus + -0.5ex minus -.2ex}{1.5ex plus 0.3ex +minus0.2ex}{\large\sc\raggedright}} + +\def\subsection{\@startsection{subsection}{2}{\z@}{-1.8ex plus +-0.5ex minus -.2ex}{0.8ex plus .2ex}{\normalsize\sc\raggedright}} +\def\subsubsection{\@startsection{subsubsection}{3}{\z@}{-1.5ex +plus -0.5ex minus -.2ex}{0.5ex plus +.2ex}{\normalsize\sc\raggedright}} +\def\paragraph{\@startsection{paragraph}{4}{\z@}{1.5ex plus +0.5ex minus .2ex}{-1em}{\normalsize\bf}} +\def\subparagraph{\@startsection{subparagraph}{5}{\z@}{1.5ex plus + 0.5ex minus .2ex}{-1em}{\normalsize\sc}} +\def\subsubsubsection{\vskip +5pt{\noindent\normalsize\rm\raggedright}} + + +% Footnotes +\footnotesep 6.65pt % +\skip\footins 9pt plus 4pt minus 2pt +\def\footnoterule{\kern-3pt \hrule width 12pc \kern 2.6pt } +\setcounter{footnote}{0} + +% Lists and paragraphs +\parindent 0pt +\topsep 4pt plus 1pt minus 2pt +\partopsep 1pt plus 0.5pt minus 0.5pt +\itemsep 2pt plus 1pt minus 0.5pt +\parsep 2pt plus 1pt minus 0.5pt +\parskip .5pc + + +%\leftmargin2em +\leftmargin3pc +\leftmargini\leftmargin \leftmarginii 2em +\leftmarginiii 1.5em \leftmarginiv 1.0em \leftmarginv .5em + +%\labelsep \labelsep 5pt + +\def\@listi{\leftmargin\leftmargini} +\def\@listii{\leftmargin\leftmarginii + \labelwidth\leftmarginii\advance\labelwidth-\labelsep + \topsep 2pt plus 1pt minus 0.5pt + \parsep 1pt plus 0.5pt minus 0.5pt + \itemsep \parsep} +\def\@listiii{\leftmargin\leftmarginiii + \labelwidth\leftmarginiii\advance\labelwidth-\labelsep + \topsep 1pt plus 0.5pt minus 0.5pt + \parsep \z@ \partopsep 0.5pt plus 0pt minus 0.5pt + \itemsep \topsep} +\def\@listiv{\leftmargin\leftmarginiv + \labelwidth\leftmarginiv\advance\labelwidth-\labelsep} +\def\@listv{\leftmargin\leftmarginv + \labelwidth\leftmarginv\advance\labelwidth-\labelsep} +\def\@listvi{\leftmargin\leftmarginvi + \labelwidth\leftmarginvi\advance\labelwidth-\labelsep} + +\abovedisplayskip 7pt plus2pt minus5pt% +\belowdisplayskip \abovedisplayskip +\abovedisplayshortskip 0pt plus3pt% +\belowdisplayshortskip 4pt plus3pt minus3pt% + +% Less leading in most fonts (due to the narrow columns) +% The choices were between 1-pt and 1.5-pt leading +%\def\@normalsize{\@setsize\normalsize{11pt}\xpt\@xpt} % got rid of @ (MK) +\def\normalsize{\@setsize\normalsize{11pt}\xpt\@xpt} +\def\small{\@setsize\small{10pt}\ixpt\@ixpt} +\def\footnotesize{\@setsize\footnotesize{10pt}\ixpt\@ixpt} +\def\scriptsize{\@setsize\scriptsize{8pt}\viipt\@viipt} +\def\tiny{\@setsize\tiny{7pt}\vipt\@vipt} +\def\large{\@setsize\large{14pt}\xiipt\@xiipt} +\def\Large{\@setsize\Large{16pt}\xivpt\@xivpt} +\def\LARGE{\@setsize\LARGE{20pt}\xviipt\@xviipt} +\def\huge{\@setsize\huge{23pt}\xxpt\@xxpt} +\def\Huge{\@setsize\Huge{28pt}\xxvpt\@xxvpt} + +\def\toptitlebar{\hrule height4pt\vskip .25in\vskip-\parskip} + +\def\bottomtitlebar{\vskip .29in\vskip-\parskip\hrule height1pt\vskip +.09in} % +%Reduced second vskip to compensate for adding the strut in \@author + + +%% % Vertical Ruler +%% % This code is, largely, from the CVPR 2010 conference style file +%% % ----- define vruler +%% \makeatletter +%% \newbox\iclrrulerbox +%% \newcount\iclrrulercount +%% \newdimen\iclrruleroffset +%% \newdimen\cv@lineheight +%% \newdimen\cv@boxheight +%% \newbox\cv@tmpbox +%% \newcount\cv@refno +%% \newcount\cv@tot +%% % NUMBER with left flushed zeros \fillzeros[] +%% \newcount\cv@tmpc@ \newcount\cv@tmpc +%% \def\fillzeros[#1]#2{\cv@tmpc@=#2\relax\ifnum\cv@tmpc@<0\cv@tmpc@=-\cv@tmpc@\fi +%% \cv@tmpc=1 % +%% \loop\ifnum\cv@tmpc@<10 \else \divide\cv@tmpc@ by 10 \advance\cv@tmpc by 1 \fi +%% \ifnum\cv@tmpc@=10\relax\cv@tmpc@=11\relax\fi \ifnum\cv@tmpc@>10 \repeat +%% \ifnum#2<0\advance\cv@tmpc1\relax-\fi +%% \loop\ifnum\cv@tmpc<#1\relax0\advance\cv@tmpc1\relax\fi \ifnum\cv@tmpc<#1 \repeat +%% \cv@tmpc@=#2\relax\ifnum\cv@tmpc@<0\cv@tmpc@=-\cv@tmpc@\fi \relax\the\cv@tmpc@}% +%% % \makevruler[][][][][] +%% \def\makevruler[#1][#2][#3][#4][#5]{\begingroup\offinterlineskip +%% \textheight=#5\vbadness=10000\vfuzz=120ex\overfullrule=0pt% +%% \global\setbox\iclrrulerbox=\vbox to \textheight{% +%% {\parskip=0pt\hfuzz=150em\cv@boxheight=\textheight +%% \cv@lineheight=#1\global\iclrrulercount=#2% +%% \cv@tot\cv@boxheight\divide\cv@tot\cv@lineheight\advance\cv@tot2% +%% \cv@refno1\vskip-\cv@lineheight\vskip1ex% +%% \loop\setbox\cv@tmpbox=\hbox to0cm{{\iclrtenhv\hfil\fillzeros[#4]\iclrrulercount}}% +%% \ht\cv@tmpbox\cv@lineheight\dp\cv@tmpbox0pt\box\cv@tmpbox\break +%% \advance\cv@refno1\global\advance\iclrrulercount#3\relax +%% \ifnum\cv@refno<\cv@tot\repeat}}\endgroup}% +%% \makeatother +%% % ----- end of vruler + +%% % \makevruler[][][][][] +%% \def\iclrruler#1{\makevruler[12pt][#1][1][3][0.993\textheight]\usebox{\iclrrulerbox}} +%% \AddToShipoutPicture{% +%% \ificlrfinal\else +%% \iclrruleroffset=\textheight +%% \advance\iclrruleroffset by -3.7pt +%% \color[rgb]{.7,.7,.7} +%% \AtTextUpperLeft{% +%% \put(\LenToUnit{-35pt},\LenToUnit{-\iclrruleroffset}){%left ruler +%% \iclrruler{\iclrrulercount}} +%% } +%% \fi +%% } +%%% To add a vertical bar on the side +%\AddToShipoutPicture{ +%\AtTextLowerLeft{ +%\hspace*{-1.8cm} +%\colorbox[rgb]{0.7,0.7,0.7}{\small \parbox[b][\textheight]{0.1cm}{}}} +%} diff --git a/templates/psycology_survey/latex/template.tex b/templates/psycology_survey/latex/template.tex new file mode 100644 index 00000000..c6bba6e8 --- /dev/null +++ b/templates/psycology_survey/latex/template.tex @@ -0,0 +1,121 @@ +\documentclass{article} % For LaTeX2e +\usepackage{iclr2024_conference,times} + +\usepackage[utf8]{inputenc} % allow utf-8 input +\usepackage[T1]{fontenc} % use 8-bit T1 fonts +\usepackage{hyperref} % hyperlinks +\usepackage{url} % simple URL typesetting +\usepackage{booktabs} % professional-quality tables +\usepackage{amsfonts} % blackboard math symbols +\usepackage{nicefrac} % compact symbols for 1/2, etc. +\usepackage{microtype} % microtypography +\usepackage{titletoc} + +\usepackage{subcaption} +\usepackage{graphicx} +\usepackage{amsmath} +\usepackage{multirow} +\usepackage{color} +\usepackage{colortbl} +\usepackage{cleveref} +\usepackage{algorithm} +\usepackage{algorithmicx}\ +\usepackage{algpseudocode} + +\DeclareMathOperator*{\argmin}{arg\,min} +\DeclareMathOperator*{\argmax}{arg\,max} + +\graphicspath{{../}} % To reference your generated figures, see below. +\begin{filecontents}{references.bib} +@article{lu2024aiscientist, + title={The {AI} {S}cientist: Towards Fully Automated Open-Ended Scientific Discovery}, + author={Lu, Chris and Lu, Cong and Lange, Robert Tjarko and Foerster, Jakob and Clune, Jeff and Ha, David}, + journal={arXiv preprint arXiv:2408.06292}, + year={2024} +} + +\end{filecontents} + +\title{TITLE HERE} + +\author{LLM\\ +Department of Computer Science\\ +University of LLMs\\ +} + +\newcommand{\fix}{\marginpar{FIX}} +\newcommand{\new}{\marginpar{NEW}} + +\begin{document} + +\maketitle + +\begin{abstract} +ABSTRACT HERE +\end{abstract} + +\section{Introduction} +\label{sec:intro} +INTRO HERE + +\section{Related Work} +\label{sec:related} +RELATED WORK HERE + +\section{Background} +\label{sec:background} +BACKGROUND HERE + +\section{Method} +\label{sec:method} +METHOD HERE + +\section{Experimental Setup} +\label{sec:experimental} +EXPERIMENTAL SETUP HERE + +\section{Results} +\label{sec:results} +RESULTS HERE + +% EXAMPLE FIGURE: REPLACE AND ADD YOUR OWN FIGURES / CAPTIONS +\begin{figure}[h] + \centering + \begin{subfigure}{0.49\textwidth} + \includegraphics[width=\textwidth]{mds_jp_happiness_similarity_matrix_run_0.png} + \label{fig:jp-happiness} + \end{subfigure} + \hfill + \begin{subfigure}{0.49\textwidth} + \includegraphics[width=\textwidth]{mds_us_happiness_similarity_matrix_run_0.png} + \label{fig:us-happiness} + \end{subfigure} + \caption{PLEASE FILL IN CAPTION HERE} + \label{fig:first_figure} +\end{figure} + +\begin{figure}[h] + \centering + \begin{subfigure}{0.49\textwidth} + \includegraphics[width=\textwidth]{mds_jp_unhappiness_similarity_matrix_run_0.png} + \label{fig:jp-unhappiness} + \end{subfigure} + \hfill + \begin{subfigure}{0.49\textwidth} + \includegraphics[width=\textwidth]{mds_us_unhappiness_similarity_matrix_run_0.png} + \label{fig:us-unhappiness} + \end{subfigure} + \caption{PLEASE FILL IN CAPTION HERE} + \label{fig:first_figure} +\end{figure} + +\section{Conclusions and Future Work} +\label{sec:conclusion} +CONCLUSIONS HERE + +This work was generated by \textsc{The AI Scientist} \citep{lu2024aiscientist}. + +\bibliographystyle{iclr2024_conference} +\bibliography{references} + +\end{document} diff --git a/templates/psycology_survey/llm.py b/templates/psycology_survey/llm.py new file mode 100644 index 00000000..2973bd4c --- /dev/null +++ b/templates/psycology_survey/llm.py @@ -0,0 +1,64 @@ +import json +from dataclasses import dataclass +from typing import Optional +import os + +import backoff +import google.genai as genai +from google.genai import types as genai_types + +@dataclass +class LLMInput: + prompt: str + max_output_tokens: Optional[int] = 1000 + temperature: float = 0 + response_type: Optional[str] = None + response_schema: Optional[dict] = None + system_instruction: Optional[str] = None + + def format_prompt(self, params): + formatted = self.prompt.format(**params) + +client = genai.client.Client(api_key=os.environ["GEMINI_API_KEY"]) + + +@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=5) +def get_llm_response(llm_input: LLMInput): + config = genai_types.GenerateContentConfig( + system_instruction=llm_input.system_instruction, + temperature=llm_input.temperature, + max_output_tokens=llm_input.max_output_tokens, + response_mime_type=llm_input.response_type, + response_schema=llm_input.response_schema + ) + resp = client.models.generate_content( + model='gemini-2.5-pro', + contents=llm_input.prompt, + config=config + ) + if not resp.candidates: + return None + return resp.text + +def get_llm_response_and_history(llm_input: LLMInput, msg_history=None) -> (str, list[str]): + config = genai_types.GenerateContentConfig( + system_instruction=llm_input.system_instruction, + temperature=llm_input.temperature, + max_output_tokens=llm_input.max_output_tokens, + response_mime_type=llm_input.response_type, + response_schema=llm_input.response_schema + ) + chat = client.chats.create( + model='gemini-2.5-flash', history=msg_history + ) + resp = chat.send_message( + llm_input.prompt, + config=config, + ) + return resp.text, chat.get_history() + +def main(): + pass + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/templates/psycology_survey/personas.py b/templates/psycology_survey/personas.py new file mode 100644 index 00000000..afba5725 --- /dev/null +++ b/templates/psycology_survey/personas.py @@ -0,0 +1,84 @@ +"""Helper function for loading personas based on yaml descriptions.""" +import argparse +from enum import Enum +import os +import pydantic +from typing import Union +from pathlib import Path +import yaml +import logging +from collections import defaultdict + +class Persona(pydantic.BaseModel): + name: str + age: int + gender: Gender + occupation: str + # Free-form description. + description: str + # List of short description of the current focus of the persona. + focus: list[str] + attrs: set[str] + + def __hash__(self): + return hash(self.description) + + def get_summary(self) -> str: + """To provide enough context to answer a survey.""" + return f""" + + age: {self.age} + gender: {self.gender} + occupation: {self.occupation} + description: {self.description} + focus: {self.focus} + + """ + +def load_personas(path: Union[str, Path]) -> list[Persona]: + personas = [] + p = Path(path) + if not p.is_dir(): + logging.error(f"Error: Path '{path}' is not a valid directory.") + return personas + + for file_path in p.glob('*.yaml'): + logging.info(f"Attempting to load personas from: {file_path.name}") + group_name = file_path.stem + personas_in_file = [] + + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + for persona in data: + persona = Persona.model_validate(persona) + personas_in_file.append(persona) + logging.info(f"Successfully loaded: '{persona.name}'") + + except pydantic.ValidationError as e: + logging.warning(f"Skipping file: '{file_path.name}' (Schema validation failed)\n{e}") + except Exception as e: + logging.error(f"An unexpected error occurred with file '{file_path.name}': {e}") + + personas.extend(personas_in_file) + + return personas + + +def main(): + parser = argparse.ArgumentParser(description="Load and display personas") + parser.add_argument( + "--path", + type=str, + default="data/happiness_unhappiness/personas", + help="personas path" + ) + args = parser.parse_args() + personas = load_personas(args.path) + print(f"#################### Personas ####################") + for persona in personas: + print(persona) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/templates/psycology_survey/plot.py b/templates/psycology_survey/plot.py new file mode 100644 index 00000000..4a0007af --- /dev/null +++ b/templates/psycology_survey/plot.py @@ -0,0 +1,58 @@ +"""Plots MDS graphs.""" +import json +import os +import os.path as osp + +import matplotlib.pyplot as plt +import numpy as np +from sklearn.cluster import KMeans +from sklearn.decomposition import PCA +from sklearn.manifold import MDS + +# LOAD FINAL RESULTS: +folders = os.listdir("./") +final_results = {} +for folder in folders: + if folder.startswith("run") and osp.isdir(folder): + with open(osp.join(folder, "final_info.json"), "r") as f: + final_results[folder] = json.load(f) + +def plot_analysis(run: str, name: str, similarity_matrix: list[list[float]]): + """Plot MDS result for the given similarity matrix.""" + data = np.array(similarity_matrix) + nmds = MDS( + n_components=2, + metric=False, + max_iter=3000, + eps=1e-12, + dissimilarity="precomputed", + random_state=42, + n_jobs=1, + n_init=1, + ) + data_nmds = nmds.fit_transform(data) + + # TODO: dynamically determine the number of clusters + # kmeans = KMeans(n_clusters=4, random_state=42, n_init='auto') + # kmeans.fit(data_nmds) + # labels = kmeans.labels_ + + pca = PCA() + data_pca = pca.fit_transform(data_nmds) + + fig = plt.figure(1) + ax = plt.axes([0.0, 0.0, 1.0, 1.0]) + + s = 100 + plt.scatter(data_pca[:, 0], data_pca[:, 1], s=s, lw=0, label="NMDS") + # ADD labels to the axis based on associating the survey response with MDS result. + # Ex: "left side of x axis has theme A and right side of x axis has theme B, therefore the axis likely means " + # TODO" + plt.savefig(f"mds_{name}_{run}.png") + plt.close(fig) + +for run, final_result in final_results.items(): + for key, similarity_matrix in final_result.items(): + # similarity matrix are stored using key with "similarity_matrix" suffix. + if key.find("similarity_matrix") != -1: + plot_analysis(run, key, similarity_matrix['matrix']) diff --git a/templates/psycology_survey/prompt.json b/templates/psycology_survey/prompt.json new file mode 100644 index 00000000..92aa88fe --- /dev/null +++ b/templates/psycology_survey/prompt.json @@ -0,0 +1,4 @@ +{ + "system": "You are an ambitious interdisciplinary PhD student studying AI and psychology in Japan. You're inspired by 'Between Us:How Culture Create Emotions', and would like to reproduce shared emotions in Japan using LLMs.", + "task_description": "You are given the following file to work with, that simulates human interactions using LLMs" +} \ No newline at end of file diff --git a/templates/psycology_survey/requirements.txt b/templates/psycology_survey/requirements.txt new file mode 100644 index 00000000..9138bc45 --- /dev/null +++ b/templates/psycology_survey/requirements.txt @@ -0,0 +1,2 @@ +google-genai==1.36.0 +scikit-learn==1.7.2 \ No newline at end of file diff --git a/templates/psycology_survey/seed_ideas.json b/templates/psycology_survey/seed_ideas.json new file mode 100644 index 00000000..720e1a78 --- /dev/null +++ b/templates/psycology_survey/seed_ideas.json @@ -0,0 +1,11 @@ +[ + { + "Name": "happiness_and_unhappiness_in_east_and_west_in_llms", + "Title": "Happiness and Unhappiness in LLMs: Comparing LLM personas in Japan and the US.", + "Experiment": "In this experiment, we try to reproduce the landmark psychology study 'happiness and unhappiness in east and west themes and variatios' by Uchida and Kitayama, but using LLM in different languages instead of real people. We survey LLM personas in 2 languages. The goal is to compare themes in the 2 languages, and producing a MDS analysis like the original paper to contrast the LLM personas in the 2 different languages.", + "Interestingness": 5, + "Feasibility": 6, + "Novelty": 8, + "novel": true + } +] diff --git a/templates/psycology_survey/survey.py b/templates/psycology_survey/survey.py new file mode 100644 index 00000000..f7001358 --- /dev/null +++ b/templates/psycology_survey/survey.py @@ -0,0 +1,218 @@ +"""Helper functions to run a virtual survey.""" +from __future__ import annotations +from typing import Union, Optional, Literal, Any +from pathlib import Path +import json +import logging +import argparse +import os +from collections import defaultdict, OrderedDict + +import pydantic + +SURVEYER_SYSTEM_INSTRUCTION = """ +Your are an expert surveyer trying to understand social and societal treands. + +You will be provided with: +1. **Persona Description:** A detailed description of the persona. +2. **Question:** A societal question that the described person is to answer. + +Your goal is to answer the question from the perspective of the persona. +""" + +class TextResponse(pydantic.BaseModel): + type: Literal['string'] + +class ChoicesResponse(pydantic.BaseModel): + type: Literal['choices'] + choices: list[str] + +class RepeatedResponse(pydantic.BaseModel): + count: int + type: Literal['repeated'] + sub_type: Union[TextResponse, ChoicesResponse] + +class Question(pydantic.BaseModel): + id: str + question: str + # Target audience for this question. Audience must meet all requirements. + targets: set[str] + response_format: Union[RepeatedResponse, ChoicesResponse, TextResponse] + # Actions to process the responses of the answers of all targets + # ex: Summarize common themes. + post_processing: list[str] = [] + + def __hash__(self): + return hash(f"{self.id}.{self.question}") + + def get_prompt_for(self, persona_summary: str): + return f""" + How might the person described below answer the question "{self.question}" + {persona_summary} + """ + + def get_response_schema(self): + def get_field_type(resp_format): + if isinstance(resp_format, ChoicesResponse): + return Literal[tuple(self.response_format.sub_type.choices)] + else: + return str + if isinstance(self.response_format, RepeatedResponse): + field_type = get_field_type(self.response_format.sub_type) + field_definitions = {f"line_{i+1}":(field_type, ...) for i in range(self.response_format.count)} + return pydantic.create_model("response", **field_definitions) + else: + field_type = get_field_type(self.response_format) + field_definitions = {"answer": (field_type, ...)} + return pydantic.create_model("response", **field_definitions) + +class Survey(pydantic.BaseModel): + title: str + description: str + questions: list[Question] + analysis: list[dict] + + def __str__(self): + questions = "\n".join([f"{q.question}" for q in self.questions]) + return ( + f"Title: {self.title}\n" + f"Desc: {self.description}\n" + f"Questions:\n{questions}\n" + ) + + def get_question_by_id(self, id): + for q in self.questions: + if q.id == id: + return q + return None + + def get_relative_path(self): + return self.title.lower().replace(" ", "_") + +SURVEY_RESP_TYPE = Union[dict, str, list] + +class SurveyResult(pydantic.BaseModel): + # Stores pairs for each respondent. + responses: dict[str, dict[Question, SURVEY_RESP_TYPE]] + + def to_json(self): + j = {} + for respondent_id, responses in self.responses.items(): + j[respondent_id] = {} + for question, response in responses.items(): + j[respondent_id][question.id] = response + return j + + def get_responses_for_question(self, q_id): + resp = {} + for respondent_id, responses in self.responses.items(): + for q, response in responses.items(): + if q.id == q_id: + resp[respondent_id] = response + return resp + + def merge_responses(self, q1, q2): + q1_responses = { + respondent_id: response[q1] for respondent_id, response in self.responses.items()} + q2_responses = { + respondent_id: response[q2] for respondent_id, response in self.responses.items()} + + merged = {} + for respondent_id, q1_resp in q1_responses.items(): + merged[respondent_id] = { k: [v] for k, v in q1_resp.items()} + if respondent_id not in q2_responses: + raise Exception("Question {} response not found for respondent {}".format(q2, respondent_id)) + q2_resp = q2_responses[respondent_id] + for k, v in q2_resp.items(): + if k not in merged[respondent_id]: + raise Exception("Invalid response shape, cannot merge") + merged[respondent_id][k].append(v) + + return merged + + @classmethod + def load(cls, base: str, survey: Survey): + # map question id back to questions. + questions = {q.id: q for q in survey.questions } + p = Path(base) + + resp_by_respondent = defaultdict(dict) + for path in p.glob('*'): + with open(path, 'r') as f: + resp_id = Path(path).stem + lines = list(map(lambda l: l.strip(), f.readlines())) + question_id = None + for line in lines: + if not line: + question_id = None + elif question_id is None: + # Saved as . + i, _ = line.split(".", 1) + question_id = i + elif ":" in line: + k, resp = line.split(":", 1) + if question_id not in questions: + raise Exception("Invalid question id found in saved survey result") + question = questions[question_id] + if question not in resp_by_respondent[resp_id]: + resp_by_respondent[resp_id][question] = {} + resp_by_respondent[resp_id][question][k] = resp.strip() + else: + print(f"Invalid line for in survey result: {line}") + + + return cls.model_validate({"responses": resp_by_respondent}) + + def save(self, base: str): + """Saves each person's repsonses to a separate file.""" + Path(base).mkdir(parents=True, exist_ok=True) + for respondent_id, resps in self.responses.items(): + path = os.path.join(base, respondent_id) + with open(path, 'w') as f: + for question, resp in resps.items(): + f.write(f"{question.id}.{question.question}\n") + if isinstance(resp, dict): + for k, r in resp.items(): + f.write(f"{k}: {r}\n") + else: + f.write(f"line_1: {resp}\n") + f.write("\n") + +def load_survey(path: str) -> Survey: + survey = [] + try: + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + + survey = Survey.model_validate(data) + logging.info(f"Successfully loaded survey: '{survey.title}'") + + except json.JSONDecodeError: + logging.warning(f"Skipping file: '{path}' (Invalid JSON format)") + except pydantic.ValidationError as e: + logging.warning(f"Skipping file: '{path}' (Schema validation failed)\n{e}") + except Exception as e: + logging.error(f"An unexpected error occurred with file '{path}': {e}") + + return survey + + +def main(): + parser = argparse.ArgumentParser(description="Load and display surveys") + parser.add_argument( + "--path", + type=str, + default="data/happiness_unhappiness/survey.json", + help="survey path" + ) + args = parser.parse_args() + survey = load_survey(args.path) + for a in survey.analysis: + print(f"Analysis: {a}") + for q in survey.questions: + print(f"Question: {q.question}") + print(f"Post Processing: {q.post_processing}") + print("") + +if __name__ == "__main__": + main() \ No newline at end of file