Skip to content

Commit b684284

Browse files
committed
Ref(analysis): refactor into class
1 parent 84f3b90 commit b684284

2 files changed

Lines changed: 84 additions & 53 deletions

File tree

codeclash/analysis/llm_as_judge/big_questions.py

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121

2222
class BigQuestionsModelResponseSchema(BaseModel):
23+
"""Schema for structured output of the model."""
24+
2325
are_edits_motivated_by_logs: bool
2426
are_edits_motivated: bool
2527
are_edits_tested_with_simulations: bool
@@ -51,54 +53,81 @@ def extract_triple_backticks(text: str) -> str:
5153
return actions[0] if actions else ""
5254

5355

54-
def evaluate(instance: Instance, config: BigQuestionsConfig) -> None:
55-
target_path = instance.trajectory_path.parent.parent.parent / "llm_as_judge.json"
56-
data_id = f"big_questions_v{config.version}_{config.model.model_name}_{instance.instance_id}"
57-
if target_path.exists():
58-
content = target_path.read_text()
59-
if content.strip():
60-
data = json.loads(content)
61-
if data.get("data_id") == data_id:
62-
logger.info(
63-
f"Skipping instance {instance.instance_id} because it already exists in {target_path} with key {data_id}"
64-
)
65-
return
66-
model = get_model(config.model.model_name, config={"model_kwargs": config.model.model_kwargs})
67-
trajectory_messages = json.loads(instance.trajectory_path.read_text())["messages"]
68-
system_message = config.system_prompt
69-
trajectory_message_str = ""
70-
for message in trajectory_messages:
71-
content = message["content"]
72-
if isinstance(message["content"], list):
73-
assert len(message["content"]) == 1
74-
content = message["content"][0]["text"]
75-
if message["role"] == "assistant":
76-
trajectory_message_str += "\n<action>\n" + extract_triple_backticks(content) + "\n</action>\n"
77-
elif message["role"] == "user":
78-
trajectory_message_str += content # already enclosed in <output>
79-
instance_message = jinja2.Template(config.instance_prompt).render(trajectory_message_str=trajectory_message_str)
80-
# print(instance_message)
81-
response = model.query(
82-
messages=[
56+
class BigQuestions:
57+
def __init__(self, config: BigQuestionsConfig):
58+
self.config = config
59+
self.model = get_model(config.model.model_name, config={"model_kwargs": config.model.model_kwargs})
60+
61+
def get_data_id(self, instance: Instance) -> str:
62+
return f"big_questions_v{self.config.version}_{self.config.model.model_name}_{instance.instance_id}"
63+
64+
def evaluate(self, instance: Instance) -> None:
65+
target_path = instance.trajectory_path.parent.parent.parent / "llm_as_judge.json"
66+
data_id = self.get_data_id(instance)
67+
68+
if self._should_skip(target_path, data_id):
69+
logger.info(
70+
f"Skipping instance {instance.instance_id} because it already exists in {target_path} with key {data_id}"
71+
)
72+
return
73+
74+
response = self.model.query(
75+
messages=self._get_messages(instance), response_format=BigQuestionsModelResponseSchema
76+
)
77+
response_data = BigQuestionsModelResponseSchema.model_validate_json(response["content"]).model_dump()
78+
79+
self._save_response(target_path, response_data, data_id)
80+
logger.info(
81+
f"Evaluated instance {instance.instance_id}: {response_data}. Saved to {target_path} with key {data_id}"
82+
)
83+
84+
def _format_traj_str(self, messages: list[dict[str, Any]]) -> str:
85+
trajectory_message_str = ""
86+
for message in messages:
87+
content = message["content"]
88+
if isinstance(message["content"], list):
89+
assert len(message["content"]) == 1
90+
content = message["content"][0]["text"]
91+
if message["role"] == "assistant":
92+
trajectory_message_str += "\n<action>\n" + extract_triple_backticks(content) + "\n</action>\n"
93+
elif message["role"] == "user":
94+
trajectory_message_str += content # already enclosed in <output>
95+
return trajectory_message_str
96+
97+
def _get_messages(self, instance: Instance) -> list[dict[str, Any]]:
98+
trajectory_messages = json.loads(instance.trajectory_path.read_text())["messages"]
99+
system_message = self.config.system_prompt
100+
instance_message = jinja2.Template(self.config.instance_prompt).render(
101+
trajectory_message_str=self._format_traj_str(trajectory_messages)
102+
)
103+
# print(instance_message)
104+
return [
83105
{"role": "system", "content": system_message},
84106
{"role": "user", "content": instance_message},
85-
],
86-
response_format=BigQuestionsModelResponseSchema,
87-
)
88-
# atomic write with file lock in case other analyses are also writing
89-
response_data = BigQuestionsModelResponseSchema.model_validate_json(response["content"]).model_dump()
90-
with FileLock(target_path):
91-
# read again if changed in the meantime
92-
data = {}
93-
if target_path.exists():
94-
content = target_path.read_text()
95-
if content.strip():
96-
data = json.loads(content)
97-
data[data_id] = response_data
98-
target_path.write_text(json.dumps(data))
99-
logger.info(
100-
f"Evaluated instance {instance.instance_id}: {response_data}. Saved to {target_path} with key {data_id}"
101-
)
107+
]
108+
109+
def _should_skip(self, target_path: Path, data_id: str) -> bool:
110+
if not target_path.exists():
111+
return False
112+
content = target_path.read_text()
113+
if not content.strip():
114+
return False
115+
data = json.loads(content)
116+
if data.get("data_id") == data_id:
117+
return True
118+
return False
119+
120+
def _save_response(self, target_path: Path, response_data: dict[str, Any], data_id: str) -> None:
121+
# atomic write with file lock in case other analyses are also writing
122+
with FileLock(target_path):
123+
# read again if changed in the meantime
124+
data = {}
125+
if target_path.exists():
126+
content = target_path.read_text()
127+
if content.strip():
128+
data = json.loads(content)
129+
data[data_id] = response_data
130+
target_path.write_text(json.dumps(data))
102131

103132

104133
if __name__ == "__main__":
@@ -108,6 +137,7 @@ def evaluate(instance: Instance, config: BigQuestionsConfig) -> None:
108137

109138
config = BigQuestionsConfig.model_validate(yaml.safe_load(config_path.read_text()))
110139
instances = get_instances(args.input_dir)
140+
big_questions = BigQuestions(config)
111141
for instance in instances:
112-
evaluate(instance, config)
142+
big_questions.evaluate(instance)
113143
print(GLOBAL_MODEL_STATS.cost)

codeclash/analysis/llm_as_judge/big_questions.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 4
33
system_prompt: |
44
You need to answer three question
55
6-
## Are edits motivated by logs?
6+
## Q1 (`are_edits_motivated_by_logs`): Are edits motivated by logs?
77
88
Are the edits of the main player file of the player directly motivated by problems discovered by reading the logs?
99
@@ -39,7 +39,7 @@ system_prompt: |
3939
- Player 2 is better most of the time (why?)
4040
- Player 1 is the last bot standing and is therefore the winner (does not explain why player 2 lost)
4141
42-
## Are edits motivated
42+
## Q2 (`are_edits_motivated`): Are edits motivated
4343
4444
Can the goal of the specific edits on the main player file be motivated by any output of previous actions?
4545
If you answered True to the previous question, answer True here as well.
@@ -63,7 +63,7 @@ system_prompt: |
6363
You should answer False, if the edits seemed unrelated to any output of previous actions before the relevant
6464
edit actions.
6565
66-
## Are edits tested with simulations?
66+
## Q3 (`are_edits_tested_with_simulations`): Are edits tested with simulations?
6767
6868
After edits to the main player file have been made, does the player simulate games by playing the game against previous versions of itself, or against example players, etc.
6969
Only if we are performing a fix or an improvement that can be validated without an opponent (e.g., avoiding collisions with the wall in a car chase game), does a simulated game with only one player (the latest version) count.
@@ -77,7 +77,7 @@ system_prompt: |
7777
7878
1. If no edits to the main player file have been made, answer `True`
7979
80-
## Are edits validated with unittests?
80+
## Q4 (`are_edits_validated_with_unittests`): Are edits validated with unittests?
8181
8282
Are the final edits covered by specific unittests that test the new or modified behavior?
8383
@@ -95,7 +95,7 @@ system_prompt: |
9595
9696
Note: if the tests did not run, or showed that the new version was broken, answer False.
9797
98-
## Categorize the kind of edits made
98+
## Q5 (`edit_category`): Categorize the kind of edits made
9999
100100
Categorize the final edits to the main file into one of the following categories.
101101
Ignore comments or documentation.
@@ -109,7 +109,8 @@ system_prompt: |
109109
110110
## Output format
111111
112-
Answer in the json format specified. Reasoning corresponds to your explanation for your answer.
112+
Answer in the json format specified.
113+
The `reasoning` field should contain an explanation for your answer that explains your reasoning for each of the answers.
113114
instance_prompt: |
114115
Here is your input file:
115116

0 commit comments

Comments
 (0)