""" 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"""
""" return html # ---- 5. Main entrypoint ---- if __name__ == "__main__": demo.launch(mcp_server=True)