MCPTest / app.py
KwabsHug's picture
Update app.py
9b6c9af verified
"""
NWFWO Practice – Gradio MCP app for ChatGPT
This file does two things:
1) Runs a normal Gradio web app (for debugging in your browser)
2) Exposes an MCP server + HTML UI card for ChatGPT Apps
MVP behaviour:
- ChatGPT passes in:
- transliteration: foreign sentence (clue)
- correct_nwfwo: natural translation in native language (clue)
- explanation: literal native sentence that follows the foreign info flow (correct order)
- user_guess: learner's attempt at ordering the native words to match that flow
- This tool compares the word order and returns feedback + data for the widget.
"""
from dataclasses import dataclass
from typing import Optional, List
import gradio as gr
# ---- 1. Data model (kept for future extension, not essential for MVP) ----
@dataclass
class NWFWOExample:
# Foreign sentence (text or transliteration)
foreign_original: str
# Natural translation in the native language
native_natural: str
# Literal native sentence that follows the foreign word order
native_literal_from_foreign: str
# Optional explanation / note
explanation: Optional[str] = None
# ---- 2. Core tool logic: word-order practice ----
@gr.mcp.tool(
_meta={
# Tell ChatGPT which widget template to use for this tool
"openai/outputTemplate": "ui://widget/nwfwo-practice-card.html",
"openai/resultCanProduceWidget": True,
"openai/widgetAccessible": True,
}
)
def check_nwfwo(
transliteration: str,
correct_nwfwo: str,
user_guess: str,
explanation: Optional[str] = None,
):
"""
Word-order practice for listening support.
Parameters (as ChatGPT should use them):
- transliteration:
foreign sentence (A1), shown as a clue.
- correct_nwfwo:
natural translation in the learner's native language (B1), shown as a clue.
- explanation:
literal native sentence that follows the foreign word order (A2).
This is the CORRECT target order the learner should try to match.
- user_guess:
learner's attempt at ordering the native words to match that foreign flow.
Behaviour:
- Split the correct literal sentence and user guess into tokens.
- Compare position-by-position.
- Return counts and per-position matches plus the sentences for display.
"""
# Rename for clarity
foreign_original = transliteration or ""
native_natural = correct_nwfwo or ""
native_literal_from_foreign = explanation or ""
user_order_native_literal = user_guess or ""
# Tokenise (simple space split MVP – ChatGPT should format inputs cleanly)
def tokenize(s: str) -> List[str]:
# Minimal normalisation: strip and split on whitespace
return [t for t in s.strip().split() if t]
correct_tokens = tokenize(native_literal_from_foreign)
user_tokens = tokenize(user_order_native_literal)
max_len = max(len(correct_tokens), len(user_tokens)) if correct_tokens or user_tokens else 0
matches_by_index: List[bool] = []
for i in range(max_len):
c = correct_tokens[i] if i < len(correct_tokens) else None
u = user_tokens[i] if i < len(user_tokens) else None
matches_by_index.append(c is not None and u is not None and c.lower() == u.lower())
num_correct = sum(1 for m in matches_by_index if m)
total_positions = len(correct_tokens)
if total_positions == 0:
feedback = (
"I couldn't see any words in the 'correct literal' sentence. "
"Make sure explanation contains the literal native sentence to follow."
)
else:
if num_correct == total_positions:
feedback = (
f"βœ… Great! All {total_positions} positions match the foreign information flow."
)
elif num_correct == 0:
feedback = (
"❌ None of the positions match the foreign flow yet. "
"Try lining up the verb and time/place words with how they appear in the foreign sentence."
)
else:
feedback = (
f"⚠️ You matched {num_correct} out of {total_positions} positions. "
"Look at which words moved and how that changes the flow."
)
result = {
# Original parameters (so existing callers still see them)
"transliteration": foreign_original,
"correct_nwfwo": native_natural,
"user_guess": user_order_native_literal,
"explanation": native_literal_from_foreign,
# Semantic fields
"foreign_original": foreign_original,
"native_natural": native_natural,
"native_literal_from_foreign": native_literal_from_foreign,
"correct_tokens": correct_tokens,
"user_tokens": user_tokens,
"matches_by_index": matches_by_index,
"num_correct": num_correct,
"total_positions": total_positions,
"feedback": feedback,
}
return result
# ---- 3. Normal Gradio UI (handy while you’re developing) ----
def local_check_ui(foreign_sentence, native_natural, user_order, native_literal):
res = check_nwfwo(
transliteration=foreign_sentence,
correct_nwfwo=native_natural,
user_guess=user_order,
explanation=native_literal,
)
# Simple text summary for the debug UI
lines = [
f"Foreign sentence: {res['foreign_original']}",
f"Translation (clue): {res['native_natural']}",
"",
f"Correct foreign flow in native words: {res['native_literal_from_foreign']}",
f"Your order: {' '.join(res['user_tokens'])}",
"",
f"Positions correct: {res['num_correct']} / {res['total_positions']}",
"",
f"Feedback: {res['feedback']}",
]
return "\n".join(lines)
with gr.Blocks() as demo:
gr.Markdown("## Word Order Practice – Local Debug UI")
with gr.Row():
with gr.Column():
foreign_box = gr.Textbox(
label="Foreign sentence (clue)",
value="Tomorrow together rice want-to-eat?",
)
native_natural_box = gr.Textbox(
label="Natural translation in your language (clue)",
value="Do you want to eat together tomorrow?",
)
native_literal_box = gr.Textbox(
label="Correct literal native sentence (foreign info flow)",
value="Tomorrow together rice want-to-eat?",
)
user_order_box = gr.Textbox(
label="Your ordered native sentence (try to follow foreign flow)",
value="Tomorrow rice want-to-eat together?",
)
btn = gr.Button("Check order")
with gr.Column():
result_box = gr.Textbox(
label="Result",
lines=10,
)
btn.click(
local_check_ui,
inputs=[
foreign_box,
native_natural_box,
user_order_box,
native_literal_box,
],
outputs=[result_box],
)
# ---- wire the MCP resource to a Gradio event ----
# This makes Gradio actually register the MCP resource.
widget_preview = gr.Code(
label="NWFWO widget HTML (for MCP)",
language="html",
visible=False,
)
# Call the resource once on load; enough to register it.
demo.load(fn=lambda: nwfwo_html_card(), inputs=None, outputs=widget_preview)
# ---- 4. HTML UI card resource for ChatGPT App ----
@gr.mcp.resource(
"ui://widget/nwfwo-practice-card.html",
mime_type="text/html+skybridge",
)
def nwfwo_html_card():
"""
This HTML will appear as a card inside ChatGPT when this tool runs.
It can read:
- window.openai.toolInput (what ChatGPT sent into the tool)
- window.openai.toolOutput (what our Python tool returned)
"""
html = r"""
<div id="nwfwo-card-root"></div>
<script>
const root = document.getElementById("nwfwo-card-root");
function render() {
const input = (window.openai && window.openai.toolInput) || {};
const output = (window.openai && window.openai.toolOutput) || {};
const foreignSentence = input.transliteration
|| output.foreign_original
|| output.transliteration
|| "Foreign sentence goes here";
const nativeNatural = input.correct_nwfwo
|| output.native_natural
|| "Natural translation goes here";
const nativeLiteral = output.native_literal_from_foreign
|| input.explanation
|| output.explanation
|| "Literal native sentence (foreign flow)";
const userOrder = output.user_tokens && output.user_tokens.length
? output.user_tokens.join(" ")
: (input.user_guess || output.user_guess || "");
const correctTokens = output.correct_tokens || [];
const userTokens = output.user_tokens || [];
const matches = output.matches_by_index || [];
const numCorrect = output.num_correct ?? 0;
const total = output.total_positions ?? correctTokens.length;
const rows = [];
const maxLen = Math.max(correctTokens.length, userTokens.length);
for (let i = 0; i < maxLen; i++) {
const c = correctTokens[i] ?? "";
const u = userTokens[i] ?? "";
const m = matches[i];
rows.push(`
<tr>
<td style="padding:4px 8px; font-size:13px;">${i + 1}</td>
<td style="padding:4px 8px; font-size:13px;">${c}</td>
<td style="padding:4px 8px; font-size:13px;">${u}</td>
<td style="padding:4px 8px; font-size:13px;">
${m === true ? "βœ…" : (u ? "❌" : "")}
</td>
</tr>
`);
}
const summaryText = total > 0
? `${numCorrect} / ${total} positions match the foreign information flow.`
: "No positions to compare yet – check the literal sentence input.";
root.innerHTML = `
<div style="
border-radius:16px;
border:1px solid #ddd;
padding:16px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
max-width: 640px;
">
<div style="font-size:14px;color:#666;margin-bottom:4px;">
Word Order Practice – follow the foreign information flow
</div>
<div style="margin-bottom:12px;">
<div style="font-size:13px;color:#666;">Foreign sentence</div>
<div style="font-size:17px;font-weight:600;">
${foreignSentence}
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:13px;color:#666;">Translation (clue)</div>
<div style="font-size:15px;">
${nativeNatural}
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:13px;color:#666;">Foreign flow in your language (correct literal order)</div>
<div style="padding:8px 12px;border-radius:12px;background:#f5f5f5;font-size:14px;">
${nativeLiteral}
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:13px;color:#666;">Your order</div>
<div style="padding:8px 12px;border-radius:12px;background:#fafafa;font-size:14px;">
${userOrder || "<span style='color:#999;'>No attempt recorded.</span>"}
</div>
</div>
<div style="margin-bottom:8px;font-size:13px;font-weight:600;">
Position-by-position comparison
</div>
<table style="border-collapse:collapse;width:100%;font-size:13px;">
<thead>
<tr>
<th style="text-align:left;padding:4px 8px;">#</th>
<th style="text-align:left;padding:4px 8px;">Correct word</th>
<th style="text-align:left;padding:4px 8px;">Your word</th>
<th style="text-align:left;padding:4px 8px;">Match</th>
</tr>
</thead>
<tbody>
${rows.join("")}
</tbody>
</table>
<div style="margin-top:8px;font-size:13px;">
${summaryText}
</div>
<div style="margin-top:4px;font-size:12px;color:#777;">
Tip: keep the foreign sentence and the translation in mind, and try to place time, place,
and verb in the same order as they appear in the foreign sentence.
</div>
</div>
`;
}
render();
// Re-render when ChatGPT updates tool globals
window.addEventListener("openai:set_globals", (event) => {
if (event?.detail?.globals?.toolOutput) {
render();
}
}, { passive: true });
</script>
"""
return html
# ---- 5. Main entrypoint ----
if __name__ == "__main__":
demo.launch(mcp_server=True)