skyleaping/structured-bloom-api
0
1import json2import os3from fastapi import FastAPI, HTTPException4from fastapi.middleware.cors import CORSMiddleware5from pydantic import BaseModel6from openai import OpenAI7 8app = FastAPI(title="Structured Bloom API")9 10app.add_middleware(11 CORSMiddleware,12 allow_origins=[13 "https://s23.aiweb2026.site",14 "http://localhost:5173",15 "http://127.0.0.1:5173",16 ],17 allow_credentials=True,18 allow_methods=["*"],19 allow_headers=["*"],20)21 22api_key = os.getenv("OPENAI_API_KEY")23client = OpenAI(api_key=api_key) if api_key else None24 25 26class AnalyzeRequest(BaseModel):27 text: str28 available_time: str29 30 31def normalize_time(value: str) -> str:32 value = str(value)33 mapping = {34 "5min": "5분",35 "10min": "10분",36 "20min": "20분",37 "30min": "30분",38 }39 return mapping.get(value, value)40 41 42def fallback_result(text: str, available_time: str):43 time_label = normalize_time(available_time)44 45 return {46 "emotion": ["지침", "정리 필요"],47 "energy": "low",48 "situation": "생각과 감정이 조금 쌓여 있어, 부담이 낮은 회복 행동이 필요한 상태입니다.",49 "template": "soft_reset",50 "flower_theme": "tulip",51 "color_theme": "warm",52 "activity": {53 "title": "책상 위에서 가장 눈에 띄는 물건 3개만 정리하기",54 "time": time_label,55 "burden": "낮음",56 "steps": [57 "타이머를 맞추고 주변을 둘러봅니다.",58 "가장 눈에 띄는 물건 3개만 제자리로 옮깁니다.",59 "마지막에 물 한 모금을 마시고 잠깐 숨을 고릅니다.",60 ],61 "first_action": "지금 손이 닿는 물건 하나를 먼저 치우기",62 },63 "mood_message": "오늘은 크게 바꾸려 하기보다, 작은 정리 하나로 흐름을 다시 만들어도 충분합니다.",64 "drink": "따뜻한 물 또는 연한 차",65 "space": "책상 앞이나 창가처럼 바로 움직일 수 있는 곳",66 "clothes": "몸을 조이지 않는 편한 옷차림",67 "reason": "작은 정리는 생각의 과부하를 줄이고 다음 행동을 시작하기 쉽게 만들어줍니다.",68 }69 70 71@app.get("/")72def health_check():73 return {"message": "Structured Bloom API is running"}74 75 76@app.get("/health")77def health():78 return {79 "status": "ok",80 "has_openai_key": bool(api_key),81 "openai_enabled": bool(client),82 }83 84 85@app.post("/analyze")86def analyze(req: AnalyzeRequest):87 if not req.text.strip():88 raise HTTPException(status_code=400, detail="분석할 문장을 입력해주세요.")89 90 if not client:91 return fallback_result(req.text, req.available_time)92 93 time_label = normalize_time(req.available_time)94 95 prompt = """96너는 사용자의 현재 상태를 분석하고, 작고 실천 가능한 회복 활동을 추천하는 Structured Bloom API야.97 98사용자 입력:99{user_text}100 101사용 가능한 시간:102{time_label}103 104반드시 아래 JSON 형식만 반환해.105설명 문장, 코드블록, ```json 같은 표시는 절대 붙이지 마.106 107{{108 "emotion": ["감정1", "감정2"],109 "energy": "low",110 "situation": "현재 상태를 한 문장으로 요약",111 "template": "soft_reset",112 "flower_theme": "tulip",113 "color_theme": "warm",114 "activity": {{115 "title": "추천 활동 제목",116 "time": "{time_label}",117 "burden": "낮음",118 "steps": ["1단계", "2단계", "3단계"],119 "first_action": "가장 먼저 할 행동"120 }},121 "mood_message": "사용자에게 건네는 짧고 부드러운 메시지",122 "drink": "추천 음료",123 "space": "추천 공간",124 "clothes": "추천 옷차림",125 "reason": "왜 이 활동이 지금 상태에 맞는지 설명"126}}127 128선택 가능한 값:129- energy: low, medium, high 중 하나130- template: soft_reset, sensory_shift, gentle_focus, tiny_action 중 하나131- flower_theme: tulip, lavender, daisy, camellia 중 하나132- color_theme: warm, calm, fresh, neutral 중 하나133""".format(134 user_text=req.text,135 time_label=time_label,136 )137 138 try:139 response = client.responses.create(140 model="gpt-4.1-mini",141 input=prompt,142 )143 144 raw_text = response.output_text.strip()145 146 if raw_text.startswith("```"):147 raw_text = raw_text.replace("```json", "").replace("```", "").strip()148 149 data = json.loads(raw_text)150 151 required_keys = [152 "emotion",153 "energy",154 "situation",155 "template",156 "flower_theme",157 "color_theme",158 "activity",159 "mood_message",160 "drink",161 "space",162 "clothes",163 "reason",164 ]165 166 for key in required_keys:167 if key not in data:168 raise ValueError(f"missing key: {key}")169 170 activity_keys = ["title", "time", "burden", "steps", "first_action"]171 for key in activity_keys:172 if key not in data["activity"]:173 raise ValueError(f"missing activity key: {key}")174 175 return data176 177 except Exception:178 return fallback_result(req.text, req.available_time)179 180 181@app.post("/recommend")182def recommend(req: AnalyzeRequest):183 result = analyze(req)184 return {185 "recommendation": f"추천 활동: {result['activity']['title']}\n이유: {result['reason']}",186 "source": "openai_or_fallback",187 }