Spaces:
Sleeping
Sleeping
| import os, json, hashlib, ast | |
| import gradio as gr | |
| from openai import OpenAI | |
| from datetime import datetime | |
| # --- Configuration & Prompts (From Colleague's Script) --- | |
| client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) | |
| MODEL = "gpt-5.2" | |
| course_username = os.environ["COURSE_USERNAME"] | |
| password_to_name_dict = ast.literal_eval(os.environ["COURSE_PASSWORDS"]) | |
| SECTION_SEQUENCE = ["0","1.1","1.2","1.3","1.4","1.5","2.1","2.2","3.1","3.2","3.3","3.4","3.5","4.1"] | |
| SECTIONS = { | |
| "0": { | |
| "title": "Summary", | |
| "student_prompt": "In one succinct paragraph, describe your idea: what it is, what problem it solves, what is innovative vs current approaches, and what impact it could have if fully realized.", | |
| "must_include": ["What the idea is", "What problem it solves", "Why it's innovative vs state of the art", "Impact if successful"], | |
| }, | |
| "1.1": { | |
| "title": "Defining the problem: what problem does this solve?", | |
| "student_prompt": "Define the problem your idea is trying to solve. Include background/context and why it matters.", | |
| "must_include": ["Context", "What fails today", "Why it matters", "Scope boundaries"], | |
| }, | |
| "1.2": { | |
| "title": "Why is this still not solved?", | |
| "student_prompt": "Explain why this problem is still not solved. Identify the key gaps or constraints in existing solutions (technical/logistical/economic/etc.).", | |
| "must_include": ["Existing approaches (brief)", "At least 2 reasons it's unsolved", "Avoid purely cost-only reasoning"], | |
| }, | |
| "1.3": { | |
| "title": "Bottleneck analysis", | |
| "student_prompt": "Describe the chain of events needed to solve the problem, then identify the main bottleneck your idea addresses and why it is the priority bottleneck.", | |
| "must_include": ["Chain of events", "Bottleneck step", "Why it blocks the chain", "Which step your idea targets"], | |
| }, | |
| "1.4": { | |
| "title": "Bar for success (metrics)", | |
| "student_prompt": "Define the quantitative bar for success for overcoming the bottleneck (key metrics and threshold values). Include technical constraints (and socio-economic if relevant).", | |
| "must_include": ["2–5 metrics", "Numeric thresholds", "Justification for thresholds", "At least one technical metric"], | |
| }, | |
| "1.5": { | |
| "title": "Feedback on problem framing", | |
| "student_prompt": "List people/roles who could give you the best feedback on the problem and current bottlenecks, and what you’d ask each of them.", | |
| "must_include": ["5–12 people/roles", "Question for each", "Mix of perspectives"], | |
| }, | |
| "2.1": { | |
| "title": "What is your idea (how it works)?", | |
| "student_prompt": "Explain your idea in detail, focusing on how it works step-by-step. Include inputs/outputs and any diagrams you’d make.", | |
| "must_include": ["Mechanism", "Inputs/outputs", "Workflow fit", "Sufficient detail to critique/build"], | |
| }, | |
| "2.2": { | |
| "title": "Novelty and closest related ideas", | |
| "student_prompt": "How new is the idea? Identify the closest related ideas and explain similarities/differences and the novelty type (combination/extension/new application/etc.).", | |
| "must_include": ["2–5 related ideas", "Similarities", "Differences", "Novelty claim"], | |
| }, | |
| "3.1": { | |
| "title": "Feasibility: dependencies in the chain", | |
| "student_prompt": "Where does your idea exist in the chain of events, and what dependencies does it have (upstream/downstream)? Does it add new burdens or links?", | |
| "must_include": ["Placement in chain", "Dependencies", "New constraints introduced"], | |
| }, | |
| "3.2": { | |
| "title": "Feasibility: meets bars and overcomes bottleneck?", | |
| "student_prompt": "Do a quick sanity check: does your idea overcome the bottleneck and meet the quantitative bars? Use napkin-level logic or calculations.", | |
| "must_include": ["Tie to each bar", "Evidence/logic", "Conclusion + uncertainties"], | |
| }, | |
| "3.3": { | |
| "title": "Contrarian analysis", | |
| "student_prompt": "Explain why the idea might not work. List and prioritize major assumptions/risks/bottlenecks, and briefly note mitigations.", | |
| "must_include": ["5–10 risks/assumptions", "Prioritization", "Failure mechanism", "Mitigation ideas (brief)"], | |
| }, | |
| "3.4": { | |
| "title": "Go/no-go experiments", | |
| "student_prompt": "Design key go/no-go experiments to test the most important assumptions. Define what results would count as go vs no-go.", | |
| "must_include": ["2–5 experiments", "Go criteria", "No-go criteria", "Simple decisive design"], | |
| }, | |
| "3.5": { | |
| "title": "Technical feedback people", | |
| "student_prompt": "List people/roles who could give you the best technical feedback, and what you’d ask them.", | |
| "must_include": ["5–12 people/roles", "Question for each", "Relevant technical coverage"], | |
| }, | |
| "4.1": { | |
| "title": "Impact", | |
| "student_prompt": "If you are 100% successful, what would be the impact and who would be impacted? Include scale, time horizon, and broader implications.", | |
| "must_include": ["Stakeholders", "Nature of impact", "Scale/time horizon", "Broader implications"], | |
| }, | |
| } | |
| # Add remaining section definitions from your colleague's code here... | |
| SYSTEM_MESSAGE = """You are an educational writing and idea-framing coach for undergraduate and early graduate students. | |
| You must help the student complete ONE section of a framing template at a time. You must follow the developer instructions. | |
| Critical output requirement: | |
| - You must respond with ONLY valid JSON that conforms exactly to the provided JSON Schema. | |
| - Do not include any extra keys, commentary, markdown, or non-JSON text. | |
| Behavioral requirements: | |
| - Be constructive, specific, and pedagogically supportive. | |
| - Maintain student ownership: help them clarify and improve, but do not fabricate facts, citations, data, or results. | |
| - If the user requests disallowed assistance (e.g., instructions for wrongdoing, dangerous biological/chemical steps, or other harmful content), refuse and redirect to safe, high-level guidance consistent with an educational framing context. | |
| """ # Full message from colleague | |
| DEVELOPER_MESSAGE = """# Role | |
| You are “Framing Coach,” a structured, rubric-driven assistant that guides a student through an idea-framing template one section at a time. You coach the student to produce clear, specific, and (when required) quantitative responses. | |
| # Primary objective | |
| For the CURRENT section only: | |
| 1) Evaluate the student's response against the section’s rubric (“must include” items + section-specific quality bars). | |
| 2) Provide targeted feedback and 1–3 follow-up questions. | |
| 3) Optionally propose an improved version that preserves the student’s intent and voice. | |
| 4) Decide whether the section is acceptable to advance. | |
| # Scope and boundaries | |
| - Focus strictly on the current section. Do not jump ahead to later sections unless the student explicitly asks and the current section is already accepted. | |
| - Maintain student ownership: do not invent details the student did not provide (numbers, results, citations, experimental outcomes, claims of novelty, named experts, etc.). | |
| - You may suggest example metrics, placeholder variables, or plausible ranges ONLY when clearly labeled as “assumptions/placeholders” and framed as options for the student to confirm or revise. | |
| - Do not provide step-by-step instructions for wrongdoing or unsafe activity. If the student’s idea involves hazardous, illegal, or harmful actions, refuse and pivot to safe, high-level framing (problem definition, ethics, risk analysis) without operational details. | |
| # Input contract (what the application provides) | |
| You will receive, in the user message, a JSON object with: | |
| - section_id: one of ["0","1.1","1.2","1.3","1.4","1.5","2.1","2.2","3.1","3.2","3.3","3.4","3.5","4.1"] | |
| - section_title: string | |
| - student_prompt: string (the main question to ask for this section) | |
| - must_include: array of strings (rubric checklist items) | |
| - student_message: string (the student’s latest attempt for this section; may be empty) | |
| - draft_so_far (optional): object containing prior accepted sections; use only for context and consistency | |
| If the user message is not valid JSON or lacks section_id/section_title/student_prompt/must_include/student_message: | |
| - Set status="needs_input" | |
| - In follow_up_questions[0], ask for the missing information in the simplest way. | |
| - Do not guess the section_id. | |
| # Section order (for next_section_id) | |
| Use this fixed sequence: | |
| ["0","1.1","1.2","1.3","1.4","1.5","2.1","2.2","3.1","3.2","3.3","3.4","3.5","4.1"] | |
| When a section is accepted, next_section_id is the next item in the sequence. If the current section is "4.1", next_section_id must be null. | |
| # Output contract (MUST match the JSON Schema exactly) | |
| Return a single JSON object with these required fields: | |
| - section_id | |
| - section_title | |
| - status: "needs_input" | "needs_revision" | "accepted" | |
| - feedback_bullets: array of strings | |
| - missing_or_unclear: array of strings | |
| - improved_version: string or null | |
| - follow_up_questions: array of 1–3 strings | |
| - advance: boolean | |
| - next_section_id: string or null | |
| - draft_update: object with | |
| - student_answer: string | |
| - accepted_version: string or null | |
| - coach_notes: string or null | |
| # How to set status and advance | |
| 1) status="needs_input" | |
| Use when the student_message is empty, non-responsive, or only meta (e.g., “I don’t know,” “help me,” or off-topic). | |
| - advance=false | |
| - next_section_id=null | |
| - improved_version=null | |
| 2) status="needs_revision" | |
| Use when the student_message attempts the section but misses key rubric items or is unclear. | |
| - advance=false | |
| - next_section_id=null | |
| - improved_version should usually be provided (unless the student_message is too thin; then keep improved_version null and focus on questions). | |
| 3) status="accepted" | |
| Use when the response satisfies must_include and is sufficiently clear for downstream sections. | |
| - advance=true | |
| - next_section_id must follow the fixed sequence (or null at the end) | |
| - accepted_version must be a clean, student-faithful version of the section | |
| # Rubric interpretation (general) | |
| - must_include items are the minimum checklist; the student doesn’t need perfection, but they must address each item meaningfully. | |
| - Prefer clarity over length; avoid jargon unless the student uses it correctly. | |
| - Encourage specificity and testability. | |
| - Be appropriately skeptical: flag hand-wavy claims and ask for grounding. | |
| # Coaching style constraints | |
| - Be direct, kind, and concrete. | |
| - Provide feedback as actionable bullets (typically 3–6). | |
| - Ask at most 3 follow-up questions; each should be targeted and non-overlapping. | |
| - Avoid writing an entire proposal or adding substantial new content the student did not supply. | |
| # Handling uncertainty and missing data | |
| - If the student lacks numbers/metrics, you may suggest candidate metrics, placeholder variables, or plausible ranges ONLY if clearly labeled as assumptions/options to verify. | |
| # Consistency with draft_so_far | |
| If draft_so_far is provided: | |
| - Maintain consistency in terminology. | |
| - If you detect contradictions, flag them and ask a follow-up question. | |
| # Safety and integrity | |
| - If asked for harmful operational guidance, refuse briefly and redirect to safe alternatives. | |
| - If the student asks you to “write it for me,” comply by coaching and offering outlines/edits, but do not generate a fully original submission without student input. | |
| # coach_notes | |
| Keep short. No chain-of-thought. No sensitive personal data. | |
| """ | |
| FRAMING_COACH_JSON_SCHEMA = { | |
| "type": "object", | |
| "additionalProperties": False, | |
| "properties": { | |
| "section_id": {"type": "string", "enum": SECTION_SEQUENCE}, | |
| "section_title": {"type": "string"}, | |
| "status": {"type": "string", "enum": ["needs_input", "needs_revision", "accepted"]}, | |
| "feedback_bullets": {"type": "array", "items": {"type": "string"}}, | |
| "missing_or_unclear": {"type": "array", "items": {"type": "string"}}, | |
| "improved_version": {"type": ["string", "null"]}, | |
| "follow_up_questions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string"}}, | |
| "advance": {"type": "boolean"}, | |
| "next_section_id": {"type": ["string", "null"], "enum": SECTION_SEQUENCE + [None]}, | |
| "draft_update": { | |
| "type": "object", | |
| "additionalProperties": False, | |
| "properties": { | |
| "student_answer": {"type": "string"}, | |
| "accepted_version": {"type": ["string", "null"]}, | |
| "coach_notes": {"type": ["string", "null"]}, | |
| }, | |
| "required": ["student_answer", "accepted_version", "coach_notes"], | |
| }, | |
| }, | |
| "required": ["section_id", "section_title", "status", "feedback_bullets", "missing_or_unclear", "improved_version", "follow_up_questions", "advance", "next_section_id", "draft_update"], | |
| } | |
| # --- Logic Wrapper --- | |
| class FramingCoachLogic: | |
| def __init__(self): | |
| self.model = MODEL | |
| def format_as_markdown(self, result): | |
| """Converts the JSON response into a beautiful Markdown string for the chat.""" | |
| md = f"### Section {result['section_id']}: {result['section_title']}\n" | |
| md += f"**Status:** `{result['status'].upper()}`\n\n" | |
| if result["feedback_bullets"]: | |
| md += "#### 📝 Feedback\n" | |
| for b in result["feedback_bullets"]: | |
| md += f"* {b}\n" | |
| if result["missing_or_unclear"]: | |
| md += "\n#### 🔍 Missing or Unclear\n" | |
| for m in result["missing_or_unclear"]: | |
| md += f"* {m}\n" | |
| if result["improved_version"]: | |
| md += f"\n#### ✨ Suggested Draft\n> {result['improved_version']}\n" | |
| md += "\n---\n#### ❓ Next Steps\n" | |
| for q in result["follow_up_questions"]: | |
| md += f"* {q}\n" | |
| if result["advance"] and result["next_section_id"]: | |
| next_title = SECTIONS[result["next_section_id"]]["title"] | |
| md += f"\n✅ **Moving to Section {result['next_section_id']}: {next_title}**" | |
| elif result["advance"] and not result["next_section_id"]: | |
| md += "\n🎉 **Template Complete!**" | |
| return md | |
| def call_llm(self, section_id, student_message, accepted_sections, safety_id): | |
| spec = SECTIONS[section_id] | |
| payload = { | |
| "section_id": section_id, | |
| "section_title": spec["title"], | |
| "student_prompt": spec["student_prompt"], | |
| "must_include": spec["must_include"], | |
| "student_message": student_message, | |
| "draft_so_far": {"accepted_sections": accepted_sections}, | |
| } | |
| resp = client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_MESSAGE}, | |
| {"role": "developer", "content": DEVELOPER_MESSAGE}, | |
| {"role": "user", "content": json.dumps(payload)}, | |
| ], | |
| response_format={"type": "json_schema", "json_schema": {"name": "coach_reply", "schema": FRAMING_COACH_JSON_SCHEMA, "strict": True}}, | |
| temperature=0.3, | |
| ) | |
| return json.loads(resp.choices[0].message.content) | |
| coach_logic = FramingCoachLogic() | |
| # --- Gradio App --- | |
| def respond(message, history, state): | |
| # Initialize state if it's the first message | |
| if state is None: | |
| state = {"current_section_id": "0", "accepted_sections": {}, "user_id": "default_user"} | |
| safety_id = hashlib.sha256(state["user_id"].encode()).hexdigest() | |
| # Call the LLM | |
| result = coach_logic.call_llm( | |
| state["current_section_id"], | |
| message, | |
| state["accepted_sections"], | |
| safety_id | |
| ) | |
| # Update State if accepted | |
| if result["status"] == "accepted": | |
| state["accepted_sections"][state["current_section_id"]] = ( | |
| result["draft_update"]["accepted_version"] or result["improved_version"] or message | |
| ) | |
| # Advance section if applicable | |
| if result["advance"] and result["next_section_id"]: | |
| state["current_section_id"] = result["next_section_id"] | |
| # Format response for UI | |
| final_md = coach_logic.format_as_markdown(result) | |
| return final_md, state | |
| with gr.Blocks(fill_height=True) as demo: | |
| # Persistent session state | |
| session_state = gr.State(None) | |
| with gr.Row(): | |
| user_input = gr.Textbox(label="Username", placeholder="Enter to start...") | |
| pass_input = gr.Textbox(label="Password", type="password") | |
| login_btn = gr.Button("Login") | |
| chat_container = gr.Column(visible=False) | |
| with chat_container: | |
| # We use a standard chatbot with a custom function to handle the state | |
| chatbot = gr.Chatbot(render_markdown=True, scale=1) | |
| msg_input = gr.Textbox(placeholder="Type your response here and press Enter...") | |
| def user_msg(user_message, history): | |
| # Append user message as a dictionary | |
| if history is None: | |
| history = [] | |
| history.append({"role": "user", "content": user_message}) | |
| return "", history | |
| def bot_msg(history, state): | |
| # The last message in history is the user's prompt | |
| user_message = history[-1]["content"] | |
| # Call your LLM logic | |
| bot_markdown, updated_state = respond(user_message, history, state) | |
| # Append the assistant's response as a dictionary | |
| history.append({"role": "assistant", "content": bot_markdown}) | |
| return history, updated_state | |
| msg_input.submit(user_msg, [msg_input, chatbot], [msg_input, chatbot]).then( | |
| bot_msg, [chatbot, session_state], [chatbot, session_state] | |
| ) | |
| def login(u, p): | |
| if (u == course_username) and (p in password_to_name_dict.keys()): | |
| return gr.update(visible=True) | |
| login_btn.click(login, [user_input, pass_input], chat_container) | |
| demo.queue(default_concurrency_limit=4) | |
| demo.launch(show_error=True) |