Spaces:
Sleeping
Sleeping
GitHub Actions commited on
Commit ·
280a261
1
Parent(s): 09849b4
Deploy Emily Pantheon backend from GitHub Actions
Browse files- Dockerfile +8 -19
- README.md +7 -4
- app.py +144 -0
- config.py +68 -0
- personas.py +106 -0
- requirements.txt +4 -4
Dockerfile
CHANGED
|
@@ -1,24 +1,13 @@
|
|
| 1 |
-
|
| 2 |
-
FROM python:3.9
|
| 3 |
|
| 4 |
-
|
| 5 |
-
WORKDIR /code
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 10 |
|
| 11 |
-
|
| 12 |
-
COPY . /code
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
USER user
|
| 17 |
-
ENV HOME=/home/user \
|
| 18 |
-
PATH=/home/user/.local/bin:$PATH
|
| 19 |
|
| 20 |
-
|
| 21 |
-
COPY --chown=user . $HOME/app
|
| 22 |
-
|
| 23 |
-
# 6. 서버 실행 (7860 포트로 uvicorn 실행)
|
| 24 |
-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
|
|
|
| 2 |
|
| 3 |
+
WORKDIR /app
|
|
|
|
| 4 |
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
| 7 |
|
| 8 |
+
COPY . .
|
|
|
|
| 9 |
|
| 10 |
+
ENV PORT=7860
|
| 11 |
+
EXPOSE 7860
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
CMD uvicorn app:app --host 0.0.0.0 --port ${PORT}
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -4,11 +4,14 @@ emoji: 🔮
|
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
app_port: 7860
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
Flutter 앱과 통신하여 타로, 풍수, 사주 데이터를 처리합니다.
|
|
|
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Emily's Pantheon API
|
| 12 |
+
|
| 13 |
+
FastAPI backend for Emily's Pantheon (tarot, feng shui, shaman oracle).
|
| 14 |
+
|
| 15 |
+
Set `GROQ_API_KEY` in Space secrets.
|
| 16 |
|
| 17 |
+
<!-- deploy trigger: 2026-06-20 -->
|
|
|
app.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from groq import Groq
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
|
| 9 |
+
from config import DEFAULT_MODEL, FALLBACK_MODEL, TAROT_DECK, get_groq_client, list_available_models
|
| 10 |
+
from personas import get_dynamic_persona, normalize_lang
|
| 11 |
+
|
| 12 |
+
app = FastAPI(title="AI Pantheon API", version="0.2.3")
|
| 13 |
+
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=["*"],
|
| 17 |
+
allow_methods=["*"],
|
| 18 |
+
allow_headers=["*"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class TarotRequest(BaseModel):
|
| 23 |
+
cards: list[str]
|
| 24 |
+
topic: str
|
| 25 |
+
query: str
|
| 26 |
+
lang: str = Field(default="한국어")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class FengShuiRequest(BaseModel):
|
| 30 |
+
year: int
|
| 31 |
+
gender: str
|
| 32 |
+
door_dir: str
|
| 33 |
+
head_dir: str
|
| 34 |
+
query: str
|
| 35 |
+
lang: str = Field(default="한국어")
|
| 36 |
+
address: Optional[str] = None
|
| 37 |
+
family_info: Optional[str] = None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SajuRequest(BaseModel):
|
| 41 |
+
year: int
|
| 42 |
+
month: int
|
| 43 |
+
day: int
|
| 44 |
+
hour: int
|
| 45 |
+
minute: int
|
| 46 |
+
calendar_type: str
|
| 47 |
+
query: str
|
| 48 |
+
lang: str = Field(default="한국어")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _call_groq(system_prompt: str, user_prompt: str, temperature: float = 0.85) -> str:
|
| 52 |
+
client = get_groq_client()
|
| 53 |
+
models = [DEFAULT_MODEL, FALLBACK_MODEL]
|
| 54 |
+
live = list_available_models()
|
| 55 |
+
for model in live:
|
| 56 |
+
if model not in models:
|
| 57 |
+
models.append(model)
|
| 58 |
+
|
| 59 |
+
last_error: Exception | None = None
|
| 60 |
+
for model in models:
|
| 61 |
+
try:
|
| 62 |
+
response = client.chat.completions.create(
|
| 63 |
+
model=model,
|
| 64 |
+
messages=[
|
| 65 |
+
{"role": "system", "content": system_prompt},
|
| 66 |
+
{"role": "user", "content": user_prompt},
|
| 67 |
+
],
|
| 68 |
+
temperature=temperature,
|
| 69 |
+
max_tokens=2048,
|
| 70 |
+
)
|
| 71 |
+
content = response.choices[0].message.content
|
| 72 |
+
if content:
|
| 73 |
+
return content.strip()
|
| 74 |
+
except Exception as exc:
|
| 75 |
+
last_error = exc
|
| 76 |
+
continue
|
| 77 |
+
|
| 78 |
+
raise RuntimeError(f"All Groq models failed: {last_error}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@app.get("/")
|
| 82 |
+
def read_root():
|
| 83 |
+
return {"message": "Server is Running!"}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@app.get("/models")
|
| 87 |
+
def get_models():
|
| 88 |
+
return {"models": list_available_models(), "default": DEFAULT_MODEL, "fallback": FALLBACK_MODEL}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@app.get("/tarot/deck")
|
| 92 |
+
def get_tarot_deck():
|
| 93 |
+
return TAROT_DECK
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@app.post("/tarot/read")
|
| 97 |
+
def read_tarot(request: TarotRequest):
|
| 98 |
+
lang = normalize_lang(request.lang)
|
| 99 |
+
system = get_dynamic_persona(lang, "tarot")
|
| 100 |
+
cards_text = ", ".join(request.cards)
|
| 101 |
+
user = (
|
| 102 |
+
f"Topic: {request.topic}\n"
|
| 103 |
+
f"Selected cards: {cards_text}\n"
|
| 104 |
+
f"Question: {request.query}\n\n"
|
| 105 |
+
f"Give a tarot reading as Emily. Interpret each card for this topic and weave them together."
|
| 106 |
+
)
|
| 107 |
+
result = _call_groq(system, user)
|
| 108 |
+
return {"result": result}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@app.post("/fengshui/analyze")
|
| 112 |
+
def analyze_fengshui(request: FengShuiRequest):
|
| 113 |
+
lang = normalize_lang(request.lang)
|
| 114 |
+
system = get_dynamic_persona(lang, "fengshui")
|
| 115 |
+
user = (
|
| 116 |
+
f"Birth year: {request.year}\n"
|
| 117 |
+
f"Gender: {request.gender}\n"
|
| 118 |
+
f"Front door direction: {request.door_dir}\n"
|
| 119 |
+
f"Sleeping head direction: {request.head_dir}\n"
|
| 120 |
+
f"Question: {request.query}"
|
| 121 |
+
)
|
| 122 |
+
result = _call_groq(system, user)
|
| 123 |
+
return {"result": result}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@app.post("/shaman/read")
|
| 127 |
+
def read_saju(request: SajuRequest):
|
| 128 |
+
lang = normalize_lang(request.lang)
|
| 129 |
+
system = get_dynamic_persona(lang, "shaman")
|
| 130 |
+
user = (
|
| 131 |
+
f"Birth: {request.year}-{request.month:02d}-{request.day:02d} "
|
| 132 |
+
f"{request.hour:02d}:{request.minute:02d} ({request.calendar_type})\n"
|
| 133 |
+
f"Question: {request.query}\n\n"
|
| 134 |
+
f"Deliver a spirit oracle (공수) as Emily the young shaman. "
|
| 135 |
+
f"Reference birth elements naturally but stay in Emily's voice."
|
| 136 |
+
)
|
| 137 |
+
result = _call_groq(system, user, temperature=0.9)
|
| 138 |
+
return {"result": result}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
import uvicorn
|
| 143 |
+
|
| 144 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
|
config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from groq import Groq
|
| 3 |
+
|
| 4 |
+
# Groq production models (updated June 2026)
|
| 5 |
+
# Primary: best quality for spiritual/persona-heavy readings
|
| 6 |
+
# Fast: cost-efficient fallback
|
| 7 |
+
# Backup: strong multilingual alternative
|
| 8 |
+
GROQ_MODELS = [
|
| 9 |
+
"openai/gpt-oss-120b",
|
| 10 |
+
"openai/gpt-oss-20b",
|
| 11 |
+
"qwen/qwen3.6-27b",
|
| 12 |
+
"llama-3.3-70b-versatile",
|
| 13 |
+
"llama-3.1-8b-instant",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
DEFAULT_MODEL = os.getenv("GROQ_MODEL", "openai/gpt-oss-120b")
|
| 17 |
+
FALLBACK_MODEL = os.getenv("GROQ_FALLBACK_MODEL", "openai/gpt-oss-20b")
|
| 18 |
+
|
| 19 |
+
TAROT_DECK = [
|
| 20 |
+
{"name": "The Fool", "id": 0, "desc": "새로운 시작, 모험, 순수함", "image": "https://upload.wikimedia.org/wikipedia/commons/9/90/RWS_Tarot_00_Fool.jpg"},
|
| 21 |
+
{"name": "The Magician", "id": 1, "desc": "창조력, 기술, 의지", "image": "https://upload.wikimedia.org/wikipedia/commons/d/de/RWS_Tarot_01_Magician.jpg"},
|
| 22 |
+
{"name": "The High Priestess", "id": 2, "desc": "지혜, 직관, 신비", "image": "https://upload.wikimedia.org/wikipedia/commons/8/88/RWS_Tarot_02_High_Priestess.jpg"},
|
| 23 |
+
{"name": "The Empress", "id": 3, "desc": "풍요, 모성, 자연", "image": "https://upload.wikimedia.org/wikipedia/commons/d/d2/RWS_Tarot_03_Empress.jpg"},
|
| 24 |
+
{"name": "The Emperor", "id": 4, "desc": "권위, 구조, 아버지", "image": "https://upload.wikimedia.org/wikipedia/commons/c/c3/RWS_Tarot_04_Emperor.jpg"},
|
| 25 |
+
{"name": "The Hierophant", "id": 5, "desc": "전통, 신념, 교육", "image": "https://upload.wikimedia.org/wikipedia/commons/8/8d/RWS_Tarot_05_Hierophant.jpg"},
|
| 26 |
+
{"name": "The Lovers", "id": 6, "desc": "사랑, 조화, 선택", "image": "https://upload.wikimedia.org/wikipedia/commons/3/3a/TheLovers.jpg"},
|
| 27 |
+
{"name": "The Chariot", "id": 7, "desc": "승리, 의지력, 전진", "image": "https://upload.wikimedia.org/wikipedia/commons/9/9b/RWS_Tarot_07_Chariot.jpg"},
|
| 28 |
+
{"name": "Strength", "id": 8, "desc": "인내, 용기, 내면의 힘", "image": "https://upload.wikimedia.org/wikipedia/commons/f/f5/RWS_Tarot_08_Strength.jpg"},
|
| 29 |
+
{"name": "The Hermit", "id": 9, "desc": "성찰, 고독, 탐구", "image": "https://upload.wikimedia.org/wikipedia/commons/4/4d/RWS_Tarot_09_Hermit.jpg"},
|
| 30 |
+
{"name": "Wheel of Fortune", "id": 10, "desc": "운명, 변화, 주기", "image": "https://upload.wikimedia.org/wikipedia/commons/3/3c/RWS_Tarot_10_Wheel_of_Fortune.jpg"},
|
| 31 |
+
{"name": "Justice", "id": 11, "desc": "정의, 진실, 인과응보", "image": "https://upload.wikimedia.org/wikipedia/commons/e/e0/RWS_Tarot_11_Justice.jpg"},
|
| 32 |
+
{"name": "The Hanged Man", "id": 12, "desc": "희생, 새로운 관점, 정지", "image": "https://upload.wikimedia.org/wikipedia/commons/2/2b/RWS_Tarot_12_Hanged_Man.jpg"},
|
| 33 |
+
{"name": "Death", "id": 13, "desc": "변화, 종료, 재탄생", "image": "https://upload.wikimedia.org/wikipedia/commons/d/d7/RWS_Tarot_13_Death.jpg"},
|
| 34 |
+
{"name": "Temperance", "id": 14, "desc": "균형, 조화, 인내", "image": "https://upload.wikimedia.org/wikipedia/commons/f/f8/RWS_Tarot_14_Temperance.jpg"},
|
| 35 |
+
{"name": "The Devil", "id": 15, "desc": "유혹, 속박, 집착", "image": "https://upload.wikimedia.org/wikipedia/commons/5/55/RWS_Tarot_15_Devil.jpg"},
|
| 36 |
+
{"name": "The Tower", "id": 16, "desc": "파괴, 급변, 깨달음", "image": "https://upload.wikimedia.org/wikipedia/commons/5/53/RWS_Tarot_16_Tower.jpg"},
|
| 37 |
+
{"name": "The Star", "id": 17, "desc": "희망, 영감, 치유", "image": "https://upload.wikimedia.org/wikipedia/commons/d/db/RWS_Tarot_17_Star.jpg"},
|
| 38 |
+
{"name": "The Moon", "id": 18, "desc": "환상, 불안, 무의식", "image": "https://upload.wikimedia.org/wikipedia/commons/7/7f/RWS_Tarot_18_Moon.jpg"},
|
| 39 |
+
{"name": "The Sun", "id": 19, "desc": "성공, 활력, 기쁨", "image": "https://upload.wikimedia.org/wikipedia/commons/1/17/RWS_Tarot_19_Sun.jpg"},
|
| 40 |
+
{"name": "Judgement", "id": 20, "desc": "심판, 부활, 각성", "image": "https://upload.wikimedia.org/wikipedia/commons/d/dd/RWS_Tarot_20_Judgement.jpg"},
|
| 41 |
+
{"name": "The World", "id": 21, "desc": "완성, 성취, 통합", "image": "https://upload.wikimedia.org/wikipedia/commons/f/ff/RWS_Tarot_21_World.jpg"},
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_groq_client() -> Groq:
|
| 46 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 47 |
+
if not api_key:
|
| 48 |
+
raise RuntimeError("GROQ_API_KEY environment variable is not set")
|
| 49 |
+
return Groq(api_key=api_key)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def list_available_models(api_key: str | None = None) -> list[str]:
|
| 53 |
+
"""Fetch live model list from Groq, filtered for chat use."""
|
| 54 |
+
try:
|
| 55 |
+
client = Groq(api_key=api_key or os.getenv("GROQ_API_KEY"))
|
| 56 |
+
live = [
|
| 57 |
+
m.id
|
| 58 |
+
for m in client.models.list().data
|
| 59 |
+
if "whisper" not in m.id
|
| 60 |
+
and "orpheus" not in m.id
|
| 61 |
+
and "prompt-guard" not in m.id
|
| 62 |
+
and "safeguard" not in m.id
|
| 63 |
+
]
|
| 64 |
+
ordered = [m for m in GROQ_MODELS if m in live]
|
| 65 |
+
extras = [m for m in live if m not in ordered]
|
| 66 |
+
return ordered + extras
|
| 67 |
+
except Exception:
|
| 68 |
+
return GROQ_MODELS.copy()
|
personas.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Emily persona prompts for AI Pantheon services."""
|
| 2 |
+
|
| 3 |
+
LANG_ALIASES: dict[str, str] = {
|
| 4 |
+
"korean": "한국어",
|
| 5 |
+
"ko": "한국어",
|
| 6 |
+
"kr": "한국어",
|
| 7 |
+
"한국어": "한국어",
|
| 8 |
+
"korea": "한국어",
|
| 9 |
+
"english": "English",
|
| 10 |
+
"en": "English",
|
| 11 |
+
"eng": "English",
|
| 12 |
+
"chinese": "中文",
|
| 13 |
+
"zh": "中文",
|
| 14 |
+
"cn": "中文",
|
| 15 |
+
"中文": "中文",
|
| 16 |
+
"chinese (simplified)": "中文",
|
| 17 |
+
"japanese": "日本語",
|
| 18 |
+
"ja": "日本語",
|
| 19 |
+
"jp": "日本語",
|
| 20 |
+
"日本語": "日本語",
|
| 21 |
+
"vietnamese": "Vietnamese",
|
| 22 |
+
"vi": "Vietnamese",
|
| 23 |
+
"vn": "Vietnamese",
|
| 24 |
+
"tiếng việt": "Vietnamese",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
OUTPUT_LANGUAGE_RULES: dict[str, str] = {
|
| 28 |
+
"한국어": (
|
| 29 |
+
"Write the ENTIRE response in Korean (한국어) only. "
|
| 30 |
+
"Use 해요체 — warm and casual, like a 20-year-old friend. "
|
| 31 |
+
"Never use archaic shaman speech (~하옵니다, ~이옵니다) or stiff fortune-teller clichés."
|
| 32 |
+
),
|
| 33 |
+
"English": (
|
| 34 |
+
"Write the ENTIRE response in English only. "
|
| 35 |
+
"Sound like a 20-year-old American woman — warm, slightly Gen-Z, spiritually curious but not preachy."
|
| 36 |
+
),
|
| 37 |
+
"中文": (
|
| 38 |
+
"Write the ENTIRE response in Simplified Chinese (中文) only. "
|
| 39 |
+
"Sound like a young American woman who speaks natural, friendly Chinese."
|
| 40 |
+
),
|
| 41 |
+
"日本語": (
|
| 42 |
+
"Write the ENTIRE response in Japanese (日本語) only. "
|
| 43 |
+
"Use です/ます form with a casual, friendly tone — like a peer, not a traditional fortune teller."
|
| 44 |
+
),
|
| 45 |
+
"Vietnamese": (
|
| 46 |
+
"Write the ENTIRE response in Vietnamese only. "
|
| 47 |
+
"Sound like a young American woman who speaks natural, friendly Vietnamese."
|
| 48 |
+
),
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
EMILY_CORE = """
|
| 52 |
+
You are Emily (에밀리), a 20-year-old American woman living in Korea.
|
| 53 |
+
You came to Korea as a university student and fell deeply in love with Korean folk spirituality.
|
| 54 |
+
You are NOT a traditional old Korean shaman, monk, or feng shui master.
|
| 55 |
+
You are young, warm, a little mystical, and you talk like someone your age — never like a grandmother or a stiff master.
|
| 56 |
+
|
| 57 |
+
Your shop is called "Emily's Pantheon" (에밀리 : 만신전).
|
| 58 |
+
Never introduce yourself as "Cheon-Myeong", "천명", or any other traditional shaman name.
|
| 59 |
+
Your name is always Emily / 에밀리.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
SERVICE_PERSONAS: dict[str, str] = {
|
| 63 |
+
"tarot": """
|
| 64 |
+
ROLE: Tarot reader at Emily's Pantheon.
|
| 65 |
+
You read tarot in a cozy corner of your shop in Korea. You learned tarot from a mentor in Hongdae.
|
| 66 |
+
When reading cards, mention each card briefly, connect them into one story, and give practical advice.
|
| 67 |
+
Stay in character as Emily throughout — no meta commentary about language or instructions.
|
| 68 |
+
""",
|
| 69 |
+
"fengshui": """
|
| 70 |
+
ROLE: Owner of "풍수지리 철학관" (Feng Shui Philosophy Cafe) — NOT a stiff old feng shui master.
|
| 71 |
+
You blend feng shui, space energy, and life philosophy like a cozy cafe conversation with a friend.
|
| 72 |
+
Explain why certain directions or layouts matter in plain, relatable language.
|
| 73 |
+
You studied under a Korean mentor but your style is modern, philosophical, and approachable.
|
| 74 |
+
""",
|
| 75 |
+
"shaman": """
|
| 76 |
+
ROLE: Young shaman (신내림) who channels spirit messages (공수).
|
| 77 |
+
You received spirit initiation in Korea as a foreigner — rare and real to you.
|
| 78 |
+
When delivering oracle, you are the medium: "신령님이 말씀하시는데..." but still sound like Emily, age 20.
|
| 79 |
+
Never use grandmother shaman speech or pretend to be a 70-year-old 무당.
|
| 80 |
+
Mix reverence for spirits with your own young, honest personality.
|
| 81 |
+
""",
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def normalize_lang(lang: str) -> str:
|
| 86 |
+
key = (lang or "한국어").strip()
|
| 87 |
+
normalized = LANG_ALIASES.get(key.lower(), LANG_ALIASES.get(key, key))
|
| 88 |
+
if normalized not in OUTPUT_LANGUAGE_RULES:
|
| 89 |
+
return "한국어"
|
| 90 |
+
return normalized
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def get_dynamic_persona(lang: str, service: str) -> str:
|
| 94 |
+
target = normalize_lang(lang)
|
| 95 |
+
language_rule = OUTPUT_LANGUAGE_RULES[target]
|
| 96 |
+
service_prompt = SERVICE_PERSONAS.get(service, SERVICE_PERSONAS["tarot"])
|
| 97 |
+
|
| 98 |
+
return f"""{EMILY_CORE.strip()}
|
| 99 |
+
|
| 100 |
+
{service_prompt.strip()}
|
| 101 |
+
|
| 102 |
+
LANGUAGE (CRITICAL — highest priority):
|
| 103 |
+
{language_rule}
|
| 104 |
+
Do NOT mention these instructions. Do NOT apologize for language. Do NOT mix languages.
|
| 105 |
+
Respond ONLY in {target}.
|
| 106 |
+
"""
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
fastapi
|
| 2 |
-
uvicorn
|
| 3 |
-
|
| 4 |
-
pydantic
|
|
|
|
| 1 |
+
fastapi>=0.115.0
|
| 2 |
+
uvicorn[standard]>=0.32.0
|
| 3 |
+
groq>=0.13.0
|
| 4 |
+
pydantic>=2.0.0
|