Spaces:
Sleeping
Sleeping
File size: 12,487 Bytes
18af949 | 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 | # -*- coding: utf-8 -*-
"""
Answerwise โ Foreign-Patient Clinic/Beauty AI Consultation & Lead-Conversion SaaS (DEMO)
๋ฐฑ์๋: FastAPI. LLM: Darwin-398B-AX (JGOS-398B @ ornith.1street.ai/v1, no-think).
๋ฐ๋ชจ 5๋ ํ๋ก์ฐ: โ ๋ณ์๋ณ ์ฑ๋ด โก๋ค๊ตญ์ด RAG์๋ด โขํ
๋ํธ ๊ฒฉ๋ฆฌ โฃAI ์์ ๊ฐ๋ โค๋ฆฌ๋+์ด๋๋ฏผ.
"""
import os, re, json, time, datetime, uuid
import requests
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
LLM_URL = os.environ.get("LLM_BACKEND_URL", "https://ornith.1street.ai/v1/chat/completions")
LLM_MODEL = os.environ.get("LLM_MODEL", "Darwin-398B-AX")
app = FastAPI(title="Answerwise Demo")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# โโ ๋ฐ๋ชจ ํ
๋ํธ(์
์ ๋ณ์) + ๋ณ์๋ณ RAG ์ง์(์ฒญํฌ) โโโโโโโโโโโโโโโโโโโโโโโโโ
CLINICS = {
"seoul_grace": {
"name": "Seoul Grace Plastic Surgery", "category": "Plastic Surgery",
"location": "Sinsa-dong, Gangnam, Seoul", "emoji": "๐ฅ", "color": "#4338CA",
"langs": ["en","zh","ja","th"],
"promo": "July: 15% off rhinoplasty consultation package",
"knowledge": [
"Seoul Grace Plastic Surgery specializes in rhinoplasty (nose), facial contouring, and double-eyelid surgery.",
"Rhinoplasty consultation package is 1,200,000 KRW; final surgery price is quoted after an in-person consultation.",
"Double-eyelid surgery starts from 900,000 KRW. Facial contouring is quoted individually.",
"Opening hours: Mon-Fri 10:00-19:00, Sat 10:00-15:00. Closed Sundays and public holidays.",
"Lead surgeon: Dr. Ji-hoon Park, board-certified plastic surgeon, 15 years of experience.",
"July promotion: 15% off the rhinoplasty consultation package for foreign patients.",
"The clinic provides English, Chinese, Japanese and Thai interpretation support on request.",
],
},
"gangnam_glow": {
"name": "Gangnam Glow Dermatology", "category": "Dermatology",
"location": "Apgujeong-ro, Gangnam, Seoul", "emoji": "โจ", "color": "#06B6D4",
"langs": ["en","zh","ja","th"],
"promo": "Skin-lifting laser bundle event this month",
"knowledge": [
"Gangnam Glow Dermatology focuses on skin lifting, laser toning, botox and filler.",
"Skin-lifting (Ulthera 300 shots) is 1,500,000 KRW. Laser toning single session is 150,000 KRW.",
"Botox (forehead or glabella) starts from 99,000 KRW. Filler starts from 350,000 KRW per 1cc.",
"Opening hours: Mon-Sat 11:00-20:00. Closed Sundays.",
"Lead dermatologist: Dr. Soo-min Lee, board-certified dermatologist.",
"This month event: skin-lifting laser bundle discount for first-time foreign visitors.",
],
},
"bright_dental": {
"name": "Apgujeong Bright Dental", "category": "Dental",
"location": "Apgujeong, Gangnam, Seoul", "emoji": "๐ฆท", "color": "#10B981",
"langs": ["en","zh","ja"],
"promo": "Whitening + scaling combo for travelers",
"knowledge": [
"Apgujeong Bright Dental offers teeth whitening, implants and laminate veneers.",
"In-office whitening is 350,000 KRW. Single implant is quoted from 1,300,000 KRW.",
"Opening hours: Mon-Fri 09:30-18:30, Sat 09:30-13:00.",
"Traveler combo: whitening plus scaling in a single visit for foreign visitors.",
],
},
}
# โโ ์ํ๋ ์ฌ์ (๋ค๊ตญ์ด) โ ์๊ธ/์ํ ํํ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RISK_R3 = ["bleeding","heavy bleeding","fever","high fever","difficulty breathing","can't breathe",
"infection","pus","fainting","unconscious","severe pain","์ถํ","๊ณ ์ด","ํธํก๊ณค๋","๊ฐ์ผ","์ค์ ",
"ๅบ่ก","็บ็ฑ","ๅผๅธๅฐ้ฃ","ๆๆ","ใใพใ","ๅบ่กไบ","ๅ็ง","ๅผๅธๅฐ้พ","ๆๆ","เนเธฅเธทเธญเธเธญเธญเธ","เธกเธตเนเธเน"]
RISK_R2 = ["side effect","swelling","won't stop","allergic","medication","re-surgery","revision",
"๋ถ์์ฉ","๋ถ๊ธฐ","์ฝ","์ฌ์์ ","ๅฏไฝ็จ","่
ซใ","่ฌ","ๅฏไฝ็จ","่ฟ","่ฟๆ","เธเธฅเธเนเธฒเธเนเธเธตเธขเธ"]
# โโ ๊ฐ๋๋ ์ผ: ์๋ฃํ๋จ/์ฌ์งํ๋
/์์ฝํ์ /๋ณด์ฅ ์์ฒญ ๊ฐ์ง โโโโโโโโโโโโโโ
def wants_diagnosis(m):
m=m.lower()
return any(k in m for k in ["is it safe for me","can i get","am i a good candidate","diagnose","what's wrong with me",
"์ ์ํ","์ง๋จ","๋ ์์ ๊ฐ๋ฅ","็งใฏๆ่กใงใใพใ","ๆ่ฝๅ","่ฏๆญ","เธงเธดเธเธดเธเธเธฑเธข"])
def wants_photo_read(m):
m=m.lower()
return any(k in m for k in ["look at my photo","check my photo","read my photo","analyze my picture","see my picture",
"์ฌ์ง ๋ด","์ฌ์ง ํ๋
","ๅ็ใ่ฆใฆ","็ๆ็็
ง็","เธเธนเธฃเธนเธเธเธญเธเธเธฑเธ"])
def wants_confirm(m):
m=m.lower()
return any(k in m for k in ["confirm my appointment","book me","confirm the booking","guarantee","์์ฝ ํ์ ","ํ์ ํด์ค",
"ไบ็ดใ็ขบๅฎ","็กฎ่ฎค้ข็บฆ","ไฟ่ฏ"])
def risk_severity(m):
ml=m.lower()
if any(k.lower() in ml for k in RISK_R3): return 3
if any(k.lower() in ml for k in RISK_R2): return 2
return 0
# โโ ํ
๋ํธ ์ค์ฝํ RAG (๊ฒฝ๋ ํค์๋ ๊ฒ์) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def retrieve(clinic, query, k=3):
q=set(re.findall(r"[a-zA-Z๊ฐ-ํฃ]{2,}", query.lower()))
scored=[]
for i,ch in enumerate(clinic["knowledge"]):
words=set(re.findall(r"[a-zA-Z]{2,}", ch.lower()))
score=len(q & words)
if score>0: scored.append((score,i,ch))
scored.sort(reverse=True)
return [(i,ch) for _,i,ch in scored[:k]]
LANG_NAME={"en":"English","zh":"Chinese","ja":"Japanese","th":"Thai","auto":"the user's language"}
def build_system(clinic, lang, chunks):
ctx="\n".join(f"- {c}" for _,c in chunks) if chunks else "(no matching clinic information found)"
return (
f"You are the AI consultation assistant for '{clinic['name']}' ({clinic['category']}), located in {clinic['location']}.\n"
f"Answer ONLY in {LANG_NAME.get(lang,'the user language')}. Be concise, warm and helpful.\n"
"STRICT RULES:\n"
"1. Answer ONLY using the CLINIC INFO below. If the info is not present, say you don't have that detail and offer to have the clinic confirm. NEVER invent prices, results or medical facts.\n"
"2. NEVER diagnose the user's personal condition, judge surgery suitability, or read/interpret photos.\n"
"3. NEVER confirm an appointment, guarantee results, promise no side-effects, or promise discounts. Appointments are confirmed only by the clinic staff.\n"
"4. For appointment requests, collect the user's preferred date and contact, and say the clinic will confirm.\n"
"5. Do not answer questions unrelated to this clinic; briefly redirect.\n\n"
f"CLINIC INFO:\n{ctx}\n\nPROMOTION: {clinic.get('promo','')}"
)
def call_llm(system, user, lang):
try:
r=requests.post(LLM_URL, json={
"model":LLM_MODEL,
"messages":[{"role":"system","content":system},{"role":"user","content":user}],
"max_tokens":320,"temperature":0.3,"stream":False,
"chat_template_kwargs":{"enable_thinking":False},
}, timeout=90)
j=r.json()
return j["choices"][0]["message"]["content"].strip()
except Exception as e:
return "(LLM error: %s)"%e
# โโ ๋ฆฌ๋ ์ ์ฅ(์ธ๋ฉ๋ชจ๋ฆฌ, ํ
๋ํธ๋ณ) + ๊ฐ์ฌ ๋ก๊ทธ โโโโโโโโโโโโโโโโโโโโโโโโโโ
LEADS={} # tenant_id -> list of leads
def grade(lead):
s=0
if lead.get("email") or lead.get("contact"): s=max(s,3)
if lead.get("name"): s=max(s,3)
if lead.get("preferred_date"): s=max(s,2)
if lead.get("photo"): s=max(s,4)
if (lead.get("name") and (lead.get("email") or lead.get("contact")) and lead.get("preferred_date")): s=5
if lead.get("interest") and s<1: s=1
return "L%d"%s
def upsert_lead(tenant, session_id, **fields):
LEADS.setdefault(tenant,[])
cur=next((l for l in LEADS[tenant] if l["session_id"]==session_id), None)
if not cur:
cur={"lead_id":uuid.uuid4().hex[:8],"session_id":session_id,"tenant":tenant,
"created":datetime.datetime.utcnow().strftime("%H:%M UTC"),"status":"new","messages":0}
LEADS[tenant].append(cur)
cur.update({k:v for k,v in fields.items() if v})
cur["grade"]=grade(cur)
return cur
# โโ API โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@app.get("/api/clinics")
def clinics():
return {"clinics":[{"id":k,"name":v["name"],"category":v["category"],"location":v["location"],
"emoji":v["emoji"],"color":v["color"],"langs":v["langs"],"promo":v["promo"]} for k,v in CLINICS.items()]}
@app.post("/api/chat")
async def chat(req: Request):
b=await req.json()
tenant=b.get("tenant"); msg=(b.get("message") or "").strip(); lang=b.get("lang","en")
session=b.get("session") or uuid.uuid4().hex[:10]
if tenant not in CLINICS: return JSONResponse({"error":"unknown clinic"},status_code=404)
clinic=CLINICS[tenant]
upsert_lead(tenant,session,interest=msg[:60])
LEADS[tenant][-1] if False else None
# ๋ฉ์์ง ์ ์ฆ๊ฐ
cur=next(l for l in LEADS[tenant] if l["session_id"]==session); cur["messages"]+=1
# โ ์๊ธ/์ํ๋
sev=risk_severity(msg)
if sev>=3:
cur["status"]="urgent_escalation"; cur["risk"]="R3"
ans=("โ ๏ธ This may be a medical emergency. Please contact your nearest emergency service or the clinic's "
"emergency line immediately. I cannot provide medical judgment. The clinic has been alerted.")
return {"answer":ans,"tenant":tenant,"session":session,"sources":[],"risk":"R3","guardrail":"emergency","grade":cur["grade"]}
# โก ๊ฐ๋๋ ์ผ(์ฌ์งํ๋
/๊ฐ์ธ์ง๋จ/์์ฝํ์ )
gr=None
if wants_photo_read(msg): gr="no_photo_read"
elif wants_diagnosis(msg): gr="no_diagnosis"
elif wants_confirm(msg): gr="no_confirm"
if gr:
cur["status"]="needs_clinic_review"
tmpl={
"no_photo_read":"I can receive your photo for the clinic to review, but I cannot analyze or diagnose photos myself. Our staff will check it. Could you share your name and preferred visit date?",
"no_diagnosis":"I can't assess your personal medical condition โ only the clinic's doctor can do that. I can share general clinic information and pass your inquiry to the staff. What procedure are you interested in?",
"no_confirm":"I can't confirm appointments or guarantee prices/results โ the clinic staff make the final confirmation. I can note your preferred date and hand it to them. When would you like to visit?",
}
return {"answer":tmpl[gr],"tenant":tenant,"session":session,"sources":[],"risk":"R"+str(sev),"guardrail":gr,"grade":cur["grade"]}
# โข ํ
๋ํธ ์ค์ฝํ RAG โ โฃ LLM(no-think)
chunks=retrieve(clinic,msg)
system=build_system(clinic,lang,chunks)
ans=call_llm(system,msg,lang)
if sev==2: cur["status"]="needs_clinic_review"; cur["risk"]="R2"
return {"answer":ans,"tenant":tenant,"session":session,
"sources":[{"idx":i,"text":c} for i,c in chunks],
"risk":"R"+str(sev),"guardrail":None,"grade":cur["grade"]}
@app.post("/api/lead")
async def lead(req: Request):
b=await req.json(); tenant=b.get("tenant"); session=b.get("session")
if tenant not in CLINICS: return JSONResponse({"error":"unknown clinic"},status_code=404)
cur=upsert_lead(tenant,session,name=b.get("name"),email=b.get("email"),contact=b.get("contact"),
preferred_date=b.get("preferred_date"),interest=b.get("interest"),photo=b.get("photo"))
return {"ok":True,"grade":cur["grade"],"lead":cur}
@app.get("/api/admin/leads")
def admin_leads(tenant: str):
return {"tenant":tenant,"clinic":CLINICS.get(tenant,{}).get("name"),"leads":list(reversed(LEADS.get(tenant,[])))}
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def root(): return FileResponse("static/index.html")
|