JOhyeongi commited on
Commit
d8a3c6f
Β·
verified Β·
1 Parent(s): c13eed8

Upload 6 files

Browse files
Files changed (6) hide show
  1. .dockerignore +29 -0
  2. Dockerfile +36 -0
  3. __init__.py +1 -0
  4. api_server.py +213 -0
  5. config.py +40 -0
  6. requirements.txt +14 -0
.dockerignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ **/__pycache__/**
3
+ **/*.py[cod]
4
+
5
+ # Git / CI
6
+ .git/
7
+ .github/
8
+
9
+ # Editors / OS
10
+ .DS_Store
11
+ Thumbs.db
12
+
13
+ # Local / Dev only
14
+ *.bat
15
+ *.sh
16
+ *.crdownload
17
+ pages/**
18
+ PythonProject2/**
19
+
20
+ # Large binaries / PDFs (adjust if needed)
21
+ **/*.pdf
22
+
23
+ # Node
24
+ frontend/node_modules/
25
+
26
+ # Build caches
27
+ **/.cache/**
28
+
29
+
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage Dockerfile: build frontend, run FastAPI backend
2
+
3
+ FROM node:20-alpine AS frontend-builder
4
+ WORKDIR /app
5
+ COPY frontend/package.json frontend/package-lock.json* ./
6
+ RUN npm ci --no-audit --no-fund || npm install --no-audit --no-fund
7
+ COPY frontend/ ./
8
+ RUN npm run build
9
+
10
+ FROM python:3.11-slim AS backend
11
+ ENV PYTHONDONTWRITEBYTECODE=1 \
12
+ PYTHONUNBUFFERED=1
13
+
14
+ WORKDIR /app
15
+
16
+ # System deps (faiss-cpu needs libgomp1 on Debian)
17
+ RUN apt-get update && apt-get install -y --no-install-recommends \
18
+ libgomp1 \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ COPY requirements.txt ./
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy application
25
+ COPY . /app
26
+
27
+ # Copy built frontend into expected path for static serving
28
+ RUN mkdir -p /app/frontend/dist
29
+ COPY --from=frontend-builder /app/dist /app/frontend/dist
30
+
31
+ EXPOSE 7860
32
+
33
+ # Hugging Face SpacesλŠ” κΈ°λ³Έ PORT=7860을 μ‚¬μš©ν•©λ‹ˆλ‹€.
34
+ CMD ["sh", "-c", "uvicorn api_server:app --host 0.0.0.0 --port ${PORT:-7860}"]
35
+
36
+
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # λͺ¨μž„톡μž₯ AI μ–΄λ“œλ°”μ΄μ € νŒ¨ν‚€μ§€
api_server.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ λͺ¨λ°”일 λ¦¬μ•‘νŠΈ ν”„λŸ°νŠΈμ—μ„œ ν˜ΈμΆœν•  REST API μ„œλ²„ (FastAPI)
3
+
4
+ μ—”λ“œν¬μΈνŠΈ:
5
+ - POST /api/advise : μ’…ν•© 상담 (금육+정보+볡지)
6
+ - POST /api/qa : 자유 질문/챗봇 응닡
7
+ - GET /api/welfare/search : 볡지정책 검색
8
+ - GET /api/products/search : κΈˆμœ΅μƒν’ˆ RAG 검색
9
+ - GET /health : μƒνƒœ 점검
10
+ """
11
+
12
+ from fastapi import FastAPI, HTTPException, Query
13
+ from fastapi.staticfiles import StaticFiles
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from pydantic import BaseModel, Field
16
+ from typing import List, Optional, Dict, Any
17
+
18
+ from agents.multi_agent_system import get_meeting_account_advisor
19
+ from agents.welfare_advisor import WelfareAdvisor
20
+ from agents.universal_info_advisor import get_universal_info_advisor
21
+ from models.rag_system import get_kb_rag
22
+
23
+ import pandas as pd
24
+ import uvicorn
25
+
26
+
27
+ app = FastAPI(title="KB 금육 AI Advisor API", version="1.0.0")
28
+
29
+ # CORS (λ¦¬μ•‘νŠΈ/λͺ¨λ°”일 μ›Ήμ—μ„œ 호좜 ν—ˆμš©)
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ # ν”„λŸ°νŠΈμ—”λ“œ 정적 파일 제곡 (frontend/distκ°€ 있으면 μžλ™ μ„œλΉ™)
39
+ try:
40
+ import os
41
+ FRONT_DIST = os.path.join(os.path.dirname(__file__), "frontend", "dist")
42
+ if os.path.isdir(FRONT_DIST):
43
+ app.mount("/", StaticFiles(directory=FRONT_DIST, html=True), name="frontend")
44
+ except Exception:
45
+ pass
46
+
47
+
48
+ class UserInfo(BaseModel):
49
+ age_group: Optional[str] = None
50
+ family_type: Optional[str] = None
51
+ income_level: Optional[str] = None
52
+ region: Optional[str] = None
53
+
54
+
55
+ class AdviseRequest(BaseModel):
56
+ purpose: str = Field(..., description="예: 슀페인 μ–΄ν•™μ—°μˆ˜, κ²°ν˜Όμ€€λΉ„ λ“±")
57
+ duration_months: int = Field(..., ge=1, le=120)
58
+ target_amount: int = Field(..., ge=1)
59
+ detailed_purpose: Optional[str] = None
60
+ current_amount: Optional[int] = 0
61
+ user_info: Optional[UserInfo] = None
62
+
63
+
64
+ class AdviseResponse(BaseModel):
65
+ purpose_category: Optional[str]
66
+ financial_recommendation: Optional[str]
67
+ detailed_info: Optional[str]
68
+ welfare_recommendation: Optional[str]
69
+ comprehensive_advice: Optional[str]
70
+ progress_info: Optional[str] = None
71
+
72
+
73
+ class QARequest(BaseModel):
74
+ question: str
75
+ detailed_context: Optional[str] = None
76
+
77
+
78
+ class QAResponse(BaseModel):
79
+ answer: str
80
+ category: Optional[str] = None
81
+
82
+
83
+ @app.get("/health")
84
+ def health() -> Dict[str, Any]:
85
+ return {"status": "ok"}
86
+
87
+
88
+ @app.post("/api/advise", response_model=AdviseResponse)
89
+ def api_advise(req: AdviseRequest):
90
+ try:
91
+ advisor = get_meeting_account_advisor()
92
+ results = advisor.get_comprehensive_advice(
93
+ purpose=req.purpose,
94
+ duration_months=req.duration_months,
95
+ target_amount=req.target_amount,
96
+ detailed_purpose=req.detailed_purpose,
97
+ )
98
+
99
+ # μ§„ν–‰ ν˜„ν™© (current_amount 제곡 μ‹œ)
100
+ progress_info = None
101
+ if req.current_amount and req.current_amount > 0:
102
+ progress_info = advisor.get_savings_progress_tracking(
103
+ req.purpose,
104
+ req.duration_months,
105
+ req.target_amount,
106
+ req.current_amount,
107
+ )
108
+
109
+ # WelfareAdvisor에 μ‚¬μš©μž 정보 전달이 ν•„μš”ν•œ 경우λ₯Ό μœ„ν•΄ 보쑰 호좜 (선택)
110
+ welfare_text = results.get("welfare_recommendation")
111
+ if (not welfare_text or "μ—†μŠ΅λ‹ˆλ‹€" in welfare_text) and req.user_info:
112
+ welfare = WelfareAdvisor().get_welfare_recommendations(
113
+ req.purpose, req.user_info.model_dump()
114
+ )
115
+ if welfare:
116
+ results["welfare_recommendation"] = welfare
117
+
118
+ return AdviseResponse(
119
+ purpose_category=results.get("purpose_category"),
120
+ financial_recommendation=results.get("financial_recommendation"),
121
+ detailed_info=results.get("detailed_info"),
122
+ welfare_recommendation=results.get("welfare_recommendation"),
123
+ comprehensive_advice=results.get("comprehensive_advice"),
124
+ progress_info=progress_info,
125
+ )
126
+ except Exception as e:
127
+ raise HTTPException(status_code=500, detail=str(e))
128
+
129
+
130
+ @app.post("/api/qa", response_model=QAResponse)
131
+ def api_qa(req: QARequest):
132
+ try:
133
+ advisor = get_universal_info_advisor()
134
+ info = advisor.get_comprehensive_info(
135
+ purpose=req.question,
136
+ detailed_purpose=req.detailed_context,
137
+ )
138
+ return QAResponse(
139
+ answer=info.get("comprehensive_info", "닡변을 μƒμ„±ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."),
140
+ category=info.get("category"),
141
+ )
142
+ except Exception as e:
143
+ raise HTTPException(status_code=500, detail=str(e))
144
+
145
+
146
+ @app.get("/api/welfare/search")
147
+ def api_welfare_search(
148
+ keyword: Optional[str] = Query(None, description="μ •μ±…λͺ…/λ‚΄μš©/λŒ€μƒ 검색어"),
149
+ region: Optional[str] = Query(None, description="μ§€μ—­λͺ… (예: μ„œμšΈ, λΆ€μ‚°)"),
150
+ page: int = Query(1, ge=1),
151
+ size: int = Query(10, ge=1, le=100),
152
+ ):
153
+ try:
154
+ wa = WelfareAdvisor()
155
+ df = wa.welfare_data
156
+ if df.empty:
157
+ return {"total": 0, "items": []}
158
+
159
+ filtered = df.copy()
160
+ if keyword:
161
+ kw = keyword.strip()
162
+ mask = (
163
+ filtered["policy_name"].fillna("").str.contains(kw, case=False)
164
+ | filtered["service_content_detail"].fillna("").str.contains(kw, case=False)
165
+ | filtered["target_audience_description"].fillna("").str.contains(kw, case=False)
166
+ )
167
+ filtered = filtered[mask]
168
+
169
+ if region and region != "전체":
170
+ filtered = filtered[
171
+ (filtered["region_name"].fillna("").str.contains(region, case=False))
172
+ | (filtered["region_name"] == "μ „κ΅­")
173
+ ]
174
+
175
+ total = len(filtered)
176
+ start = (page - 1) * size
177
+ end = start + size
178
+ page_df = filtered.iloc[start:end]
179
+
180
+ items = []
181
+ for _, row in page_df.iterrows():
182
+ items.append(
183
+ {
184
+ "policy_name": row.get("policy_name", ""),
185
+ "governing_body": row.get("governing_body_name", ""),
186
+ "target_audience": row.get("target_audience_description", ""),
187
+ "service_content": row.get("service_content_detail", ""),
188
+ "region": row.get("region_name", "μ „κ΅­"),
189
+ "application_link": row.get("application_link", ""),
190
+ "last_updated": row.get("last_updated", ""),
191
+ "tags": row.get("target_audience_tags", ""),
192
+ }
193
+ )
194
+
195
+ return {"total": total, "items": items}
196
+ except Exception as e:
197
+ raise HTTPException(status_code=500, detail=str(e))
198
+
199
+
200
+ @app.get("/api/products/search")
201
+ def api_products_search(q: str = Query(..., description="검색 질의"), k: int = Query(5, ge=1, le=20)):
202
+ try:
203
+ rag = get_kb_rag()
204
+ results = rag.search_products(q, k=k) or []
205
+ return {"items": results}
206
+ except Exception as e:
207
+ raise HTTPException(status_code=500, detail=str(e))
208
+
209
+
210
+ if __name__ == "__main__":
211
+ uvicorn.run("api_server:app", host="0.0.0.0", port=8000, reload=True)
212
+
213
+
config.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ λͺ¨μž„톡μž₯ AI μ—μ΄μ „νŠΈ μ„€μ • 파일
3
+ """
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ # .env 파일이 있으면 λ‘œλ“œ (선택사항)
8
+ load_dotenv()
9
+
10
+ # API Keys - ν™˜κ²½λ³€μˆ˜ μš°μ„ , ν•„μš” μ‹œ api_keys.py
11
+ IGNORE_API_KEYS = os.getenv("IGNORE_API_KEYS", "0") == "1"
12
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
13
+ TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
14
+
15
+ if not IGNORE_API_KEYS and not GOOGLE_API_KEY:
16
+ try:
17
+ from api_keys import GOOGLE_API_KEY as FILE_GOOGLE_API_KEY, TAVILY_API_KEY as FILE_TAVILY_API_KEY
18
+ GOOGLE_API_KEY = GOOGLE_API_KEY or FILE_GOOGLE_API_KEY
19
+ TAVILY_API_KEY = TAVILY_API_KEY or FILE_TAVILY_API_KEY
20
+ print("βœ… api_keys.pyμ—μ„œ API ν‚€λ₯Ό μ„±κ³΅μ μœΌλ‘œ λ‘œλ“œν–ˆμŠ΅λ‹ˆλ‹€.")
21
+ except ImportError:
22
+ print("⚠️ api_keys.pyλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. ν™˜κ²½λ³€μˆ˜μ—μ„œ λ‘œλ“œλ₯Ό μ‹œλ„ν•©λ‹ˆλ‹€.")
23
+
24
+ SERPAPI_API_KEY = os.getenv("SERPAPI_API_KEY")
25
+
26
+ # Model Settings
27
+ GEMINI_MODEL_NAME = "gemini-2.0-flash-exp"
28
+
29
+ # RAG Settings
30
+ CHUNK_SIZE = 1000
31
+ CHUNK_OVERLAP = 200
32
+ TOP_K_RESULTS = 3
33
+
34
+ # Data Paths
35
+ KB_DATASET_PATH = "data/kb_products.csv"
36
+ VECTOR_STORE_PATH = "data/vector_store"
37
+
38
+ # Streamlit Settings
39
+ PAGE_TITLE = "λͺ¨μž„톡μž₯ AI μ–΄λ“œλ°”μ΄μ €"
40
+ PAGE_ICON = "πŸ’°"
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain>=0.1.0
2
+ langchain-google-genai>=1.0.0
3
+ langchain-community>=0.0.35
4
+ streamlit>=1.30.0
5
+ pandas>=2.0.0
6
+ numpy>=1.21.0
7
+ faiss-cpu>=1.7.0
8
+ python-dotenv>=1.0.0
9
+ tavily-python>=0.3.0
10
+ requests>=2.30.0
11
+ sentence-transformers>=2.2.0
12
+ fastapi>=0.111.0
13
+ uvicorn[standard]>=0.30.0
14
+ pydantic>=2.7.0