| import os |
| import json |
| from typing import List, Tuple, Dict |
|
|
| import gradio as gr |
| from huggingface_hub import InferenceClient |
|
|
|
|
| |
| HF_MODEL = os.getenv("HF_MODEL", "openai/gpt-oss-20b") |
|
|
|
|
| SYSTEM_PROMPT = ( |
| "You are Maya, the owner of Klinik Sehat Sentosa, a small outpatient clinic in Manado. " |
| "A student systems analyst is interviewing you to gather information requirements for a simple " |
| "appointment & queueing system (web + mobile).\n\n" |
| "Goals: reduce patient wait time, prevent double bookings, support WhatsApp reminders, basic daily reports.\n" |
| "Persona: friendly, busy, non-technical. Answer concretely based on realistic daily operations at a small clinic. " |
| "If the student asks vague questions, ask specific clarifying questions before answering.\n" |
| "Scope boundaries: No billing, no insurance, no EMR details—focus only on scheduling, queue order, reminders, and daily counts.\n" |
| "Constraints: staff have low digital literacy; internet is intermittent; must run on existing Android phones; budget is small.\n" |
| "Progress strategy: do not dump everything. Reveal details only when asked well. If the student asks leading questions, gently correct with realistic constraints.\n\n" |
| "Style: Speak as Maya in first person. Be concise and concrete. Avoid technical jargon; describe operations in everyday terms." |
| ) |
|
|
|
|
| def _build_messages(history: List[Tuple[str, str]], latest_user: str) -> List[Dict[str, str]]: |
| messages: List[Dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}] |
| for user, assistant in history: |
| if user: |
| messages.append({"role": "user", "content": user}) |
| if assistant: |
| messages.append({"role": "assistant", "content": assistant}) |
| if latest_user: |
| messages.append({"role": "user", "content": latest_user}) |
| return messages |
|
|
|
|
| SUMMARY_SYSTEM_PROMPT = ( |
| "You are a requirements summarizer assisting the student and Maya. " |
| "Based ONLY on the conversation transcript, produce a single JSON object that captures the current understanding of requirements. " |
| "Do not invent details that were not stated or clearly implied by Maya. If unknown, use empty arrays or nulls.\n\n" |
| "Output strictly valid JSON (no markdown, no extra text).\n\n" |
| "Schema keys: \n" |
| "- actors: string[]\n" |
| "- goals: string[]\n" |
| "- constraints: string[]\n" |
| "- functional_requirements: string[]\n" |
| "- non_functional_requirements: string[]\n" |
| "- user_stories: string[] (format: 'As a <actor>, I want <need>, so that <benefit>.')\n" |
| "- edge_cases: string[]\n" |
| "- assumptions: string[]\n" |
| "- open_questions: string[]\n" |
| "- acceptance_criteria: string[]\n" |
| ) |
|
|
|
|
| def _history_to_transcript(history: List[Tuple[str, str]]) -> str: |
| lines = [] |
| for user, assistant in history: |
| if user: |
| lines.append(f"Student: {user}") |
| if assistant: |
| lines.append(f"Maya: {assistant}") |
| return "\n".join(lines) |
|
|
|
|
| def user_submit(user_message: str, history: List[Tuple[str, str]]): |
| history = history + [(user_message, "")] |
| return "", history |
|
|
|
|
| def _oauth_or_env_token(hf_token: gr.OAuthToken | None): |
| try: |
| if hf_token and getattr(hf_token, "token", None): |
| return hf_token.token |
| except Exception: |
| pass |
| return os.getenv("HF_TOKEN") |
|
|
|
|
| def _provider_for_model(model_id: str) -> str | None: |
| |
| if model_id.startswith("openai/"): |
| return "together" |
| return None |
|
|
|
|
| def _provider_api_key(model_id: str) -> str | None: |
| prov = _provider_for_model(model_id) |
| if prov == "together": |
| return os.getenv("TOGETHER_API_KEY") |
| return None |
|
|
|
|
| def _inference_params(hf_token: gr.OAuthToken | None, model_id: str): |
| provider = _provider_for_model(model_id) |
| api_key = _provider_api_key(model_id) |
| |
| |
| if provider: |
| token = None |
| else: |
| token = _oauth_or_env_token(hf_token) |
| return token, provider, api_key |
|
|
|
|
| def bot_reply(history: List[Tuple[str, str]], temperature: float = 0.7, top_p: float = 0.95, max_tokens: int = 512, hf_token: gr.OAuthToken = None): |
| last_user = history[-1][0] if history else "" |
| messages = _build_messages(history[:-1], last_user) |
| token, provider, api_key = _inference_params(hf_token, HF_MODEL) |
| client = InferenceClient(token=token, model=HF_MODEL) |
|
|
| acc = "" |
| try: |
| for event in client.chat_completion( |
| messages, |
| max_tokens=max_tokens, |
| stream=True, |
| temperature=temperature, |
| top_p=top_p, |
| provider=provider, |
| api_key=api_key, |
| ): |
| token = "" |
| try: |
| choices = event.choices |
| if len(choices) and getattr(choices[0], "delta", None) and choices[0].delta.content: |
| token = choices[0].delta.content |
| except Exception: |
| pass |
| acc += token |
| history[-1] = (history[-1][0], acc) |
| yield history |
| except Exception as e: |
| err = ( |
| f"[Model error] {type(e).__name__}: {e}.\n" |
| "Tip: Click Sign in or set HF_TOKEN.\n" |
| "You can also set HF_MODEL to an HF-hosted chat model, e.g. 'meta-llama/Meta-Llama-3-8B-Instruct' or 'HuggingFaceH4/zephyr-7b-beta'." |
| ) |
| history[-1] = (history[-1][0], err) |
| yield history |
|
|
|
|
| def summarize(history: List[Tuple[str, str]], hf_token: gr.OAuthToken = None): |
| transcript = _history_to_transcript(history) |
| if not transcript.strip(): |
| return {"note": "No conversation yet. Ask Maya some questions first."} |
|
|
| token, provider, api_key = _inference_params(hf_token, HF_MODEL) |
| client = InferenceClient(token=token, model=HF_MODEL) |
| messages = [ |
| {"role": "system", "content": SUMMARY_SYSTEM_PROMPT}, |
| { |
| "role": "user", |
| "content": ( |
| "Using the transcript below, generate the JSON. Remember: valid JSON only.\n\n" |
| f"Transcript:\n{transcript}" |
| ), |
| }, |
| ] |
| |
| try: |
| event = client.chat_completion( |
| messages, |
| max_tokens=1024, |
| stream=False, |
| temperature=0.2, |
| top_p=0.95, |
| provider=provider, |
| api_key=api_key, |
| ) |
| except Exception as e: |
| return {"error": f"{type(e).__name__}: {e}"} |
| |
| text = "{}" |
| try: |
| if event.choices and event.choices[0].message and event.choices[0].message.content: |
| text = event.choices[0].message.content |
| except Exception: |
| pass |
| try: |
| return json.loads(text) |
| except Exception: |
| return {"raw": text} |
|
|
|
|
| with gr.Blocks(title="Maya – Klinik Owner (Role-Play)") as demo: |
| |
| is_space = bool(os.getenv("SPACE_ID") or os.getenv("SYSTEM") == "spaces") |
| if is_space: |
| gr.Markdown("## Sign in to use the model") |
| gr.LoginButton() |
| else: |
| gr.Markdown("Tip: set env var `HF_TOKEN` for local use.") |
| gr.Markdown("Model: `" + HF_MODEL + "`") |
|
|
| gr.Markdown( |
| """ |
| # Maya – Klinik Sehat Sentosa (Role-Play) |
| Interview Maya to elicit requirements for an appointment & queueing system. |
| - Ask clear, specific questions. Maya will ask for clarification if vague. |
| - Click "Generate Requirements JSON" anytime to summarize current findings. |
| """ |
| ) |
|
|
| with gr.Row(): |
| chatbot = gr.Chatbot(label="Maya (Clinic Owner)", height=420) |
| json_out = gr.JSON(label="Requirements JSON", value=None) |
|
|
| msg = gr.Textbox(placeholder="Ask Maya your next question…", label="Your message") |
|
|
| with gr.Row(): |
| send_btn = gr.Button("Send", variant="primary") |
| summarize_btn = gr.Button("Generate Requirements JSON") |
| clear_btn = gr.ClearButton([chatbot, msg, json_out]) |
|
|
| |
| msg.submit(user_submit, [msg, chatbot], [msg, chatbot]).then( |
| bot_reply, [chatbot], [chatbot] |
| ) |
| |
| send_btn.click(user_submit, [msg, chatbot], [msg, chatbot]).then( |
| bot_reply, [chatbot], [chatbot] |
| ) |
|
|
| summarize_btn.click(summarize, [chatbot], [json_out]) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|