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 = "" end_tag = "" 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": "๊ทธ๋‚ ์„ ๋– ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํž˜๋“ค์–ด์š”."} ] } } '''