File size: 8,606 Bytes
d25fb87 5b46cad a4582fe d25fb87 5b46cad d25fb87 5b46cad 4b0a93e a4582fe d25fb87 a4582fe 5b46cad d25fb87 a4582fe d25fb87 5b46cad a4582fe d25fb87 a4582fe d25fb87 4b0a93e cc1a921 d25fb87 5b46cad | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | import os
import json
from typing import List, Tuple, Dict
import gradio as gr
from huggingface_hub import InferenceClient
# Default to GPT-OSS per your preference
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:
# Route provider explicitly when needed
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)
# Important: when using external providers (e.g., Together), do NOT pass HF OAuth/token,
# otherwise the router attempts a delegated call and may 403.
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}"
),
},
]
# Non-streaming summarize for simplicity
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}"}
# Extract content
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:
# Top login section: enable OAuth on Spaces, skip locally
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])
# Send (Enter)
msg.submit(user_submit, [msg, chatbot], [msg, chatbot]).then(
bot_reply, [chatbot], [chatbot]
)
# Send (button)
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()
|