Skip to content

Commit fcb53ed

Browse files
authored
Allow --prompt and --system_prompt to accept file paths (#17)
1 parent ec96c99 commit fcb53ed

File tree

4 files changed

+162
-49
lines changed

4 files changed

+162
-49
lines changed

README.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ For the image scope, the program takes up to two files, depending on the prompt
2929
| Argument | Description | Required |
3030
|----------------------|-------------------------------------------------------------------|----------|
3131
| `--submission_type` | Type of submission (from `arg_options.FileType`) ||
32-
| `--prompt` | The name of a preddefined prompt file (from `arg_options.Prompt`) |**|
32+
| `--prompt` | Pre-defined prompt name or file path to custom prompt file |**|
3333
| `--prompt_text` | Additional string text prompt that can be fed to model. |** |
34-
| `--prompt_custom` | The name of prompt file uploaded to be used by model. |** |
3534
| `--scope` | Processing scope (`image` or `code` or `text`) ||
3635
| `--submission` | Submission file path ||
3736
| `--question` | Specific question to evaluate ||
@@ -41,10 +40,10 @@ For the image scope, the program takes up to two files, depending on the prompt
4140
| `--test_output` | File path for the file containing the results from tests ||
4241
| `--submission_image` | File path for the submission image file ||
4342
| `--solution_image` | File path for the solution image file ||
44-
| `--system_prompt` | File path for the system instructions prompt ||
43+
| `--system_prompt` | Pre-defined system prompt name or file path to custom system prompt ||
4544
| `--llama_mode` | How to invoke deepSeek-v3 (choices in `arg_options.LlamaMode`) ||
4645
| `--output_template` | Output template file (from `arg_options.OutputTemplate) ||
47-
** One of either prompt, prompt_custom, or prompt_text must be selected.
46+
** One of either `--prompt` or `--prompt_text` must be selected.
4847

4948
## Scope
5049
The program supports three scopes: code or text or image. Depending on which is selected, the program supports different models and prompts tailored for each option.
@@ -66,8 +65,15 @@ The user can also explicitly specify the submission type using the `--submission
6665
Currently, jupyter notebook, pdf, and python assignments are supported.
6766

6867
## Prompts
69-
The user can use this argument to specify which predefined prompt they wish the model to use.
70-
To view the predefined prompts, navigate to the ai_feedback/data/prompts/user folder. Each prompt is stored as a markdown file that can contain template placeholders with the following structure:
68+
The `--prompt` argument accepts either pre-defined prompt names or custom file paths:
69+
70+
### Pre-defined Prompts
71+
To use pre-defined prompts, specify the prompt name (without extension). Pre-defined prompts are stored as markdown (.md) files in the `ai_feedback/data/prompts/user/` directory.
72+
73+
### Custom Prompt Files
74+
To use custom prompt files, specify the file path to your custom prompt. The file should be a markdown (.md) file.
75+
76+
Prompt files can contain template placeholders with the following structure:
7177

7278
```markdown
7379
Consider this question:
@@ -85,7 +91,7 @@ Prompt Naming Conventions:
8591
- Prompts to be used when --scope image is selected are prefixed with image_{}.md
8692
- Prompts to be used when --scope text is selected are prefixed with text_{}.md
8793

88-
If the --scope argument is provided and its value does not match the prefix of the selected --prompt, an error message will be displayed.
94+
Scope validation (prefix matching) only applies to pre-defined prompts. Custom prompt files can be used with any scope.
8995

9096
All prompts are treated as templates that can contain special placeholder blocks, the following template placeholders are automatically replaced:
9197
- `{context}` - Question context
@@ -122,8 +128,16 @@ All prompts are treated as templates that can contain special placeholder blocks
122128
## Prompt_text
123129
Additonally, the user can pass in a string through the --prompt_text argument. This will either be concatenated to the prompt if --prompt is used or fed in as the only prompt if --prompt is not used.
124130

125-
## Prompt_custom
126-
The user can pass in their own custom prompt file and use the --prompt_custom argument to flag that the model should use the custom prompt. This can be used instead of choosing one of the predefined prompts.
131+
## System Prompts
132+
The `--system_prompt` argument accepts either pre-defined system prompt names or custom file paths:
133+
134+
### Pre-defined System Prompts
135+
To use pre-defined system prompts, specify the system prompt name (without extension). Pre-defined system prompts are stored as markdown (.md) files in the `ai_feedback/data/prompts/system/` directory.
136+
137+
### Custom System Prompt Files
138+
To use custom system prompt files, specify the file path to your custom system prompt. The file should be a markdown (.md) file.
139+
140+
System prompts define the AI model's behavior, tone, and approach to providing feedback. They are used to set the context and personality of the AI assistant.
127141

128142
## Models
129143
The models used can be seen under the ai_feedback/models folder.
@@ -303,6 +317,11 @@ python3 -m ai_feedback --prompt code_table --scope code \
303317
--model deepSeek-v3 --llama_mode cli
304318
```
305319

320+
#### Evaluate using custom prompt file path
321+
```bash
322+
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
323+
```
324+
306325
#### Using Ollama
307326
In order to run this project on Bigmouth:
308327
1. SSH into teach.cs
@@ -340,8 +359,6 @@ Files:
340359
- python_tester_llm_pdf.py: Runs LLM on any pdf assignment (solution file and submission file) uploaded to the autotester. Creates general feedback about whether the student's written responses matches the instructors feedback. Dislayed in test outputs and overall comments.
341360
- custom_tester_llm_code.sh: Runs LLM on assignments (solution file, submission file, test output file) uploaded to the custom autotester. Currently, supports jupyter notebook files uploaded. Can specify prompt and model used in the script. Displays in overall comments and in test outputs. Can optionally uncomment the annotations section to display annotations, however the annotations will display on the .txt version of the file uploaded by the student, not the .ipynb file.
342361

343-
<<<<<<< Updated upstream
344-
345362
#### Python AutoTester Usage
346363
##### Code Scope
347364
1. Ensure the student has submitted a submission file (_submission suffixed).
@@ -406,7 +423,7 @@ Also pip install other packages that the submission or solution file uses.
406423
- Student uploads: test1_submission.ipynb, test1_submission.txt
407424

408425
NOTE: if the LLM Test Group appears to be blank/does not turn green, try increasing the timeout.
409-
=======
426+
410427
#### Custom Tester
411428
- custom_tester_llm_code.sh: Runs LLM on any assignment (solution file, submission file, test output file) uploaded to the autotester. Can specify prompt and model used in the script. Displays in overall comments and in test outputs.
412429

@@ -429,4 +446,3 @@ To run the test suite:
429446
```console
430447
$ pytest
431448
```
432-
>>>>>>> Stashed changes

ai_feedback/__main__.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from . import code_processing, image_processing, text_processing
99
from .helpers import arg_options
10-
from .helpers.constants import HELP_MESSAGES, TEST_OUTPUTS_DIRECTORY
10+
from .helpers.constants import HELP_MESSAGES
1111

1212

1313
def detect_submission_type(filename: str) -> str:
@@ -52,26 +52,77 @@ def load_markdown_template(template: str) -> str:
5252
sys.exit(1)
5353

5454

55-
def load_markdown_prompt(prompt_name: str) -> dict:
56-
"""Loads a markdown prompt file.
55+
def _load_content_with_fallback(
56+
content_arg: str, predefined_values: list[str], predefined_subdir: str, content_type: str
57+
) -> str:
58+
"""Generic function to load content by trying pre-defined names first, then treating as file path.
5759
5860
Args:
59-
prompt_name (str): Name of the prompt file (without extension)
61+
content_arg (str): Either a pre-defined name or a file path
62+
predefined_values (list[str]): List of valid pre-defined names
63+
predefined_subdir (str): Subdirectory for pre-defined files (e.g., "user", "system")
64+
content_type (str): Type of content for error messages (e.g., "prompt", "system prompt")
6065
6166
Returns:
62-
dict: Dictionary containing prompt_content
67+
str: The content
6368
6469
Raises:
65-
SystemExit: If the prompt file is not found
70+
SystemExit: If the content cannot be loaded
6671
"""
67-
try:
68-
prompt_file = os.path.join(os.path.dirname(__file__), f"data/prompts/user/{prompt_name}.md")
69-
with open(prompt_file, "r") as file:
70-
prompt_content = file.read()
71-
return {"prompt_content": prompt_content}
72-
except FileNotFoundError:
73-
print(f"Error: Prompt file '{prompt_name}.md' not found in user subfolder.")
74-
sys.exit(1)
72+
# First, check if it's a pre-defined name
73+
if content_arg in predefined_values:
74+
try:
75+
file_path = os.path.join(os.path.dirname(__file__), f"data/prompts/{predefined_subdir}/{content_arg}.md")
76+
with open(file_path, "r", encoding='utf-8') as file:
77+
return file.read()
78+
except FileNotFoundError:
79+
print(
80+
f"Error: Pre-defined {content_type} file '{content_arg}.md' not found in {predefined_subdir} subfolder."
81+
)
82+
sys.exit(1)
83+
else:
84+
# Treat as a file path
85+
try:
86+
with open(content_arg, "r", encoding='utf-8') as file:
87+
return file.read()
88+
except FileNotFoundError:
89+
print(f"Error: {content_type.title()} file '{content_arg}' not found.")
90+
sys.exit(1)
91+
except Exception as e:
92+
print(f"Error reading {content_type} file '{content_arg}': {e}")
93+
sys.exit(1)
94+
95+
96+
def load_prompt_content(prompt_arg: str) -> str:
97+
"""Loads prompt content by trying pre-defined names first, then treating as file path.
98+
99+
Args:
100+
prompt_arg (str): Either a pre-defined prompt name or a file path
101+
102+
Returns:
103+
str: The prompt content
104+
105+
Raises:
106+
SystemExit: If the prompt cannot be loaded
107+
"""
108+
return _load_content_with_fallback(prompt_arg, arg_options.get_enum_values(arg_options.Prompt), "user", "prompt")
109+
110+
111+
def load_system_prompt_content(system_prompt_arg: str) -> str:
112+
"""Loads system prompt content by trying pre-defined names first, then treating as file path.
113+
114+
Args:
115+
system_prompt_arg (str): Either a pre-defined system prompt name or a file path
116+
117+
Returns:
118+
str: The system prompt content
119+
120+
Raises:
121+
SystemExit: If the system prompt cannot be loaded
122+
"""
123+
return _load_content_with_fallback(
124+
system_prompt_arg, arg_options.get_enum_values(arg_options.SystemPrompt), "system", "system prompt"
125+
)
75126

76127

77128
def main() -> int:
@@ -97,12 +148,10 @@ def main() -> int:
97148
parser.add_argument(
98149
"--prompt",
99150
type=str,
100-
choices=arg_options.get_enum_values(arg_options.Prompt),
101151
required=False,
102152
help=HELP_MESSAGES["prompt"],
103153
)
104154
parser.add_argument("--prompt_text", type=str, required=False, help=HELP_MESSAGES["prompt_text"])
105-
parser.add_argument("--prompt_custom", type=str, required=False, help=HELP_MESSAGES["prompt_custom"])
106155
parser.add_argument(
107156
"--scope",
108157
type=str,
@@ -147,7 +196,6 @@ def main() -> int:
147196
"--system_prompt",
148197
type=str,
149198
required=False,
150-
choices=arg_options.get_enum_values(arg_options.SystemPrompt),
151199
help=HELP_MESSAGES["system_prompt"],
152200
default="student_test_feedback",
153201
)
@@ -168,18 +216,12 @@ def main() -> int:
168216

169217
prompt_content = ""
170218

171-
system_prompt_path = os.path.join(
172-
os.path.dirname(os.path.abspath(__file__)), f"data/prompts/system/{args.system_prompt}.md"
173-
)
174-
with open(system_prompt_path, encoding='utf-8') as file:
175-
system_instructions = file.read()
219+
system_instructions = load_system_prompt_content(args.system_prompt)
176220

177-
if args.prompt_custom:
178-
prompt_filename = os.path.join("./", args.prompt_custom)
179-
with open(prompt_filename, encoding='utf-8') as prompt_file:
180-
prompt_content += prompt_file.read()
181-
else:
182-
if args.prompt:
221+
if args.prompt:
222+
# Only validate scope for pre-defined prompts (not for arbitrary file paths)
223+
predefined_prompts = arg_options.get_enum_values(arg_options.Prompt)
224+
if args.prompt in predefined_prompts:
183225
if not args.prompt.startswith("image") and args.scope == "image":
184226
print("Error: The prompt must start with 'image'. Please re-run the command with a valid prompt.")
185227
sys.exit(1)
@@ -190,14 +232,13 @@ def main() -> int:
190232
print("Error: The prompt must start with 'text'. Please re-run the command with a valid prompt.")
191233
sys.exit(1)
192234

193-
prompt = load_markdown_prompt(args.prompt)
194-
prompt_content += prompt["prompt_content"]
235+
prompt_content += load_prompt_content(args.prompt)
195236

196-
if args.prompt_text:
197-
prompt_content += args.prompt_text
237+
if args.prompt_text:
238+
prompt_content += args.prompt_text
198239

199240
if args.scope == "image":
200-
prompt["prompt_content"] = prompt_content
241+
prompt = {"prompt_content": prompt_content}
201242
request, response = image_processing.process_image(args, prompt, system_instructions)
202243
elif args.scope == "text":
203244
request, response = text_processing.process_text(args, prompt_content, system_instructions)

ai_feedback/helpers/constants.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
TEST_OUTPUTS_DIRECTORY = "test_responses_md"
22
HELP_MESSAGES = {
33
"submission_type": "The format of the submission file (e.g., Jupyter notebook, Python script).",
4-
"prompt": "The specific prompt to use for evaluating the assignment.",
4+
"prompt": "Pre-defined prompt name (from ai_feedback/data/prompts/user/) or file path to custom prompt file.",
55
"prompt_text": "Additional messages to concatenate to the prompt.",
6-
"prompt_custom": "The path to a prompt to use.",
76
"scope": "The section of the assignment the model should analyze (e.g., code or image).",
87
"submission": "The file path for the submission file.",
98
"solution": "The file path for the solution file.",
@@ -15,5 +14,5 @@
1514
"test_output": "The output of tests from evaluating the assignment.",
1615
"submission_image": "The file path for the image file.",
1716
"solution_image": "The file path to the solution image.",
18-
"system_prompt": "The specific system instructions to send to the AI Model.",
17+
"system_prompt": "Pre-defined system prompt name (from ai_feedback/data/prompts/system/) or file path to custom system prompt file.",
1918
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from unittest.mock import mock_open, patch
2+
3+
import pytest
4+
5+
from ai_feedback.__main__ import _load_content_with_fallback
6+
7+
8+
class TestLoadContentWithFallback:
9+
"""Test cases for _load_content_with_fallback function - the core logic we rely on."""
10+
11+
def test_load_content_predefined_name_success(self):
12+
"""Test loading content using a predefined name."""
13+
content = "Test prompt content"
14+
predefined_values = ["test_prompt", "another_prompt"]
15+
16+
with patch("builtins.open", mock_open(read_data=content)):
17+
with patch("os.path.join") as mock_join:
18+
mock_join.return_value = "/fake/path/user/test_prompt.md"
19+
20+
result = _load_content_with_fallback("test_prompt", predefined_values, "user", "prompt")
21+
22+
assert result == content
23+
mock_join.assert_called_once()
24+
25+
def test_load_content_custom_file_path_success(self):
26+
"""Test loading content using a custom file path."""
27+
content = "Custom file content"
28+
predefined_values = ["predefined_prompt"]
29+
30+
with patch("builtins.open", mock_open(read_data=content)):
31+
result = _load_content_with_fallback("/custom/path/file.md", predefined_values, "user", "prompt")
32+
33+
assert result == content
34+
35+
def test_load_content_predefined_name_not_found(self, capsys):
36+
"""Test error when predefined file is not found."""
37+
predefined_values = ["test_prompt"]
38+
39+
with patch("builtins.open", side_effect=FileNotFoundError):
40+
with pytest.raises(SystemExit) as exc_info:
41+
_load_content_with_fallback("test_prompt", predefined_values, "user", "prompt")
42+
43+
assert exc_info.value.code == 1
44+
captured = capsys.readouterr()
45+
assert "Pre-defined prompt file 'test_prompt.md' not found in user subfolder." in captured.out
46+
47+
def test_load_content_custom_file_path_not_found(self, capsys):
48+
"""Test error when custom file is not found."""
49+
predefined_values = ["predefined_prompt"]
50+
51+
with patch("builtins.open", side_effect=FileNotFoundError):
52+
with pytest.raises(SystemExit) as exc_info:
53+
_load_content_with_fallback("/nonexistent/file.md", predefined_values, "user", "prompt")
54+
55+
assert exc_info.value.code == 1
56+
captured = capsys.readouterr()
57+
assert "Prompt file '/nonexistent/file.md' not found." in captured.out

0 commit comments

Comments
 (0)