Skip to content

Commit b00e162

Browse files
Updating parameter extraction, addition of LLM solver and feedback functions
1 parent 7477b42 commit b00e162

File tree

7 files changed

+463
-13
lines changed

7 files changed

+463
-13
lines changed

app/LLM_solver.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Any, TypedDict
2+
import os
3+
from dotenv import load_dotenv
4+
from langchain_openai import ChatOpenAI
5+
import re
6+
import requests
7+
import base64
8+
9+
10+
class Params(TypedDict):
11+
pass
12+
13+
14+
class Result(TypedDict):
15+
preview: Any
16+
17+
def LLM_solve(question_txt: str, input_type: str, pre_response_txt: str, post_response_txt: str) -> str:
18+
load_dotenv()
19+
llm = ChatOpenAI(
20+
model=os.environ['OPENAI_MODEL'],
21+
api_key=os.environ["OPENAI_API_KEY"],
22+
)
23+
24+
prompt = fr"""
25+
Follow these steps carefully:
26+
27+
A question text and its "input type" are given at the end of this prompt.
28+
The task is to answer the question only, without any additional explanation.
29+
The question is in the topic of either mathematics or science.
30+
The "input type" can be one of the following:
31+
BOOLEAN, EXPRESSION, MATRIX, MULTIPLE_CHOICE, NUMBER, NUMERIC_UNITS, TEXT.
32+
33+
For BOOLEAN type, answer either "True" or "False" only.
34+
For EXPRESSION type, answer with a mathematical expression in LaTeX format.
35+
For MATRIX type, answer with in LaTeX format the expressions or numbers for each of the element, from left to right and from top to bottom.
36+
For MULTIPLE_CHOICE type, answer with either 1st, 2nd, 3rd, or 4th only.
37+
For NUMBER type, answer with a number only.
38+
For NUMERIC_UNITS type, answer with a number followed by a space and the unit, e.g., "9.8 m/s^2".
39+
For TEXT type, answer with a short text without any explanation. The answer is usually 2 words or less.
40+
41+
The "Pre response text" and "Post response text" are also given to you at the end of this prompt to help you understand the context.
42+
For example, if the answer is "x=yz" and "Pre response text" is "x=", then you should answer with "yz" only.
43+
44+
Question text:
45+
{question_txt}
46+
Input type:
47+
{input_type}
48+
Pre response text:
49+
{pre_response_txt}
50+
Post response text:
51+
{post_response_txt}
52+
53+
Now answer the question.
54+
"""
55+
56+
expr = llm.invoke(prompt).content.strip()
57+
58+
return expr

app/LLM_solver_testing.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from LLM_solver import LLM_solve
2+
3+
Q_list = [
4+
["Does water freeze at 0 degrees Celsius?", "BOOLEAN"],
5+
["Is an electron heavier than a proton?", "BOOLEAN"],
6+
["Solve for x. x^2 - 4 = 0", "EXPRESSION"],
7+
["d/dx (x^3)", "EXPRESSION"],
8+
["Write the unitary 2x2 matrix", "MATRIX"],
9+
["Transpose [[1,2,3],[4,5,6]]", "MATRIX"],
10+
["What is the largest planet in our solar system? (a) Earth (b) Jupiter (c) Saturn (d) Venus", "MULTIPLE_CHOICE"],
11+
["What is the chemical symbol for oxygen? (a) O (b) H (c) C (d) N", "MULTIPLE_CHOICE"],
12+
["5 + 7", "NUMBER"],
13+
["Square root of 9", "NUMBER"],
14+
["What is the acceleration due to gravity on Earth in m/s^2?", "NUMERIC_UNITS"],
15+
["Speed of light in vacuum in m/s?", "NUMERIC_UNITS"],
16+
["What is the satellite of Earth?", "TEXT"],
17+
["What is the chemical formula for water?", "TEXT"],
18+
]
19+
20+
Q_list_fmx3 = [
21+
["Use elementary geometry to derive the rotation matrix that converts the Cartesian vector components (u₁, u₂, u₃) into cylindrical polar components (Vᵣ, Vθ, Vz). Write the 3x3 linear vector component transformation matrix.",
22+
"MATRIX", "$\begin{pmatrix}V_{r} \\ V_{\theta} \\ V_{z} \end{pmatrix}=$", "$\begin{pmatrix}u_{1} \\ u_{2} \\ u_{3} \end{pmatrix}$"],
23+
]
24+
25+
for q, t, pre, post in Q_list_fmx3:
26+
answer = LLM_solve(q, t, pre, post)
27+
print(answer)

app/evaluate_with_feedback.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 (![](url))"""
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()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from evaluate_with_feedback import eval_with_feedback
2+
3+
question_markdown = """
4+
Unless otherwise stated, assume standard atmosphere values of $\rho=1.225 \mathrm{~kg} / \mathrm{m}^{3}, \mu=1.79 \times 10^{-5} \mathrm{~kg} /(\mathrm{ms}), R=287.1 \mathrm{~J} /(\mathrm{kgK})$ and $\gamma=1.4$.
5+
6+
A model of the real, separated flow around a circular cylinder is to approximate it as potential flow up to the separation point $\theta=\theta_{s}$, so that for $\theta_{s} \leq \theta \leq \pi$ :
7+
8+
$$
9+
\phi=U_{\infty}\left(r+\frac{R^{2}}{r}\right) \cos \theta
10+
$$
11+
12+
Beyond the separation point ( $0 \leq \theta<\theta_{s}$ ), the cylinder surface pressure is assumed constant and equal to the potential flow value at $\theta=\theta_{s}$. This is shown graphically in following figure, where $C_{p}=\left(p-p_{\infty}\right) /\left(\frac{1}{2} \rho U_{\infty}^{2}\right)$.
13+
14+
![](https://lambda-feedback-prod-frontend-client-bucket.s3.eu-west-2.amazonaws.com/97c443aa-a1ad-494e-9277-54bcaa258dc3/ad69050e-0a87-49d7-a10f-8a746a213388.png){ width=60% }
15+
"""
16+
part_markdown = """
17+
Taking $\theta_{s}=99^{\circ}$ (i.e. separation $81^{\circ}$ from the front stagnation point), calculate the variation of $C_{p}$ on the surface.
18+
19+
When $0 < \theta \leq \theta_s$:
20+
"""
21+
pre_response_text = """
22+
$C_p=$
23+
"""
24+
student_answer = """
25+
1-4 sin( theta)^2
26+
"""
27+
post_response_text = """
28+
"""
29+
correct_answer = """
30+
1-4sin(thetas)^2
31+
"""
32+
print(eval_with_feedback(question_markdown, part_markdown, pre_response_text, student_answer, post_response_text, correct_answer))

app/extract_parameter.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def extract_parameter(question_txt: str) -> str:
3030
The conditions are mainly classified into the following two types:
3131
1) Conditions that define the properties of a constant (e.g., "x is a real number", "y is a complex number", "x > 0", etc.)
3232
2) Conditions that define the types of variables (e.g., "y is a function of x", "f is a matrix", "u is a vector", etc.)
33+
3) Conditions that define the domain of variables (e.g., "x is in (0,1)", "y is larger than or equal to 2", etc.)
3334
These may be combined together (e.g., "y is a real-valued function of x", "A is a positive definite matrix", etc.)
3435
3536
The output format is as follows:
@@ -55,13 +56,21 @@ def extract_parameter(question_txt: str) -> str:
5556
Example:
5657
"y is a function of x" → ["y(x)"]
5758
"f is a function of x and z" → ["f(x,z)"]
59+
60+
For type 3) conditions, output in the format:
61+
"domain"="(a, b)", "(a,b]", "[a, b)" or "[a,b]" depending on whether the endpoints are included or not.
62+
Example:
63+
"x is in (0,1)" → "(0,1)"
64+
"y is larger than or equal to 2 and less than 5" → "[2,5)"
65+
"z is between -1 and 1, inclusive" → "[-1,1]"
5866
59-
If both types are present, include both in the output dictionary.
67+
If more than one type is present, include both/all in the output dictionary.
6068
6169
Return the result strictly as a JSON-like Python dictionary with keys:
6270
{{
6371
"symbol_assumptions"={{...}},
64-
"function"=[...]
72+
"function"=[...],
73+
"domain"="..."
6574
}}
6675
6776
Do not include explanations, output only the dictionary.

0 commit comments

Comments
 (0)