m97j's picture
Initial commit
361d672
import asyncio
from pathlib import Path
import markdown
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.templating import Jinja2Templates
from contextlib import asynccontextmanager
from manager.dialogue_manager import handle_dialogue
from rag.rag_manager import chroma_initialized, load_game_docs_from_disk, add_docs, set_embedder
from models.model_loader import load_fallback_model, load_embedder
from schemas import AskReq, AskRes
from config import (
FALLBACK_MODEL_NAME, FALLBACK_MODEL_DIR,
EMBEDDER_MODEL_NAME, EMBEDDER_MODEL_DIR,
HF_TOKEN, BASE_DIR
)
templates = Jinja2Templates(directory="templates")
model_ready = False
async def load_models(app: FastAPI):
global model_ready
print("๐Ÿš€ ๋ชจ๋ธ ๋กœ๋”ฉ ์‹œ์ž‘...")
fb_tokenizer, fb_model = load_fallback_model(FALLBACK_MODEL_NAME, FALLBACK_MODEL_DIR, token=HF_TOKEN)
app.state.fallback_tokenizer = fb_tokenizer
app.state.fallback_model = fb_model
embedder = load_embedder(EMBEDDER_MODEL_NAME, EMBEDDER_MODEL_DIR, token=HF_TOKEN)
app.state.embedder = embedder
set_embedder(embedder)
docs_path = BASE_DIR / "rag" / "docs"
if not chroma_initialized():
docs = load_game_docs_from_disk(str(docs_path))
add_docs(docs)
print(f"โœ… RAG ๋ฌธ์„œ {len(docs)}๊ฐœ ์‚ฝ์ž… ์™„๋ฃŒ")
else:
print("๐Ÿ”„ RAG DB ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋จ")
model_ready = True
print("โœ… ๋ชจ๋“  ๋ชจ๋ธ ๋กœ๋”ฉ ์™„๋ฃŒ")
@asynccontextmanager
async def lifespan(app: FastAPI):
asyncio.create_task(load_models(app))
yield
print("๐Ÿ›‘ ์„œ๋ฒ„ ์ข…๋ฃŒ ์ค‘...")
app = FastAPI(title="ai-server", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://fpsgame-rrbc.onrender.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/", include_in_schema=False)
async def root(request: Request):
md_path = Path(__file__).parent / "README.md"
md_content = md_path.read_text(encoding="utf-8")
start_tag = "<!-- app-tab:start -->"
end_tag = "<!-- app-tab:end -->"
if start_tag in md_content and end_tag in md_content:
short_md = md_content.split(start_tag)[1].split(end_tag)[0].strip()
else:
short_md = md_content # fallback: ์ „์ฒด ๋‚ด์šฉ
html_from_md = markdown.markdown(short_md)
return templates.TemplateResponse("index.html", {"request": request, "readme_content": html_from_md})
@app.get("/status")
async def status():
return {"ready": model_ready}
@app.post("/wake")
async def wake(request: Request):
session_id = (await request.json()).get("session_id", "unknown")
print(f"๐Ÿ“ก Wake signal received for session: {session_id}")
if not model_ready:
asyncio.create_task(load_models(app))
return {"status": "awake", "model_ready": model_ready}
@app.post("/ask", response_model=AskRes)
async def ask(request: Request, req: AskReq):
if not model_ready:
raise HTTPException(status_code=503, detail="Model not ready")
if not req.context:
raise HTTPException(status_code=400, detail="missing context")
if not (req.session_id and req.npc_id and req.user_input):
raise HTTPException(status_code=400, detail="missing fields")
context = req.context
npc_config_dict = context.npc_config.model_dump() if context.npc_config else None
return await handle_dialogue(
request=request,
session_id=req.session_id,
npc_id=req.npc_id,
user_input=req.user_input,
context=context.model_dump(),
npc_config=npc_config_dict
)
'''
์ตœ์ข… gameโ€‘server โ†’ aiโ€‘server ์š”์ฒญ ์˜ˆ์‹œ
{
"session_id": "abc123",
"npc_id": "mother_abandoned_factory",
"user_input": "์•„! ๋จธ๋ฆฌ๊ฐ€โ€ฆ ๊ธฐ์–ต์ด ๋– ์˜ฌ๋ž์–ด์š”.",
/* game-server์—์„œ ํ•„ํ„ฐ๋งํ•œ ํ•„์ˆ˜/์„ ํƒ require ์š”์†Œ๋งŒ ํฌํ•จ */
"context": {
"require": {
"items": ["photo_forgotten_party"], // ํ•„์ˆ˜/์„ ํƒ ๊ตฌ๋ถ„์€ npc_config.json์—์„œ
"actions": ["visited_factory"],
"game_state": ["box_opened"], // ํ•„์š” ์‹œ
"delta": { "trust": 0.35, "relationship": 0.1 }
},
"player_state": {
"level": 7,
"reputation": "helpful",
"location": "map1"
/* ์ „์ฒด ์ธ๋ฒคํ† ๋ฆฌ/ํ–‰๋™ ๋กœ๊ทธ๋Š” ํ•„์š” ์‹œ ๋ณ„๋„ ์ „๋‹ฌ */
},
"game_state": {
"current_quest": "search_jason",
"quest_stage": "in_progress",
"location": "map1",
"time_of_day": "evening"
},
"npc_state": {
"id": "mother_abandoned_factory",
"name": "์‹ค๋น„์•„",
"persona_name": "Silvia",
"dialogue_style": "emotional",
"relationship": 0.35,
"npc_mood": "grief"
},
"dialogue_history": [
{
"player": "ํ˜น์‹œ ์ด ๊ณต์žฅ์—์„œ ๋ณธ ๊ฑธ ๋งํ•ด์ค˜์š”.",
"npc": "๊ทธ๋‚ ์„ ๋– ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํž˜๋“ค์–ด์š”."
}
]
}
}
'''
'''
{
"session_id": "abc123",
"npc_id": "mother_abandoned_factory",
"user_input": "์•„! ๋จธ๋ฆฌ๊ฐ€โ€ฆ ๊ธฐ์–ต์ด ๋– ์˜ฌ๋ž์–ด์š”.",
"precheck_passed": true,
"context": {
"player_status": {
"level": 7,
"reputation": "helpful",
"location": "map1",
"trigger_items": ["photo_forgotten_party"], // game-server์—์„œ ์กฐ๊ฑด ํ•„ํ„ฐ ํ›„ key๋กœ ๋ณ€ํ™˜
"trigger_actions": ["visited_factory"] // ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ key ๋ฌธ์ž์—ด
/* ์›๋ณธ ์ „์ฒด inventory/actions ๋ฐฐ์—ด์€ ์„œ๋น„์Šค ํ•„์š” ์‹œ ๋ณ„๋„ ์ „๋‹ฌ ๊ฐ€๋Šฅ
ํ•˜์ง€๋งŒ ai-server ์กฐ๊ฑด ํŒ์ •์—๋Š” trigger_*๋งŒ ์‚ฌ์šฉ */
},
"game_state": {
"current_quest": "search_jason",
"quest_stage": "in_progress",
"location": "map1",
"time_of_day": "evening"
},
"npc_config": {
"id": "mother_abandoned_factory",
"name": "์‹ค๋น„์•„",
"persona_name": "Silvia",
"dialogue_style": "emotional",
"relationship": 0.35,
"npc_mood": "grief",
"trigger_values": {
"in_progress": ["๊ธฐ์–ต", "์‚ฌ์ง„", "ํŒŒํ‹ฐ"]
},
"trigger_definitions": {
"in_progress": {
"required_text": ["๊ธฐ์–ต", "์‚ฌ์ง„"],
"required_items": ["photo_forgotten_party"], // trigger_items์™€ ๋งค์นญ
"required_actions": ["visited_factory"], // trigger_actions์™€ ๋งค์นญ
"emotion_threshold": { "sad": 0.2 },
"fallback_style": {
"style": "guarded",
"npc_emotion": "suspicious"
}
}
}
},
"dialogue_history": [
{
"player": "ํ˜น์‹œ ์ด ๊ณต์žฅ์—์„œ ๋ณธ ๊ฑธ ๋งํ•ด์ค˜์š”.",
"npc": "๊ทธ๋‚ ์„ ๋– ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํž˜๋“ค์–ด์š”."
}
]
}
}
------------------------------------------------------------------------------------------------------
์ด์ „ game-server ์š”์ฒญ ๊ตฌ์กฐ ์˜ˆ์‹œ:
{
"session_id": "abc123",
"npc_id": "mother_abandoned_factory",
"user_input": "์•„! ๋จธ๋ฆฌ๊ฐ€โ€ฆ ๊ธฐ์–ต์ด ๋– ์˜ฌ๋ž์–ด์š”.",
"context": {
"player_status": {
"level": 7,
"reputation": "helpful",
"location": "map1",
"items": ["photo_forgotten_party"],
"actions": ["visited_factory", "talked_to_guard"]
},
"game_state": {
"current_quest": "search_jason",
"quest_stage": "in_progress",
"location": "map1",
"time_of_day": "evening"
},
"npc_config": {
"id": "mother_abandoned_factory",
"name": "์‹ค๋น„์•„",
"persona_name": "Silvia",
"dialogue_style": "emotional",
"relationship": 0.35,
"npc_mood": "grief",
"trigger_values": {
"in_progress": ["๊ธฐ์–ต", "์‚ฌ์ง„", "ํŒŒํ‹ฐ"]
},
"trigger_definitions": {
"in_progress": {
"required_text": ["๊ธฐ์–ต", "์‚ฌ์ง„"],
"emotion_threshold": {"sad": 0.2},
"fallback_style": {"style": "guarded", "npc_emotion": "suspicious"}
}
}
},
"dialogue_history": [
{"player": "ํ˜น์‹œ ์ด ๊ณต์žฅ์—์„œ ๋ณธ ๊ฑธ ๋งํ•ด์ค˜์š”.", "npc": "๊ทธ๋‚ ์„ ๋– ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํž˜๋“ค์–ด์š”."}
]
}
}
'''