fastAPI 심화
- Chart.js - pdf, csv 파일 업로드 후 데이터 정제하여 llm으로 처리 후 결과 도출 - sqlite로 데이터 저장 - ORM - SQLAlchemy
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from langchain_ibm import WatsonxEmbeddings
|
||||
from backend.config.settings import settings
|
||||
|
||||
watson_embedding = WatsonxEmbeddings(
|
||||
model_id="ibm/granite-embedding-278m-multilingual",
|
||||
url=f"{settings.watsonx_url}",
|
||||
api_key=f"{settings.watsonx_api_key}",
|
||||
project_id=f"{settings.watsonx_project_id}",
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
from langchain_ibm import ChatWatsonx
|
||||
from backend.config.settings import settings
|
||||
|
||||
watson_llm = ChatWatsonx(
|
||||
model_id="ibm/granite-4-h-small",
|
||||
url=f"{settings.watsonx_url}",
|
||||
api_key=f"{settings.watsonx_api_key}",
|
||||
project_id=f"{settings.watsonx_project_id}",
|
||||
max_tokens=2000,
|
||||
params={
|
||||
"temperature": 0
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file="backend/.env", extra="ignore")
|
||||
# 사용할 모델
|
||||
watsonx_api_key: str = Field(alias="WATSONX_API_KEY")
|
||||
watsonx_project_id: str = Field(alias="WATSONX_PROJECT_ID")
|
||||
watsonx_url: str = Field(alias="WATSONX_URL")
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, Request, UploadFile
|
||||
from backend.schemas.card_schema import AnalysisRequest
|
||||
from backend.services.card_service import upload_csv, card_history, get_dashboard, card_analysis
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
router = APIRouter(prefix="/api/card")
|
||||
templates = Jinja2Templates(directory="backend/templates")
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_file(file: UploadFile):
|
||||
return await upload_csv(file)
|
||||
|
||||
@router.get("/history")
|
||||
async def history(request : Request):
|
||||
card_infos = card_history()
|
||||
|
||||
return templates.TemplateResponse(request, name = "history.html", context = {"history": card_infos})
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def dashboard():
|
||||
return get_dashboard()
|
||||
|
||||
@router.post("/analysis")
|
||||
async def sql_llm_analysis(request : AnalysisRequest):
|
||||
return card_analysis(request.question)
|
||||
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="backend/templates")
|
||||
|
||||
# http://127.0.0.1:8000
|
||||
@router.get("/")
|
||||
async def home(request : Request):
|
||||
return templates.TemplateResponse(request = request, name="index.html")
|
||||
|
||||
@router.get("/card/upload")
|
||||
async def rag(request : Request):
|
||||
return templates.TemplateResponse(request = request, name="card.html")
|
||||
|
||||
@router.get("/card/dashboard")
|
||||
async def dashboard(request : Request):
|
||||
return templates.TemplateResponse(request = request, name="dashboard.html")
|
||||
|
||||
@router.get("/card/analysis")
|
||||
async def analysis(request : Request):
|
||||
return templates.TemplateResponse(request = request, name="analysis.html")
|
||||
@@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AnalysisRequest(BaseModel):
|
||||
question: str
|
||||
|
||||
class AnalysisResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
from fastapi import UploadFile
|
||||
from langchain_core.runnables import RunnablePassthrough
|
||||
|
||||
from backend.services.db_service import get_connection
|
||||
from backend.services.db_service import get_table_columns
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from backend.ai.llm import watson_llm
|
||||
import csv
|
||||
|
||||
async def upload_csv(file :UploadFile):
|
||||
"""
|
||||
csv 파일을 테이블에 저장
|
||||
"""
|
||||
|
||||
conn =get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# file.read() : 비동기 함수임
|
||||
contents = await file.read()
|
||||
csv_text = contents.decode('utf-8')
|
||||
reader = csv.DictReader(csv_text.splitlines())
|
||||
count = 0
|
||||
|
||||
for row in reader:
|
||||
cursor.execute("""
|
||||
INSERT INTO transactions (date, category, merchant, amount)
|
||||
VALUES (?, ?,?, ?)"""
|
||||
,
|
||||
(row['date'], row['category'], row['merchant'], row['amount'])
|
||||
)
|
||||
count += 1
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"message": f"{count} 건 저장 완료"}
|
||||
|
||||
def card_history():
|
||||
"""
|
||||
db에서 카드 정보 조회
|
||||
"""
|
||||
conn =get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try :
|
||||
cursor.execute("""
|
||||
SELECT * FROM transactions ORDER BY date DESC
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
query_result = [dict(row) for row in rows]
|
||||
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
query_result = [f"SQL 실행 오류 : {e}"]
|
||||
|
||||
return query_result
|
||||
|
||||
def get_dashboard():
|
||||
"""
|
||||
db에서 대시보드 정보 조회
|
||||
"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 카테고리 별 사용금액
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT category, SUM(amount)
|
||||
FROM transactions
|
||||
GROUP BY category
|
||||
ORDER BY SUM(amount) DESC
|
||||
"""
|
||||
)
|
||||
|
||||
category_rows = cursor.fetchall()
|
||||
|
||||
# 월별 사용금액
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT strftime('%Y-%m', date), SUM(amount)
|
||||
FROM transactions
|
||||
GROUP BY strftime('%Y-%m', date)
|
||||
ORDER BY strftime('%Y-%m', date)
|
||||
"""
|
||||
)
|
||||
month_rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"category": [{"category":row[0], "amount":row[1]} for row in category_rows],
|
||||
"monthly": [{"month":row[0], "amount":row[1]} for row in month_rows],
|
||||
}
|
||||
|
||||
def sql_generate_llm(question):
|
||||
"""자연어 -> SQL"""
|
||||
sql_prompt = ChatPromptTemplate.from_template("""
|
||||
당신은 SQLite 전문가입니다.
|
||||
|
||||
테이블명 : transactions
|
||||
|
||||
컬럼 : {columns}
|
||||
|
||||
질문 : {question}
|
||||
|
||||
SQL 만 출력하세요
|
||||
""")
|
||||
|
||||
columns = get_table_columns("transactions")
|
||||
sql_chain = sql_prompt | watson_llm | StrOutputParser()
|
||||
|
||||
sql = sql_chain.invoke({"columns": columns, "question": question})
|
||||
print(f"sql: {sql}")
|
||||
|
||||
sql = sql.replace("```sql","").replace("```", "").strip()
|
||||
|
||||
# sql 문 실제 실행
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sql)
|
||||
rows = cursor.fetchall()
|
||||
query_result = [dict(row) for row in rows]
|
||||
conn.close()
|
||||
|
||||
return query_result
|
||||
|
||||
def card_analysis(question):
|
||||
query_result = sql_generate_llm(question)
|
||||
|
||||
analysis_prompt = ChatPromptTemplate.from_template("""
|
||||
사용자 질문 : {question}
|
||||
|
||||
SQL 결과
|
||||
{result}
|
||||
|
||||
결과를 설명하고 소비 습관을 분석하고 절약 팁을 제시해 주세요.
|
||||
""")
|
||||
analysis_chain = analysis_prompt | watson_llm | StrOutputParser()
|
||||
answer = analysis_chain.invoke({"question": question, "result": query_result})
|
||||
|
||||
return {"message": answer}
|
||||
@@ -0,0 +1,44 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
DB_PATH = "db/history.db"
|
||||
|
||||
"""
|
||||
cursor.execute('select id, name for users')
|
||||
cursor.fetchone() => (1, 'alice', '010-1234-5678')
|
||||
|
||||
row['id']
|
||||
"""
|
||||
|
||||
def get_connection():
|
||||
coon = sqlite3.connect(DB_PATH, isolation_level=None)
|
||||
# 조회 결과를 어떻게 반활할지 설정
|
||||
coon.row_factory = sqlite3.Row
|
||||
return coon
|
||||
|
||||
def init_db():
|
||||
# 디렉토리 생성
|
||||
os.makedirs("db", exist_ok=True)
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""create table if not exists transactions (
|
||||
id integer primary key AUTOINCREMENT,
|
||||
date text,
|
||||
category text,
|
||||
merchant text,
|
||||
amount integer
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("DB 초기화")
|
||||
|
||||
def get_table_columns(table_name):
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("PRAGMA table_info({})".format(table_name))
|
||||
columns = cursor.fetchall()
|
||||
conn.close()
|
||||
return [column[1] for column in columns]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
document.querySelector("button").addEventListener("click", ask)
|
||||
|
||||
async function ask() {
|
||||
// 사용자가 질문 입력 시 질문을 서버로 전송
|
||||
const question = document.querySelector('#question').value
|
||||
|
||||
const response = await fetch("/api/card/analysis", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({question : question})
|
||||
})
|
||||
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||
const answer = await response.json()
|
||||
document.querySelector('#answer').textContent = answer.message
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 파일 업로드
|
||||
document.querySelector("#uploadBtn").addEventListener("click",uploadFile)
|
||||
async function uploadFile() {
|
||||
const fileInput = document.querySelector("#file")
|
||||
// 첨부파일 정보 가져오기
|
||||
const file = fileInput.files[0]
|
||||
|
||||
if(!file){
|
||||
alert('파일을 선택하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// form 만들어 전송
|
||||
const formData = new FormData();
|
||||
formData.append("file",file);
|
||||
|
||||
const response = await fetch("/api/card/upload",{
|
||||
method:"POST",
|
||||
body:formData
|
||||
})
|
||||
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||
const answer = await response.json()
|
||||
document.querySelector('#result').textContent = answer.message
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
const drawCategoryChart = (data) => {
|
||||
new Chart(document.querySelector("#categoryChart"), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels : data.map((x) => x.category),
|
||||
datasets:[
|
||||
{
|
||||
data:data.map((x)=>x.amount)
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '카테고리별 소비내역'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const drawMonthlyChart = (data) => {
|
||||
new Chart(document.querySelector("#monthlyChart"), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels : data.map((x) => x.month),
|
||||
datasets:[
|
||||
{
|
||||
label:"월별 소비",
|
||||
data:data.map((x)=>x.amount)
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '월별 소비내역'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
const response = await fetch("/api/card/dashboard");
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
drawCategoryChart(data.category);
|
||||
drawMonthlyChart(data.monthly)
|
||||
}
|
||||
|
||||
window.onload = loadDashboard;
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<H2>신용카드 사용내역 분석</H2>
|
||||
<div>
|
||||
<input type = "text" name = "question" id = "question" size = "50">
|
||||
<button type="button">질문하기</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="answer"></div>
|
||||
<script src="{{ url_for('static', path='js/analysis.js')}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>카드 정보 추가</h2>
|
||||
<div>
|
||||
<input type="file" name="file" id="file">
|
||||
<button id="uploadBtn">업로드</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="result"></div>
|
||||
<script src="{{ url_for('static', path='js/card.js')}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>소비내역 대시보드</h2>
|
||||
<div style ="width: 700px; height: 300px">
|
||||
<canvas id="categoryChart"></canvas>
|
||||
</div>
|
||||
<div style ="width: 700px; height: 300px">
|
||||
<canvas id="monthlyChart"></canvas>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="{{ url_for('static', path='js/dashboard.js')}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>카드 소비 내역</h2>
|
||||
<table border = "1">
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<th>카테고리</th>
|
||||
<th>가맹점</th>
|
||||
<th>금액</th>
|
||||
</tr>
|
||||
<tbody>
|
||||
{% for row in history %}
|
||||
<tr>
|
||||
<td>{{ row.date }}</td>
|
||||
<td>{{ row.category }}</td>
|
||||
<td>{{ row.merchant }}</td>
|
||||
<td>{{ row.amount }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>신용카드 분석</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/card/upload">1. 카드 사용 정보 업로드</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/api/card/history">2. 카드 사용 내역 확인</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/card/dashboard">3. 카드 소비 내역 대시보드</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/card/analysis">4. 카드 소비 내역 분석 및 조언</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user