|
| 1 | +import re |
| 2 | +from typing import Any, TypedDict |
| 3 | +import os |
| 4 | +from dotenv import load_dotenv |
| 5 | +from langchain_openai import ChatOpenAI |
| 6 | + |
| 7 | + |
| 8 | +def parse_markdown_with_images(markdown_text: str): |
| 9 | + """Parse markdown text that may contain embedded images ()""" |
| 10 | + pattern = r'!\[.*?\]\((.*?)\)' |
| 11 | + content = [] |
| 12 | + last_end = 0 |
| 13 | + |
| 14 | + for match in re.finditer(pattern, markdown_text): |
| 15 | + start, end = match.span() |
| 16 | + url = match.group(1).strip() |
| 17 | + |
| 18 | + text_before = markdown_text[last_end:start].strip() |
| 19 | + if text_before: |
| 20 | + content.append({"type": "text", "text": text_before}) |
| 21 | + |
| 22 | + content.append({"type": "image_url", "image_url": {"url": url}}) |
| 23 | + last_end = end |
| 24 | + |
| 25 | + remaining = markdown_text[last_end:].strip() |
| 26 | + if remaining: |
| 27 | + content.append({"type": "text", "text": remaining}) |
| 28 | + return content |
| 29 | + |
| 30 | + |
| 31 | +def eval_with_feedback( |
| 32 | + question_markdown: str, |
| 33 | + part_markdown: str, |
| 34 | + pre_response_text: str, |
| 35 | + student_answer: str, |
| 36 | + post_response_text: str, |
| 37 | + correct_answer: str, |
| 38 | +) -> str: |
| 39 | + """ |
| 40 | + Evaluate a student's answer based on combined context from: |
| 41 | + - Question (text + images) |
| 42 | + - Part (text + images) |
| 43 | + - Pre and post response text |
| 44 | + """ |
| 45 | + load_dotenv() |
| 46 | + |
| 47 | + llm = ChatOpenAI( |
| 48 | + model=os.environ["OPENAI_MODEL"], # must support image input (e.g. gpt-4o, gpt-5) |
| 49 | + api_key=os.environ["OPENAI_API_KEY"], |
| 50 | + ) |
| 51 | + |
| 52 | + # Parse both question and part markdowns |
| 53 | + question_content = parse_markdown_with_images(question_markdown) |
| 54 | + part_content = parse_markdown_with_images(part_markdown) |
| 55 | + |
| 56 | + # Feedback generation instruction prompt |
| 57 | + instruction_text = fr""" |
| 58 | +Follow these steps carefully: |
| 59 | +
|
| 60 | +You are given: |
| 61 | +- A question and its sub-part (each may include diagrams or equations). |
| 62 | +- The pre-response text and post-response text that appear around the student's answer box. |
| 63 | +- The student's answer and the correct answer. |
| 64 | +
|
| 65 | +Your task: |
| 66 | +1. Understand the problem statement and its context (including the question, part, and images). |
| 67 | +2. Analyze the reasoning that leads from the question to the correct answer. |
| 68 | +3. Identify *why* the student’s answer might differ (conceptual misunderstanding, skipped step, sign/unit error, etc.). |
| 69 | +4. Write one **short, indirect feedback sentence** that: |
| 70 | + - Encourages the student to rethink that specific step or concept (thought trigger), and |
| 71 | + - Refers to the relevant mathematical action or context (action trigger). |
| 72 | +5. Do NOT reveal the correct formula or result. |
| 73 | +
|
| 74 | +Guidelines: |
| 75 | +- Use imperative mood: "Re-examine...", "Review...", "Reconsider...", "Verify...". |
| 76 | +- Mention a specific step or operation, e.g. "when integrating", "when substituting", "when solving for x". |
| 77 | +- Keep it concise (max 15 words). |
| 78 | +- Be constructive and professional. |
| 79 | +
|
| 80 | +Now, generate only the final feedback sentence. |
| 81 | +
|
| 82 | +Pre-response text: {pre_response_text} |
| 83 | +Student's answer (LaTeX): {student_answer} |
| 84 | +Post-response text: {post_response_text} |
| 85 | +Correct answer (LaTeX): {correct_answer} |
| 86 | +
|
| 87 | +Output only the feedback sentence. |
| 88 | +""" |
| 89 | + |
| 90 | + # Combine all content, preserving order and image placement |
| 91 | + full_content = ( |
| 92 | + [{"type": "text", "text": "Main question:"}] |
| 93 | + + question_content |
| 94 | + + [{"type": "text", "text": "\nSub-part:"}] |
| 95 | + + part_content |
| 96 | + + [{"type": "text", "text": instruction_text}] |
| 97 | + ) |
| 98 | + |
| 99 | + response = llm.invoke([{"role": "user", "content": full_content}]) |
| 100 | + return response.content.strip() |
0 commit comments