import os from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from manager.dialogue_manager import handle_dialogue from rag.rag_manager import chroma_initialized, load_game_docs_from_disk, add_docs, set_embedder from contextlib import asynccontextmanager from models.model_loader import load_fallback_model, load_embedder from schemas import AskReq, AskRes from pathlib import Path from config import ( FALLBACK_MODEL_NAME, FALLBACK_MODEL_DIR, EMBEDDER_MODEL_NAME, EMBEDDER_MODEL_DIR, HF_TOKEN, BASE_DIR ) @asynccontextmanager async def lifespan(app: FastAPI): print("๐Ÿš€ ์„œ๋ฒ„ ์‹œ์ž‘ ์ค‘... ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...") # Fallback 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 embedder = load_embedder(EMBEDDER_MODEL_NAME, EMBEDDER_MODEL_DIR, token=HF_TOKEN) app.state.embedder = embedder set_embedder(embedder) # ์ถ”๊ฐ€ print("โœ… ๋ชจ๋“  ๋ชจ๋ธ ๋กœ๋”ฉ ์™„๋ฃŒ") # RAG ์ดˆ๊ธฐํ™” 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 ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋จ") yield # ์•ฑ ์‹คํ–‰ print("๐Ÿ›‘ ์„œ๋ฒ„ ์ข…๋ฃŒ ์ค‘...") app = FastAPI(title="ai-server", lifespan=lifespan) # CORS ์„ค์ • (game-server์—์„œ ์š”์ฒญ ๊ฐ€๋Šฅํ•˜๋„๋ก) app.add_middleware( CORSMiddleware, allow_origins=["https://fpsgame-rrbc.onrender.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.post("/ask", response_model=AskRes) async def ask(request: Request, req: AskReq): 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 = context.npc_config npc_config_dict = npc_config.model_dump() if npc_config else None result = 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 ) return result @app.post("/wake") async def wake(request: Request): body = await request.json() session_id = body.get("session_id", "unknown") print(f"๐Ÿ“ก Wake signal received for session: {session_id}") return {"status": "awake", "session_id": session_id} ''' ์ตœ์ข… 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": "๊ทธ๋‚ ์„ ๋– ์˜ฌ๋ฆฌ๋Š” ๊ฒŒ ๋„ˆ๋ฌด ํž˜๋“ค์–ด์š”."} ] } } '''