import gradio as gr
import requests
import json
import os
from pyvis.network import Network
import networkx as nx
from openai import OpenAI
# Env Vars
METABASE_USERNAME = os.getenv('METABASE_USERNAME')
METABASE_PASSWORD = os.getenv('METABASE_PASSWORD')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
# # get by local_config.json file
# with open("local_config.json") as f:
# config = json.load(f)
# METABASE_USERNAME = config['METABASE_USERNAME']
# METABASE_PASSWORD = config['METABASE_PASSWORD']
# OPENAI_API_KEY = config['OPENAI_API_KEY']
# OpenAI API
OPENAI_API_CLIENT = OpenAI(api_key=OPENAI_API_KEY)
# Original elements
elements = [
{'data': {'id': 'jnc-4-05-1-1', 'label': '真分數、假分數與帶分數的命名及說、讀、聽、寫、做'}},
{'data': {'id': 'jnc-4-05-2-1', 'label': '假分數與帶分數的互換'}},
{'data': {'id': 'jnc-4-05-3-1', 'label': '同分母分數的大小比較'}},
{'data': {'id': 'jnc-4-05-3-2', 'label': '同分母分數的加減'}},
{'data': {'id': 'jnc-4-05-3-3', 'label': '分數的整數倍'}},
{'data': {'id': 'jnc-4-06-1-1', 'label': '認識等值分數'}},
{'data': {'id': 'jnc-4-06-1-2', 'label': '找出等值分數'}},
{'data': {'id': 'jnc-4-06-2-1', 'label': '簡單異分母分數的比較'}},
{'data': {'id': 'jnc-4-06-2-2', 'label': '簡單異分母分數的加減'}},
{'data': {'id': 'jnc-4-06-3-1', 'label': '分數與一位小數的互換'}},
{'data': {'id': 'jnc-4-06-3-2', 'label': '分數與二位小數的互換'}},
{'data': {'id': 'jnc-4-08-2-1', 'label': '認識分數數線'}},
{'data': {'id': 'jnc-4-08-2-2', 'label': '數線的整數、分數、小數'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-05-2-1'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-05-3-1'}},
{'data': {'source': 'jnc-4-05-2-1', 'target': 'jnc-4-05-3-1'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-05-3-2'}},
{'data': {'source': 'jnc-4-05-2-1', 'target': 'jnc-4-05-3-2'}},
{'data': {'source': 'jnc-4-05-2-1', 'target': 'jnc-4-05-3-3'}},
{'data': {'source': 'jnc-4-05-3-2', 'target': 'jnc-4-05-3-3'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-06-1-1'}},
{'data': {'source': 'jnc-4-06-1-1', 'target': 'jnc-4-06-1-2'}},
{'data': {'source': 'jnc-4-06-1-1', 'target': 'jnc-4-06-2-1'}},
{'data': {'source': 'jnc-4-06-1-2', 'target': 'jnc-4-06-2-1'}},
{'data': {'source': 'jnc-4-06-1-1', 'target': 'jnc-4-06-2-2'}},
{'data': {'source': 'jnc-4-06-1-2', 'target': 'jnc-4-06-2-2'}},
{'data': {'source': 'jnc-4-06-3-1', 'target': 'jnc-4-06-3-2'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-08-2-1'}},
{'data': {'source': 'jnc-4-08-2-1', 'target': 'jnc-4-08-2-2'}},
{'data': {'source': 'jnc-4-05-1-1', 'target': 'jnc-4-06-3-1'}}
]
# Create a NetworkX graph
nx_graph = nx.DiGraph()
# Add nodes and edges
for element in elements:
if 'source' not in element['data']: # It's a node
nx_graph.add_node(element['data']['id'], title=element['data']['label'])
else: # It's an edge
nx_graph.add_edge(element['data']['source'], element['data']['target'])
def update_node_colors(result_data):
color_mapping = {'1': 'green', '0': 'red', 'default': 'orange'}
for node in nx_graph.nodes:
if node in result_data:
score = str(result_data[node])
color = color_mapping.get(score, color_mapping['default'])
else:
color = color_mapping['default']
nx_graph.nodes[node]['color'] = color
# Function to generate the graph in hierarchical layout
def needs_analysis(topic_result=None, exercise_quiz_result=None):
if topic_result:
update_node_colors(topic_result)
nt = Network(directed=True)
nt.from_nx(nx_graph)
nt.repulsion(node_distance=120, central_gravity=0.0, spring_length=100, spring_strength=0.05, damping=0.09)
nt.set_options("""
{
"layout": {
"hierarchical": {
"enabled": false,
"levelSeparation": 150,
"nodeSpacing": 200,
"treeSpacing": 500,
"blockShifting": true,
"edgeMinimization": true,
"parentCentralization": true,
"direction": "UD",
"sortMethod": "directed"
}
}
}
""")
map_html = nt.generate_html()
# Replace single quotes with double quotes in HTML
map_html = map_html.replace("'", "\"")
html = f""""""
topic_table = generate_html_table(topic_result, exercise_quiz_result)
html += f"
Topic Table
{topic_table}"
return html
def query_metabase_topic(class_code, topic_card_id, user_id, username=METABASE_USERNAME, password=METABASE_PASSWORD):
try:
# 获取会话令牌
session_response = requests.post(
'https://metabase.cloud.junyiacademy.org/api/session',
headers={'Content-Type': 'application/json'},
json={'username': username, 'password': password}
)
session_response.raise_for_status()
session_token = session_response.json()['id']
print(f"Session token: {session_token}")
# 打印请求信息
request_payload = {
"parameters": [
{
"type": "category",
"target": ["variable", ["template-tag", "class_code"]],
"value": class_code
},
{
"type": "category",
"target": ["variable", ["template-tag", "user_id"]],
"value": user_id
}
]
}
print(f"Request payload: {json.dumps(request_payload, indent=2)}")
# 使用提供的 card_id 查询 Metabase 卡片
query_response = requests.post(
f'https://metabase.cloud.junyiacademy.org/api/card/{topic_card_id}/query/json',
headers={
'Content-Type': 'application/json',
'X-Metabase-Session': session_token
},
json=request_payload
)
query_response.raise_for_status()
# filter class_code and user_id
query_response = query_response.json()
query_response = [item for item in query_response if item['class_code'] == class_code and item['user_id'] == user_id]
return query_response[0] if query_response else {}
except requests.RequestException as e:
print(f"Failed to query Metabase card: {str(e)}")
return {"error": f"Failed to query Metabase card: {str(e)}"}
except Exception as e:
print(f"Error: {str(e)}")
return {"error": str(e)}
def query_metabase_exercise_quiz(class_code, exercise_card_id, user_id, username=METABASE_USERNAME, password=METABASE_PASSWORD):
try:
# 获取会话令牌
session_response = requests.post(
'https://metabase.cloud.junyiacademy.org/api/session',
headers={'Content-Type': 'application/json'},
json={'username': username, 'password': password}
)
session_response.raise_for_status()
session_token = session_response.json()['id']
print(f"Session token: {session_token}")
# 打印请求信息
request_payload = {
"parameters": [
{
"type": "category",
"target": ["variable", ["template-tag", "class_code"]],
"value": class_code
},
{
"type": "category",
"target": ["variable", ["template-tag", "user_id"]],
"value": user_id
}
]
}
print(f"Request payload: {json.dumps(request_payload, indent=2)}")
# 使用提供的 card_id 查询 Metabase 卡片
query_response = requests.post(
f'https://metabase.cloud.junyiacademy.org/api/card/{exercise_card_id}/query/json',
headers={
'Content-Type': 'application/json',
'X-Metabase-Session': session_token
},
json=request_payload
)
query_response.raise_for_status()
# filter class_code and user_id
query_response = query_response.json()
print(f"query_response: {query_response}")
query_response = [item for item in query_response if item['class_code'] == class_code and item['user_id'] == user_id]
return query_response
except requests.RequestException as e:
print(f"Failed to query Metabase card: {str(e)}")
return {"error": f"Failed to query Metabase card: {str(e)}"}
except Exception as e:
print(f"Error: {str(e)}")
return {"error": str(e)}
def generate_html_table(result_data, exercise_quiz_result):
color_mapping = {'1': 'green', '0': 'red', 'default': 'orange'}
table_html = """
ID |
Name |
Exercise Results |
"""
for node in nx_graph.nodes:
name = nx_graph.nodes[node].get('title', node)
if node in result_data:
score = str(result_data[node])
color = color_mapping.get(score, color_mapping['default'])
else:
color = color_mapping['default']
exercise_results = [
f"Quiz ID: {quiz['quiz_id']}, Correct: {quiz['is_correct']}, Time Taken: {quiz['total_time_taken']}, Hint Used: {quiz['is_hint_used']}"
for quiz in exercise_quiz_result
if quiz['exercise_title'] == name and quiz['user_id'] == result_data['user_id'] and quiz['class_code'] == result_data['class_code']
]
exercise_results_html = '' + ''.join(exercise_results) + '
'
row_id = f"row-{node}"
table_html += f"""
{node} |
{name} |
{exercise_results_html} |
"""
table_html += "
"
return table_html
def get_ai_suggestion(topic_result, exercise_quiz_result):
# 使用 OPenAI API 获取建议
model = "gpt-4o"
sys_content = f"""
You are a professional teaching expert. I am a classroom teacher.
Please provide user (he/she is a teacher) a personalized analysis
"""
user_content = f"""
Based on the data,
topic: {topic_result},
exercise_quiz_result: {exercise_quiz_result}
Please provide suggestions for the course.
rules:
- The suggestions should be based on the data provided.
- The suggestions should be actionable and specific.
- The suggestions should be relevant to the course content.
- use ZH-TW language, this is very important.
- return markdown format
- user will get a knowledge graph and data, 1 will be the best, 0 will be the worst.
then 1 will show color green, 0 will show color red, others will show color orange.
so you can just transfer the number to color to explain the user's performance.
restrictions:
- don't use any personal information
- don't use any sensitive information
- don't use any offensive language
- don't use any inappropriate language
- don't use any 簡體字,or 大陸用語,ex: 視頻請替換成影片、練習冊請替換成練習本、等等
for this student based on the given knowledge graph and data:
1. How to interpret this knowledge graph.
2. How to assess the student's strengths and weaknesses.
3. How I can help this student improve.
for example:
# 針對 emdob01 學生的課程建議
### 1. 如何解讀此知識圖與數據
知識圖呈現學生在不同知識點上的掌握程度,用數字顯示熟悉度(0-1)。例如:
- `jnc-4-05-3-1` : 1(綠色) 代表完全掌握
- `jnc-4-06-2-2` : 0(紅色) 代表未掌握
該學生的表現數據顯示各個練習題的回答正確性與所花費的時間。這有助於了解學生對不同題目的理解程度和答題速度。
# 2. 如何評估學生的強項與弱項
### 強項
- 學生在某些特定的練習題中得到了正確答案。例如:
- `認識等值分數` 題目 `quiz_id` 108363,答對且用時62秒
- `真分數、假分數與帶分數的命名及說、讀、聽、寫、做` 題目 `quiz_id` 116882,答對且用時123秒
### 弱項
- 學生在大多數練習題中答錯問題,特別在以下主題:
- `認識等值分數`:多次答錯
- `簡單異分母分數的比較`:多次答錯,且用時較長
- `假分數與帶分數的互換`:多次答錯
# 3. 如何幫助學生改進
基於以上分析,給予以下改善建議:
### 針對弱點加強練習
1. **認識等值分數**:
- 提供更多視覺化的教學資源,如分數圖表和視頻講解。
- 設計一系列有針對性的互動練習,幫助學生理解分數等值的概念。
- 安排小組討論,讓學生分享他們的解題思路並相互學習。
2. **簡單異分母分數的比較**:
- 強化基本概念教學,如找共同分母的方法。
- 設計漸進增難度的習題,從簡單到複雜,逐步提高學生的理解和應用能力。
- 鼓勵學生使用實物模型或數線來理解異分母分數的比較。
3. **假分數與帶分數的互換**:
- 重點教學如何將假分數轉換為帶分數,並展示具體步驟。
- 制作練習題目集,讓學生反復練習,並提供及時的反饋和指導。
- 透過遊戲或互動活動來增強學生對該概念的興趣和記憶。
# 總結與提點建議
- 在課後提供補充資料(如影片、練習冊)供學生複習。
- 設計定期的小測試來檢驗學生的進步情況。
- 鼓勵學生對於每次錯誤進行反思,分析失誤原因並加以改正。
- 建立個性化學習計劃,根據每次測試結果調整練習重點和方法。
希望這些建議能夠幫助該學生提升學習效果並更好地掌握課程內容。
"""
print(f"user_content: {user_content}")
messages = [
{"role": "system", "content": sys_content},
{"role": "user", "content": user_content}
]
request_payload = {
"model": model,
"messages": messages,
"max_tokens": 4000,
}
response = OPENAI_API_CLIENT.chat.completions.create(**request_payload)
suggestion = response.choices[0].message.content
return suggestion
with gr.Blocks() as app:
gr.Markdown("# Metabase Query and Visualization")
with gr.Row():
topic_card_id = gr.Textbox(label="Topic Card ID", value="6267")
exercise_card_id = gr.Textbox(label="Exercis Quiz Card ID", value="6284")
class_code = gr.Textbox(label="Class Code", value="CAFSR")
user_id = gr.Textbox(label="User ID", value="emdob01")
all_in_one_button = gr.Button("Fetch Data and Generate Graph")
with gr.Accordion(open=False, label="Raw Data"):
topic_result = gr.JSON(label="Query Result")
exercise_quiz_result = gr.JSON(label="Quiz Query Result")
with gr.Row():
graph_html = gr.HTML()
with gr.Row():
gr.Markdown("# AI suggestion Powered by Junyi Academy")
with gr.Row():
ai_suggestion = gr.Markdown()
all_in_one_button.click(
fn=query_metabase_topic,
inputs=[class_code, topic_card_id, user_id],
outputs=topic_result
).then(
fn=query_metabase_exercise_quiz,
inputs=[class_code, exercise_card_id, user_id],
outputs=exercise_quiz_result
).then(
fn=needs_analysis,
inputs=[topic_result, exercise_quiz_result],
outputs=graph_html
).then(
fn=get_ai_suggestion,
inputs=[topic_result, exercise_quiz_result],
outputs=ai_suggestion
)
app.launch()