| import os |
| from typing import List, Dict, Tuple |
| import numpy as np |
| from openai import OpenAI |
| from faq_store import FAQ_ENTRIES, FAQ_VECS |
|
|
| RAG_CONFIDENCE_THRESHOLD = 0.6 |
| MAX_FAQ_MATCHES = 3 |
| _EMBED_MODEL = "text-embedding-3-small" |
| _CHAT_MODEL = "gpt-4o-mini" |
|
|
| SYSTEM_PROMPT = ( |
| "You are a helpful assistant for ScanAssured, a medical document OCR and NER app. " |
| "Answer only based on the provided FAQ context. " |
| "You do NOT have access to any user scan results or personal medical data. " |
| "For personal medical advice, always direct users to a qualified healthcare professional. " |
| "Keep answers concise and clear." |
| ) |
| FALLBACK_MESSAGE = ( |
| "I'm not certain about that. Please consult a qualified healthcare professional " |
| "for personal medical advice, or refer to the app documentation for usage questions." |
| ) |
|
|
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) |
|
|
| _query_cache: dict[str, np.ndarray] = {} |
|
|
|
|
| def cosine(a: np.ndarray, b: np.ndarray) -> float: |
| return float(a.dot(b) / (np.linalg.norm(a) * np.linalg.norm(b))) |
|
|
|
|
| async def get_answer(question: str, history: List[Dict]) -> Tuple[str, List[Dict]]: |
| if question in _query_cache: |
| vec = _query_cache[question] |
| else: |
| resp = client.embeddings.create(model=_EMBED_MODEL, input=question) |
| vec = np.array(resp.data[0].embedding, dtype=np.float32) |
| _query_cache[question] = vec |
|
|
| scores = [(fid, cosine(vec, fvec)) for fid, fvec in FAQ_VECS] |
| scores.sort(key=lambda x: x[1], reverse=True) |
|
|
| if not scores or scores[0][1] < RAG_CONFIDENCE_THRESHOLD: |
| return FALLBACK_MESSAGE, [] |
|
|
| matches = [] |
| for fid, score in scores[:MAX_FAQ_MATCHES]: |
| faq = FAQ_ENTRIES[fid] |
| matches.append({"id": fid, "answer": faq["answer"], "source": faq["source"], "score": score}) |
|
|
| messages: List[Dict] = [{"role": "system", "content": SYSTEM_PROMPT}] |
| for msg in history: |
| messages.append({"role": msg["role"], "content": msg["content"]}) |
| for faq in matches: |
| messages.append({"role": "system", "content": faq["answer"]}) |
| messages.append({"role": "user", "content": question}) |
|
|
| chat_resp = client.chat.completions.create( |
| model=_CHAT_MODEL, |
| messages=messages, |
| stream=False, |
| ) |
| answer = chat_resp.choices[0].message.content |
|
|
| citations = [{"id": faq["id"], "source": faq["source"]} for faq in matches] |
| return answer, citations |
|
|