thoughtful_test / app.py
csmith715's picture
Initial commit
e223a00
import re
from dataclasses import dataclass
from typing import List, Tuple
from data import RAW_DATA, GENERIC_HELP, EXAMPLES
import gradio as gr
# ======= Retrieval utilities (dependency-free) =======
@dataclass
class QA:
question: str
answer: str
keywords: List[str]
def normalize(text: str) -> str:
text = text.lower()
text = re.sub(r"[^a-z0-9\s\(\)\-\'&/]", " ", text)
text = re.sub(r"\s+", " ", text).strip()
return text
# A simplified tokenizer to reduce latency
def tokenize(text: str) -> List[str]:
return normalize(text).split()
# Other similarity measures could be used, but jaccard is simple enough and it works
def jaccard(a: List[str], b: List[str]) -> float:
sa, sb = set(a), set(b)
if not sa and not sb:
return 0.0
return len(sa & sb) / len(sa | sb)
def seq_ratio(a: str, b: str) -> float:
# lightweight character-overlap ratio (no external dependencies)
sa, sb = set(a), set(b)
if not sa and not sb:
return 0.0
return len(sa & sb) / max(len(sa), len(sb))
def contains_any(text: str, needles: List[str]) -> int:
t = normalize(text)
return sum(1 for n in needles if n in t)
def build_qa_bank(raw) -> List[QA]:
bank = []
for item in raw["questions"]:
q = item["question"]
a = item["answer"]
kws = []
lq = q.lower()
if "eva" in lq:
kws += ["eva", "eligibility", "benefits", "verification"]
if "cam" in lq:
kws += ["cam", "claims", "processing", "reimbursement"]
if "phil" in lq:
kws += ["phil", "payment", "posting", "reconciliation"]
if "agents" in lq or "thoughtful ai" in lq:
kws += ["agents", "thoughtful ai", "suite", "automation", "healthcare"]
bank.append(QA(q, a, kws))
return bank
QA_BANK = build_qa_bank(RAW_DATA)
def score_query(user_msg: str, qa: QA) -> float:
"""Return a confidence score for how well `qa` answers `user_msg`."""
u_norm = normalize(user_msg)
q_tokens = tokenize(qa.question + " " + qa.answer)
u_tokens = tokenize(u_norm)
s_jaccard = jaccard(u_tokens, q_tokens) # word overlap
s_seq_q = seq_ratio(u_norm, normalize(qa.question)) # char overlap vs question
s_seq_a = seq_ratio(u_norm, normalize(qa.answer)) # char overlap vs answer
s_kw = 0.06 * contains_any(u_norm, qa.keywords) # keyword hints
s_agent_hint = 0.03 if "agent" in u_norm else 0.0
score = (0.5 * s_jaccard) + (0.25 * s_seq_q) + (0.15 * s_seq_a) + s_kw + s_agent_hint
return min(score, 1.5)
def retrieve_best_answer(user_msg: str) -> Tuple[str, str, float]:
best = None
best_score = -1.0
for qa in QA_BANK:
s = score_query(user_msg, qa)
if s > best_score:
best, best_score = qa, s
return best.question, best.answer, best_score
# ======= Chat logic =======
def chat_step(user_msg: str, history: List[Tuple[str, str]], show_conf: bool):
"""
Stateless step function for the UI.
Returns updated history and an empty textbox string.
"""
try:
user_msg = (user_msg or "").strip()
if not user_msg:
# gentle nudge without crashing the flow
bot_reply = "Please enter a question about Thoughtful AI’s agents (EVA, CAM, PHIL)."
return history + [(user_msg, bot_reply)], ""
matched_q, answer, score = retrieve_best_answer(user_msg)
# Arbitrarily setting the matching score to 0.18
if score < 0.18:
bot_reply = (
f"Here’s a quick overview:\n\n{GENERIC_HELP}\n\n"
f"_Tip: mention an agent name like EVA, CAM, or PHIL for a precise answer._"
)
else:
bot_reply = f"**Answer:** {answer}"
if show_conf:
bot_reply += (
f"\n\n_Matched topic:_ “{matched_q}” \n"
f"_Confidence:_ {score:.2f}"
)
return history + [(user_msg, bot_reply)], ""
except Exception as e:
# UI Robustness
bot_reply = (
"Sorry — I ran into an unexpected error while processing that. "
"Please try again or rephrase your question."
)
# In a real setting, I would log `e` to a file/monitoring system.
print(e)
return history + [(user_msg or "", bot_reply)], ""
# ======= UI =======
CSS = """
#app-title {font-size: 28px; font-weight: 700; margin-bottom: 2px;}
#app-sub {opacity: 0.8; margin-bottom: 16px;}
"""
with gr.Blocks(css=CSS, theme=gr.themes.Soft()) as demo:
gr.Markdown(
"<div id='app-title'>Thoughtful AI – Support Assistant</div>"
"<div id='app-sub'>Ask about EVA, CAM, PHIL, or general benefits.</div>"
)
with gr.Row():
show_conf = gr.Checkbox(label="Show match & confidence", value=True)
chatbot = gr.Chatbot(type='tuples', height=380)
with gr.Row():
inp = gr.Textbox(placeholder="Ask a question about Thoughtful AI…", lines=2)
with gr.Row():
submit = gr.Button("Ask", variant="primary")
clear = gr.Button("Clear Chat")
gr.Examples(examples=EXAMPLES, inputs=inp, label="Try these")
state = gr.State([]) # chat history
def on_submit(user_msg, history, conf):
new_history, cleared = chat_step(user_msg, history, conf)
return new_history, cleared
submit.click(on_submit, inputs=[inp, state, show_conf], outputs=[chatbot, inp])
inp.submit(on_submit, inputs=[inp, state, show_conf], outputs=[chatbot, inp])
def on_clear():
return [], ""
clear.click(on_clear, outputs=[chatbot, inp])
# keep state in sync with what's shown
def sync_state(chat_history):
return chat_history
chatbot.change(sync_state, inputs=[chatbot], outputs=[state])
if __name__ == "__main__":
demo.launch()