fastAPI 심화
- Chart.js - pdf, csv 파일 업로드 후 데이터 정제하여 llm으로 처리 후 결과 도출 - sqlite로 데이터 저장 - ORM - SQLAlchemy
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="~/Source/project/CREDIT_APP/.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CsvFileAttributes">
|
||||||
|
<option name="attributeMap">
|
||||||
|
<map>
|
||||||
|
<entry key="/transactions.csv">
|
||||||
|
<value>
|
||||||
|
<Attribute>
|
||||||
|
<option name="separator" value="," />
|
||||||
|
</Attribute>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="history" uuid="d34d9830-06d9-4e92-bd10-84567e5a4809">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/history.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
<data-source source="LOCAL" name="users" uuid="2d5d9d87-b30f-47d4-9d9d-61542cb64ea0">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/users.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="db-forest-configuration">
|
||||||
|
<data version="2">.
|
||||||
|
----------------------------------------
|
||||||
|
1:0:d34d9830-06d9-4e92-bd10-84567e5a4809
|
||||||
|
2:0:2d5d9d87-b30f-47d4-9d9d-61542cb64ea0
|
||||||
|
.</data>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/CREDIT_APP.iml" filepath="$PROJECT_DIR$/.idea/CREDIT_APP.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PyToolsState">
|
||||||
|
<option name="tools">
|
||||||
|
<map>
|
||||||
|
<entry key="black">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="pyrefly">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="pyright">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="ruff">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="ty">
|
||||||
|
<value>
|
||||||
|
<ToolEntry />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -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>
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from backend.services.db_service import init_db
|
||||||
|
from backend.routers.api_router import router as api_router
|
||||||
|
from backend.routers.page_router import router as page_router
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
print("서버 시작")
|
||||||
|
init_db()
|
||||||
|
yield
|
||||||
|
print("DB 초기화")
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan, title="Insights Advisor", version="1.0")
|
||||||
|
# app = FastAPI()
|
||||||
|
|
||||||
|
# static 폴더 지정
|
||||||
|
app.mount("/static", StaticFiles(directory="backend/static"), name="static")
|
||||||
|
|
||||||
|
# 라우터 등록
|
||||||
|
app.include_router(page_router)
|
||||||
|
app.include_router(api_router)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### ORM(Object Relational Mapping)\n",
|
||||||
|
"- 데이터베이스 테이블을 파이썬의 클래스로 매핑\n",
|
||||||
|
"- 컬럼 == 속성\n",
|
||||||
|
"- SQLALchemy 라이브러리가 ORM 지원"
|
||||||
|
],
|
||||||
|
"id": "11f3431eb6bf7a12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"id": "initial_id",
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": true,
|
||||||
|
"ExecuteTime": {
|
||||||
|
"end_time": "2026-06-16T08:48:53.375278031Z",
|
||||||
|
"start_time": "2026-06-16T08:48:51.509377417Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "!pip install sqlalchemy",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Requirement already satisfied: sqlalchemy in ./.venv/lib/python3.12/site-packages (2.0.51)\n",
|
||||||
|
"Requirement already satisfied: greenlet>=1 in ./.venv/lib/python3.12/site-packages (from sqlalchemy) (3.5.1)\n",
|
||||||
|
"Requirement already satisfied: typing-extensions>=4.6.0 in ./.venv/lib/python3.12/site-packages (from sqlalchemy) (4.15.0)\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"execution_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"ExecuteTime": {
|
||||||
|
"end_time": "2026-06-16T08:57:27.860045705Z",
|
||||||
|
"start_time": "2026-06-16T08:57:27.561065535Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"# Users 테이블 (id, name, email)\n",
|
||||||
|
"# create table users()\n",
|
||||||
|
"from sqlalchemy import Column, Integer, String, create_engine\n",
|
||||||
|
"from sqlalchemy.orm import declarative_base, sessionmaker\n",
|
||||||
|
"\n",
|
||||||
|
"# 데이터베이스 연결\n",
|
||||||
|
"engine = create_engine('sqlite:///db/users.db')\n",
|
||||||
|
"# engine = create_engine('sqlite:///users.db')\n",
|
||||||
|
"\n",
|
||||||
|
"# 모든 모델 크래스의 부모 클래스가 될 Base 객체 생성\n",
|
||||||
|
"Base = declarative_base()\n",
|
||||||
|
"class User(Base):\n",
|
||||||
|
" __tablename__ = 'users'\n",
|
||||||
|
"\n",
|
||||||
|
" id = Column(Integer, primary_key=True)\n",
|
||||||
|
" name = Column(String)\n",
|
||||||
|
" email = Column(String, unique=True)\n",
|
||||||
|
"\n",
|
||||||
|
" def __str__(self):\n",
|
||||||
|
" return f\"<User(name='{self.name}', email='{self.email}')>\"\n",
|
||||||
|
"\n",
|
||||||
|
"# 테이블 생성\n",
|
||||||
|
"Base.metadata.create_all(engine)"
|
||||||
|
],
|
||||||
|
"id": "7a41f863de1efd48",
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"cell_type": "code",
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"source": "",
|
||||||
|
"id": "de1ebcd8f162fd42"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython2",
|
||||||
|
"version": "2.7.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
date,category,merchant,amount
|
||||||
|
2023-12-05,Online Shopping,E-Store,120
|
||||||
|
2023-12-02,Utilities,Internet Bill,80
|
||||||
|
2023-11-29,Dining,Restaurant A,85
|
||||||
|
2023-11-22,Groceries,Supermarket,60
|
||||||
|
2023-11-15,Travel,Airline Ticket,350
|
||||||
|
2023-11-12,Utilities,Electricity Bill,75
|
||||||
|
2023-11-08,Online Shopping,Online Marketplace,45
|
||||||
|
2023-11-05,Entertainment,Cinema,30
|
||||||
|
2023-10-20,Dining,Cafe,20
|
||||||
|
2023-10-15,Travel,Hotel Booking,200
|
||||||
|
2023-10-10,Groceries,Local Market,50
|
||||||
|
2023-10-06,Utilities,Water Bill,40
|
||||||
|
2023-09-25,Online Shopping,Tech Store,150
|
||||||
|
2023-09-15,Travel,Taxi Service,25
|
||||||
|
2023-09-22,Dining,Restaurant B,90
|
||||||
|
2023-09-10,Utilities,Electricity Bill,100
|
||||||
|
2023-09-05,Groceries,Grocery Store,80
|
||||||
|
2023-08-18,Utilities,Gas Bill,60
|
||||||
|
2023-08-12,Entertainment,Streaming Service,15
|
||||||
|
2023-08-06,Online Shopping,E-Store,300
|
||||||
|
2023-07-30,Travel,Train Ticket,40
|
||||||
|
2023-07-10,Dining,Fast Food,25
|
||||||
|
2023-07-03,Utilities,Water Bill,50
|
||||||
|
Generated
-13
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="PortForwardingSettings">
|
|
||||||
<ports>
|
|
||||||
<entry key="8000">
|
|
||||||
<ForwardedPortInfo>
|
|
||||||
<option name="hostPort" value="8000" />
|
|
||||||
<option name="readOnly" value="false" />
|
|
||||||
</ForwardedPortInfo>
|
|
||||||
</entry>
|
|
||||||
</ports>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/rag_app.iml" filepath="$PROJECT_DIR$/.idea/rag_app.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/rag-app.iml" filepath="$PROJECT_DIR$/.idea/rag-app.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PySourceRootDetectionService">
|
||||||
|
<option name="sourcePathsSet">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
from fastapi import APIRouter, Request, UploadFile
|
from fastapi import APIRouter, Request, UploadFile
|
||||||
from backend.services.llm_service import question_and_answer
|
from backend.services.llm_service import question_and_answer
|
||||||
from backend.schemas.basic_schema import QuestionRequest
|
from backend.schemas.basic_schema import QuestionRequest
|
||||||
from backend.services.rag_service import upload_document
|
from backend.services.rag_service import upload_document, rag_chat, rag_chat_stream
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
# http://127.0.0.1:8000/api/question
|
# http://127.0.0.1:8000/api/question
|
||||||
@router.post("/question")
|
@router.post("/question")
|
||||||
async def question(req:QuestionRequest):
|
async def question(req: QuestionRequest):
|
||||||
answer = question_and_answer(req.question)
|
answer = question_and_answer(req.question)
|
||||||
|
|
||||||
return {"message" : answer}
|
return {"message" : answer}
|
||||||
|
|
||||||
# http://127.0.0.1:8000/api/rag/upload
|
# http://127.0.0.1:8000/api/rag/upload
|
||||||
@router.post("/rag/upload")
|
@router.post("/rag/upload")
|
||||||
async def fileUpload(file:UploadFile):
|
async def file_Upload(file: UploadFile):
|
||||||
# 서비스 호출
|
# 서비스 호출
|
||||||
return upload_document(file)
|
return upload_document(file)
|
||||||
|
|
||||||
|
|
||||||
# http://127.0.0.1:8000/api/rag/question
|
# http://127.0.0.1:8000/api/rag/question
|
||||||
|
|
||||||
@router.post("/rag/question")
|
@router.post("/rag/question")
|
||||||
async def question():
|
async def question(req: QuestionRequest):
|
||||||
pass
|
answer = rag_chat(req.question)
|
||||||
|
|
||||||
|
return {"message" : answer}
|
||||||
|
|
||||||
|
@router.post("/rag/question/stream")
|
||||||
|
async def question_stream(req: QuestionRequest):
|
||||||
|
answer = rag_chat_stream(req.question)
|
||||||
|
|
||||||
|
return StreamingResponse(rag_chat_stream(req.question), media_type="text/plain")
|
||||||
@@ -1,6 +1,90 @@
|
|||||||
# pdf 업로드 => 분할 => 인덱스 생성
|
import os
|
||||||
def upload_document(file):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
from langchain_community.document_loaders import PyPDFLoader
|
||||||
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||||
|
from langchain_community.vectorstores import FAISS
|
||||||
|
from langchain_core.runnables import RunnablePassthrough
|
||||||
|
from langchain_core.output_parsers import StrOutputParser
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate
|
||||||
|
|
||||||
|
from backend.ai.embedding import watson_embedding
|
||||||
|
from backend.ai.llm import watson_llm
|
||||||
|
|
||||||
|
UPLOAD_PATH = "uploads"
|
||||||
|
|
||||||
|
def upload_document(file):
|
||||||
|
# file 저장
|
||||||
|
file_path = os.path.join(UPLOAD_PATH, file.filename)
|
||||||
|
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(file.file.read())
|
||||||
|
|
||||||
|
# pdf 업로드 => 분할 => 인덱스 생성
|
||||||
|
# pdf 로드
|
||||||
|
loader = PyPDFLoader(file_path)
|
||||||
|
docs = loader.load()
|
||||||
|
|
||||||
|
splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=30)
|
||||||
|
chunks = splitter.split_documents(docs)
|
||||||
|
|
||||||
|
faiss_store = FAISS.from_documents(documents=chunks, embedding=watson_embedding)
|
||||||
|
faiss_store.save_local("./db/vectorstore")
|
||||||
|
|
||||||
|
return {"message": "업로드 성공"}
|
||||||
|
|
||||||
# 질문 => 유사도 검색 => 문서 => llm 답변 생성
|
# 질문 => 유사도 검색 => 문서 => llm 답변 생성
|
||||||
|
def rag_chat(question: str):
|
||||||
|
|
||||||
|
faiss_store = FAISS.load_local("./db/vectorstore", watson_embedding, allow_dangerous_deserialization=True)
|
||||||
|
|
||||||
|
retriever = faiss_store.as_retriever(search_kwargs={"k": 3})
|
||||||
|
|
||||||
|
### LLM
|
||||||
|
# 1. prompt
|
||||||
|
message = """\
|
||||||
|
당신은 PDF 기반 RAG AI 입니다.
|
||||||
|
다음 문서를 참고해서 질문에 답변하세요.
|
||||||
|
|
||||||
|
문서:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
질문:
|
||||||
|
{question}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rag_prompt = ChatPromptTemplate.from_template(message)
|
||||||
|
# 2. chain
|
||||||
|
chain = {"context": retriever, "question": RunnablePassthrough()} | rag_prompt | watson_llm | StrOutputParser()
|
||||||
|
|
||||||
|
# 3. answer
|
||||||
|
answer = chain.invoke(question)
|
||||||
|
return answer
|
||||||
|
|
||||||
|
async def rag_chat_stream(question: str):
|
||||||
|
|
||||||
|
faiss_store = FAISS.load_local("./db/vectorstore", watson_embedding, allow_dangerous_deserialization=True)
|
||||||
|
|
||||||
|
retriever = faiss_store.as_retriever(search_kwargs={"k": 3})
|
||||||
|
|
||||||
|
### LLM
|
||||||
|
# 1. prompt
|
||||||
|
message = """\
|
||||||
|
당신은 PDF 기반 RAG AI 입니다.
|
||||||
|
다음 문서를 참고해서 질문에 답변하세요.
|
||||||
|
|
||||||
|
문서:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
질문:
|
||||||
|
{question}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rag_prompt = ChatPromptTemplate.from_template(message)
|
||||||
|
# 2. chain
|
||||||
|
chain = {"context": retriever, "question": RunnablePassthrough()} | rag_prompt | watson_llm | StrOutputParser()
|
||||||
|
|
||||||
|
# 3. answer
|
||||||
|
# chain.stream() : 동기방식(요청 => 응답을 할 떄까지 기다리는 방식)
|
||||||
|
# chain.astream() : 비동기방식(다른 일 처리 가능)
|
||||||
|
async for chunk in chain.astream(question):
|
||||||
|
yield chunk
|
||||||
@@ -17,28 +17,58 @@ async function ask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 파일 업로드
|
// 파일 업로드
|
||||||
document.querySelector("#uploadBtn").addEventListener("click", uploadFile)
|
document.querySelector("#uploadBtn").addEventListener("click",uploadFile)
|
||||||
async function uploadFile()
|
async function uploadFile() {
|
||||||
{
|
const fileInput = document.querySelector("#file")
|
||||||
const fileInput = document.querySelector("#file");
|
|
||||||
// 첨부파일 정보 가져오기
|
// 첨부파일 정보 가져오기
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0]
|
||||||
|
|
||||||
if(!file)
|
if(!file){
|
||||||
{
|
alert('파일을 선택하세요');
|
||||||
alert("파일을 선택해주세요.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// form 만들어 전송
|
// form 만들어 전송
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append("file", file)
|
formData.append("file",file);
|
||||||
|
|
||||||
const response = await fetch("/api/reg/question", {
|
const response = await fetch("/api/rag/upload",{
|
||||||
method: "POST",
|
method:"POST",
|
||||||
body: formData
|
body:formData
|
||||||
})
|
})
|
||||||
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||||
const answer = await response.json()
|
const answer = await response.json()
|
||||||
document.querySelector('#answer').textContent = answer.message
|
document.querySelector('#result').textContent = answer.message
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#askBtn").addEventListener("click", rag_ask)
|
||||||
|
async function rag_ask() {
|
||||||
|
// 사용자가 질문 입력 시 질문을 서버로 전송
|
||||||
|
const question = document.querySelector('#question').value
|
||||||
|
|
||||||
|
const response = await fetch("/api/rag/question", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({question : question})
|
||||||
|
})
|
||||||
|
// 전송 후 answer 도착 시 answer 화면에 보여주기
|
||||||
|
// const answer = await response.json()
|
||||||
|
// document.querySelector('#answer_result').textContent = answer.message
|
||||||
|
|
||||||
|
// stream 방식
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let answer = "";
|
||||||
|
|
||||||
|
while(true){
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
answer += decoder.decode(value);
|
||||||
|
document.querySelector('#answer_result').textContent = answer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="result"></div>
|
<div id="result"></div>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<input type="text" name="question" id="question">
|
||||||
|
<button id="askBtn">질문</button>
|
||||||
|
</div>
|
||||||
|
<div id="answer_result"></div>
|
||||||
<script src="{{ url_for('static', path='js/index.js')}}"></script>
|
<script src="{{ url_for('static', path='js/index.js')}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
Binary file not shown.
@@ -2,4 +2,6 @@
|
|||||||
name = "rag-app"
|
name = "rag-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"recursivecharactertextsplitter>=0.1.0",
|
||||||
|
]
|
||||||
|
|||||||
Binary file not shown.
Generated
+992
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user