Skip to content

Commit b251f8d

Browse files
authored
Add json schema to compatible models (#20)
1 parent 624d7f4 commit b251f8d

16 files changed

+248
-21
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ For the image scope, the program takes up to two files, depending on the prompt
4343
| `--system_prompt` | Pre-defined system prompt name or file path to custom system prompt ||
4444
| `--llama_mode` | How to invoke deepSeek-v3 (choices in `arg_options.LlamaMode`) ||
4545
| `--output_template` | Output template file (from `arg_options.OutputTemplate) ||
46+
| `--json_schema` | File path to json file for schema for structured output ||
4647
** One of either `--prompt` or `--prompt_text` must be selected. If both are provided, `--prompt_text` will be appended to the contents of the file specified by `--prompt`.
4748

4849
## Scope
@@ -317,6 +318,12 @@ python3 -m ai_feedback --prompt code_table --scope code \
317318
--model deepSeek-v3 --llama_mode cli
318319
```
319320

321+
322+
#### Get annotations for cnn_example test using openAI model
323+
```bash
324+
python -m ai_feedback --prompt code_annotations --scope code --submission test_submissions/cnn_example/cnn_submission --solution test_submissions/cnn_example/cnn_solution.py --model openai --json_schema ai_feedback/data/schema/code_annotation_schema.json
325+
```
326+
320327
#### Evaluate using custom prompt file path
321328
```bash
322329
python -m ai_feedback --prompt ai_feedback/data/prompts/user/code_overall.md --scope code --submission test_submissions/csc108/correct_submission/correct_submission.py --solution test_submissions/csc108/solution.py --model codellama:latest

ai_feedback/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ def main() -> int:
207207
default="cli",
208208
help=HELP_MESSAGES["llama_mode"],
209209
)
210+
parser.add_argument(
211+
"--json_schema",
212+
type=str,
213+
required=False,
214+
default="",
215+
help=HELP_MESSAGES["json_schema"],
216+
)
210217

211218
args = parser.parse_args()
212219

ai_feedback/code_processing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def process_code(args, prompt: str, system_instructions: str) -> Tuple[str, str]
8787
question_num=args.question,
8888
system_instructions=system_instructions,
8989
llama_mode=args.llama_mode,
90+
json_schema=args.json_schema,
9091
)
9192
else:
9293
request, response = model.generate_response(
@@ -96,6 +97,7 @@ def process_code(args, prompt: str, system_instructions: str) -> Tuple[str, str]
9697
test_output=test_output_file,
9798
system_instructions=system_instructions,
9899
llama_mode=args.llama_mode,
100+
json_schema=args.json_schema,
99101
)
100102

101103
return request, response
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "student_code_annotation",
3+
"description": "List of code annotations describing specific mistakes in the student's code.",
4+
"schema": {
5+
"type": "object",
6+
"properties": {
7+
"annotations": {
8+
"type": "array",
9+
"items": {
10+
"type": "object",
11+
"properties": {
12+
"filename": {
13+
"type": "string",
14+
"description": "The name of the student's file where the issue was found."
15+
},
16+
"content": {
17+
"type": "string",
18+
"description": "A short description of the mistake or issue."
19+
},
20+
"line_start": {
21+
"type": "integer",
22+
"description": "The starting line number where the issue begins.",
23+
"minimum": 1
24+
},
25+
"line_end": {
26+
"type": "integer",
27+
"description": "The ending line number where the issue ends.",
28+
"minimum": 1
29+
},
30+
"column_start": {
31+
"type": "integer",
32+
"description": "The starting column position of the mistake.",
33+
"minimum": 0
34+
},
35+
"column_end": {
36+
"type": "integer",
37+
"description": "The ending column position of the mistake.",
38+
"minimum": 0
39+
}
40+
},
41+
"required": [
42+
"filename",
43+
"content",
44+
"line_start",
45+
"line_end",
46+
"column_start",
47+
"column_end"
48+
]
49+
}
50+
}
51+
},
52+
"required": ["annotations"]
53+
}
54+
}

ai_feedback/helpers/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
"test_output": "The output of tests from evaluating the assignment.",
1515
"submission_image": "The file path for the image file.",
1616
"solution_image": "The file path to the solution image.",
17+
"json_schema": "file path to a json file that contains the schema for ai output",
1718
"system_prompt": "Pre-defined system prompt name (from ai_feedback/data/prompts/system/) or file path to custom system prompt file.",
1819
}

ai_feedback/image_processing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def process_image(args, prompt: dict, system_instructions: str) -> tuple[str, st
165165
system_instructions=system_instructions,
166166
question_num=question,
167167
submission_image=args.submission_image,
168+
json_schema=args.json_schema,
168169
)
169170
responses.append(str(response))
170171
else:

ai_feedback/models/ClaudeModel.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def generate_response(
2929
question_num: Optional[int] = None,
3030
test_output: Optional[Path] = None,
3131
llama_mode: Optional[str] = None,
32+
json_schema: Optional[str] = None,
3233
) -> Optional[Tuple[str, str]]:
3334
"""
3435
Generates a response from Claude using the provided prompt and assignment file context.
@@ -42,6 +43,7 @@ def generate_response(
4243
question_num (Optional[int]): Specific task number to extract from text files.
4344
system_instructions (str): instructions for the model
4445
llama_mode (Optional[str]): Optional mode to invoke llama.cpp in.
46+
json_schema (Optional[str]): Optional json schema to use.
4547
4648
Returns:
4749
Optional[Tuple[str, str]]: The original prompt and the model's response, or None if the response is invalid.

ai_feedback/models/CodeLlamaModel.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from pathlib import Path
23
from typing import Optional, Tuple
34

@@ -26,6 +27,7 @@ def generate_response(
2627
test_output: Optional[Path] = None,
2728
scope: Optional[str] = None,
2829
llama_mode: Optional[str] = None,
30+
json_schema: Optional[str] = None,
2931
) -> Optional[Tuple[str, str]]:
3032
"""
3133
Generates a response from the CodeLlama model using the provided prompt
@@ -40,18 +42,28 @@ def generate_response(
4042
question_num (Optional[int]): An optional specific question number to extract content for.
4143
system_instructions (str): instructions for the model
4244
llama_mode (Optional[str]): Optional mode to invoke llama.cpp in.
45+
json_schema (Optional[str]): Optional json schema to use.
4346
4447
Returns:
4548
Optional[Tuple[str, str]]: A tuple of the request and the model's response,
4649
or None if no valid response is returned.
4750
"""
51+
if json_schema:
52+
schema_path = Path(json_schema)
53+
if not schema_path.exists():
54+
raise FileNotFoundError(f"JSON schema file not found: {schema_path}")
55+
with open(schema_path, "r", encoding="utf-8") as f:
56+
schema = json.load(f)
57+
else:
58+
schema = None
4859

4960
response = ollama.chat(
5061
model=self.model["model"],
5162
messages=[
5263
{"role": "system", "content": system_instructions},
5364
{"role": "user", "content": prompt},
5465
],
66+
format=schema['schema'] if schema else None,
5567
)
5668

5769
if not response or "message" not in response or "content" not in response["message"]:

ai_feedback/models/DeepSeekModel.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from pathlib import Path
23
from typing import Optional, Tuple
34

@@ -24,6 +25,7 @@ def generate_response(
2425
test_output: Optional[Path] = None,
2526
scope: Optional[str] = None,
2627
llama_mode: Optional[str] = None,
28+
json_schema: Optional[str] = None,
2729
) -> Optional[Tuple[str, str]]:
2830
"""
2931
Generate a model response using the prompt and assignment files.
@@ -37,18 +39,28 @@ def generate_response(
3739
question_num (Optional[int]): An optional question number to target specific content.
3840
system_instructions (str): instructions for the model
3941
llama_mode (Optional[str]): Optional mode to invoke llama.cpp in.
42+
json_schema (Optional[str]): Optional json schema to use.
4043
4144
Returns:
4245
Optional[Tuple[str, str]]: A tuple containing the prompt and the model's response,
4346
or None if the response was invalid.
4447
"""
48+
if json_schema:
49+
schema_path = Path(json_schema)
50+
if not schema_path.exists():
51+
raise FileNotFoundError(f"JSON schema file not found: {schema_path}")
52+
with open(schema_path, "r", encoding="utf-8") as f:
53+
schema = json.load(f)
54+
else:
55+
schema = None
4556

4657
response = ollama.chat(
4758
model=self.model["model"],
4859
messages=[
4960
{"role": "system", "content": system_instructions},
5061
{"role": "user", "content": prompt},
5162
],
63+
format=schema['schema'] if schema else None,
5264
)
5365

5466
if not response or "message" not in response or "content" not in response["message"]:

ai_feedback/models/DeepSeekV3Model.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import subprocess
34
import sys
@@ -31,6 +32,7 @@ def generate_response(
3132
question_num: Optional[int] = None,
3233
test_output: Optional[Path] = None,
3334
llama_mode: Optional[str] = None,
35+
json_schema: Optional[str] = None,
3436
) -> Optional[Tuple[str, str]]:
3537
"""
3638
Generate a model response using the prompt and assignment files.
@@ -44,18 +46,28 @@ def generate_response(
4446
test_output (Optional[Path]): Path Object pointing to the test output file.
4547
llama_mode (Optional[str]): Optional mode to invoke llama.cpp in.
4648
question_num (Optional[int]): An optional question number to target specific content.
49+
json_schema (Optional[str]): Optional json schema to use.
4750
4851
Returns:
4952
Optional[Tuple[str, str]]: A tuple containing the prompt and the model's response,
5053
or None if the response was invalid.
5154
"""
55+
if json_schema:
56+
schema_path = Path(json_schema)
57+
if not schema_path.exists():
58+
raise FileNotFoundError(f"JSON schema file not found: {schema_path}")
59+
with open(schema_path, "r", encoding="utf-8") as f:
60+
schema = json.load(f)
61+
else:
62+
schema = None
63+
5264
prompt = f"{system_instructions}\n{prompt}"
5365
if llama_mode == 'server':
5466
self._ensure_env_vars('LLAMA_SERVER_URL')
55-
response = self._get_response_server(prompt)
67+
response = self._get_response_server(prompt, schema)
5668
else:
5769
self._ensure_env_vars('LLAMA_MODEL_PATH', 'LLAMA_CLI_PATH')
58-
response = self._get_response_cli(prompt)
70+
response = self._get_response_cli(prompt, schema)
5971

6072
response = response.strip()
6173

@@ -81,24 +93,24 @@ def _ensure_env_vars(self, *names):
8193
if missing:
8294
raise RuntimeError(f"Error: Environment variable(s) {', '.join(missing)} not set")
8395

84-
def _get_response_server(
85-
self,
86-
prompt: str,
87-
) -> str:
96+
def _get_response_server(self, prompt: str, schema: Optional[dict] = None) -> str:
8897
"""
8998
Generate a model response using the prompt
9099
91100
Args:
92101
prompt (str): The input prompt provided by the user.
102+
schema (Optional[dict]): Optional schema provided by the user.
93103
94104
Returns:
95105
str: A tuple containing the model response or None if the response was invalid.
96106
"""
97107
url = f"{LLAMA_SERVER_URL}/v1/completions"
98108

99-
payload = {
100-
"prompt": prompt,
101-
}
109+
payload = {"prompt": prompt, "temperature": 0.7, "max_tokens": 1000}
110+
111+
if schema:
112+
raw_schema = schema.get("schema", schema)
113+
payload["json_schema"] = raw_schema
102114

103115
try:
104116
response = requests.post(url, json=payload, timeout=3000)
@@ -116,15 +128,13 @@ def _get_response_server(
116128

117129
return model_output
118130

119-
def _get_response_cli(
120-
self,
121-
prompt: str,
122-
) -> str:
131+
def _get_response_cli(self, prompt: str, schema: Optional[dict] = None) -> str:
123132
"""
124133
Generate a model response using the prompt
125134
126135
Args:
127136
prompt (str): The input prompt provided by the user.
137+
schema (Optional[dict]): Optional schema provided by the user.
128138
129139
Returns:
130140
str: The model response or None if the response was invalid.
@@ -141,6 +151,10 @@ def _get_response_cli(
141151
"--no-display-prompt",
142152
]
143153

154+
if schema:
155+
raw_schema = schema["schema"] if "schema" in schema else schema
156+
cmd += ["--json-schema", json.dumps(raw_schema)]
157+
144158
try:
145159
completed = subprocess.run(
146160
cmd, input=prompt.encode(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300

0 commit comments

Comments
 (0)