GitHub Actions commited on
Commit
280a261
·
1 Parent(s): 09849b4

Deploy Emily Pantheon backend from GitHub Actions

Browse files
Files changed (6) hide show
  1. Dockerfile +8 -19
  2. README.md +7 -4
  3. app.py +144 -0
  4. config.py +68 -0
  5. personas.py +106 -0
  6. requirements.txt +4 -4
Dockerfile CHANGED
@@ -1,24 +1,13 @@
1
- # 1. 파이썬 3.9 환경
2
- FROM python:3.9
3
 
4
- # 2. 작업 폴더
5
- WORKDIR /code
6
 
7
- # 3. 라이브러리 설치
8
- COPY ./requirements.txt /code/requirements.txt
9
- RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
10
 
11
- # 4. 코드 복사
12
- COPY . /code
13
 
14
- # 5. 권한 설정 (HF 필수)
15
- RUN useradd -m -u 1000 user
16
- USER user
17
- ENV HOME=/home/user \
18
- PATH=/home/user/.local/bin:$PATH
19
 
20
- WORKDIR $HOME/app
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
- # 🔮 AI Pantheon API Server
 
 
 
 
12
 
13
- 이곳은 **AI 만신전(Pantheon)**의 두뇌 역할을 하는 FastAPI 서버입니다.
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
- huggingface_hub
4
- pydantic
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.32.0
3
+ groq>=0.13.0
4
+ pydantic>=2.0.0