1. FastAPI 기초와 RESTful API 설계
1.1. Rest API
웹 서비스에서 자원을 요청하고, HTTP 메서드를 활용해 자원에 대한 행동을 표현하는 표준 인터페이스
1.2. FAST API
FAST API는 파이썬의 API를 빌드하기 위한 웹 프레임워크이며, NodeJS와 GO와 대등할 정도로 성능이 매우 높다. 현재로서는 사용가능한 파이썬 프레임 워크중에 가장 빠르다.
FastAPI
FastAPI framework, high performance, easy to learn, fast to code, ready for production
fastapi.tiangolo.com
1.2. Uvicorn
FastAPI는 Python으로 작성된 고성능 웹 프레임워크로, ASGI(Application Server Gateway Interface)를 사용합니다. FastAPI는 자체적으로 서버를 실행하는 기능이 없기 때문에, ASGI 서버를 실행시킬 수 있는 도구가 필요하다. 이때 사용하는 대표적인 도구가 바로 Uvicorn이다.
1.2.2. FastAPI는 ASGI 표준을 따르므로 ASGI 서버가 필요
1.2.2. uvloop(이벤트 루프)와 httptools(HTTP 프로토콜 구현) 같은 고성능 라이브러리를 사용하여 빠른 속도를 제공
1.2.3. FastAPI의 비동기 기능을 활용하려면 비동기 I/O를 지원하는 서버가 필요합니다. Uvicorn은 이를 완벽히 지원
1.2.4. Uvicorn을 사용하면 FastAPI 애플리케이션을 손쉽게 실행할 수 있습니다. 명령어 하나로 개발 서버를 실행하고, 프로덕션 환경에서도 배포가 용이.
1.2.5. fasAPI와 Uvicorn 설치
pip install fastapi uvicorn[standard]
2. FastAPI와 Langchain RAG 서비스 연동
(코드 설명은 주석에 달았음)
import os
from dotenv import load_dotenv # .env 파일로부터 값을 불러올 수 있다.
# FastAPI
from fastapi import FastAPI, HTTPException # FastAPI 라이브러리 (HTTP 에러도 추가)
from fastapi.staticfiles import StaticFiles # 사용자에게 정적 파일을 제공
from fastapi.responses import FileResponse # 파일 응답
from fastapi.middleware.cors import CORSMiddleware # 접근 가능한 사용자를 설정
# LangChain
from langchain_openai import OpenAIEmbeddings, ChatOpenAI # OpenAI 관련
from langchain_community.vectorstores import FAISS # Vector store
from langchain_community.document_loaders import WebBaseLoader # 웹 링크 로더
from langchain_core.output_parsers import StrOutputParser # 답변 텍스트만 보냄
from langchain_core.runnables import RunnablePassthrough # LCEL 형식
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
) # 텍스트 청크 변환기
# Load environment variables
load_dotenv()
# FastAPI 설정
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"*"
], # 개발 중에는 모든 오리진을 허용. 프로덕션 환경에서는 구체적으로 지정 필요
allow_credentials=True,
allow_methods=["*"], # 모든 HTTP 메서드 허용
allow_headers=["*"], # 모든 HTTP 헤더 허용
)
# 정적 파일 제공 설정
app.mount("/static", StaticFiles(directory="static"), name="static")
# OpenAI API 키는 .env 파일에서 관리.
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY not found in environment variables")
# 임베딩 모델과 LLM 모델
embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)
llm = ChatOpenAI(openai_api_key=openai_api_key, model="gpt-4o-mini")
# 링크와 사용자 입력 값을 Str형식으로 받아오기
class URLInput(BaseModel):
url: str
class QueryInput(BaseModel):
query: str
# 전역 변수로 RAG 체인을 관리
rag_chain = None
# 사용자가 루트 디렉토리(/)로 접근하면 static의 index.html 정적 파일을 비동기로 보낸다. (여러 사용자가 접근해도 호출가능)
@app.get("/")
async def root():
return FileResponse("static/index.html")
# 프로그램 실행시 작동하는 API
@app.post("/process_url")
async def process_url(url_input: URLInput):
global rag_chain
# 체인구축
try:
loader = WebBaseLoader(
web_paths=(url_input.url,),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
# url의 클래스
class_=("newsct_article _article_body",)
)
),
)
docs = loader.load()
# 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200
)
splits = text_splitter.split_documents(docs)
# 스플릿 된 문서들을 벡터 스토어에 임베딩 해서 저장
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
# 벡터 임베딩의 추출기
retriever = vectorstore.as_retriever()
prompt = hub.pull("rlm/rag-prompt")
# 다큐먼트 객체들이 갖고 있는 페이지 컨텐츠들이 하나의 텍스트로 붙는다.
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | fromat_docs,
"question": RunnablePassthrough(),
}
| prompt
| llm
| StrOutputParser()
)
return {"message": "URL processed successfully"}
# 에러 처리
except Exception as e:
error_trace = traceback.format_exc()
print(f"Error in process_url: {error_trace}")
raise HTTPException(status_code=500, detail=str(e))
# 사용자의 질문의 답변을 하는 API
@app.post("/query")
async def query(query_input: QueryInput):
global rag_chain
if not rag_chain:
raise HTTPException(status_code=400, detail="Please process a URL first")
try:
result = rag_chain.invoke({"input": query_input.query})
return {"answer": result["answer"]}
except Exception as e:
error_trace = traceback.format_exc()
print(f"Error in query: {error_trace}")
raise HTTPException(status_code=500, detail=str(e))
# 파이썬 파일이 실행됐을때 uvicorn 갖고와라
if __name__ == "__main__":
import uvicorn
# uvicorn에서 해당 app을 실행하는데, 호스트 ip와 포트번호 설정
uvicorn.run(app, host="0.0.0.0", port=8000)
app 실행하는 CLI 명령어
uvicorn main:app --reload
<결과>
2.2 설치 라이브러리 모음
python -m pip install --upgrade pip
pip install streamlit
pip install langchain_openai
pip install langchain
pip install -U langchain-community
pip install PyPDF2
pip install chromadb
pip install beautifulsoup4
pip install faiss-cpu
pip install fastapi uvicorn[standard]
3. HTML/CSS 기초: 챗봇 인터페이 스 구현
3.1. 파일 구조
3.2. 앱 실행 결과
3.3. main.py
import os
from dotenv import load_dotenv # .env 파일로부터 값을 불러올 수 있다.
# FastAPI
from fastapi import FastAPI, HTTPException # FastAPI 라이브러리 (HTTP 에러도 추가)
from fastapi.staticfiles import StaticFiles # 사용자에게 정적 파일을 제공
from fastapi.responses import FileResponse # 파일 응답
from fastapi.middleware.cors import CORSMiddleware # 접근 가능한 사용자를 설정
import bs4
import traceback
from pydantic import BaseModel
# LangChain
from langchain_openai import OpenAIEmbeddings, ChatOpenAI # OpenAI 관련
from langchain_community.vectorstores import FAISS # Vector store
from langchain_community.document_loaders import WebBaseLoader # 웹 링크 로더
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
) # 텍스트 청크 변환기
from langchain.schema import Document # Document 객체 가져오기
from langchain.chains import RetrievalQA # Retrieval QA 체인 가져오기
# Load environment variables
load_dotenv()
# FastAPI 설정
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"*"
], # 개발 중에는 모든 오리진을 허용. 프로덕션 환경에서는 구체적으로 지정 필요
allow_credentials=True,
allow_methods=["*"], # 모든 HTTP 메서드 허용
allow_headers=["*"], # 모든 HTTP 헤더 허용
)
# 정적 파일 제공 설정
app.mount("/static", StaticFiles(directory="static"), name="static")
# OpenAI API 키는 .env 파일에서 관리
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY not found in environment variables")
# 임베딩 모델과 LLM 모델
embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)
llm = ChatOpenAI(openai_api_key=openai_api_key, model="gpt-4o-mini")
# 링크와 사용자 입력 값을 Str 형식으로 받아오기
class URLInput(BaseModel):
url: str
class QueryInput(BaseModel):
query: str
# 전역 변수로 RAG 체인을 관리
rag_chain = None
# 사용자가 루트 디렉토리(/)로 접근하면 static의 index.html 정적 파일을 비동기로 보냄
@app.get("/")
async def root():
return FileResponse("static/index.html")
@app.post("/process_url")
async def process_url(url_input: URLInput):
global rag_chain
try:
# 웹 페이지 로드
loader = WebBaseLoader(
web_paths=(url_input.url,),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
# URL의 클래스 이름 (수정 필요시 여기에 반영)
class_=("editor-p",)
)
),
)
docs = loader.load()
# 텍스트 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200
)
splits = text_splitter.split_documents(docs)
# 텍스트를 Document 객체로 변환
splits = [
Document(page_content=chunk.page_content, metadata=chunk.metadata)
for chunk in splits
if isinstance(chunk.page_content, str)
]
# 벡터 스토어에 임베딩 저장
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
# 벡터 임베딩의 추출기
retriever = vectorstore.as_retriever()
# Retrieval QA 체인 생성
rag_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # 필요한 체인 타입 설정
retriever=retriever,
return_source_documents=True, # 원본 문서 반환 여부
)
return {"message": "URL processed successfully"}
except Exception as e:
# 에러 처리 및 디버깅 정보 출력
error_trace = traceback.format_exc()
print(f"Error in process_url: {error_trace}")
raise HTTPException(status_code=500, detail=str(e))
# 사용자의 질문의 답변을 하는 API
@app.post("/query")
async def query(query_input: QueryInput):
global rag_chain
if not rag_chain:
raise HTTPException(status_code=400, detail="Please process a URL first")
try:
# RetrievalQA 체인 호출
result = rag_chain({"query": query_input.query})
# 응답 반환
return {
"answer": result["result"], # 응답 텍스트
"source_documents": result.get("source_documents", []), # 원본 문서 (옵션)
}
except Exception as e:
error_trace = traceback.format_exc()
print(f"Error in query: {error_trace}")
raise HTTPException(status_code=500, detail=str(e))
# 파이썬 파일이 실행됐을 때 uvicorn 실행
if __name__ == "__main__":
import uvicorn
# uvicorn에서 해당 app을 실행하는데, 호스트 IP와 포트번호 설정
uvicorn.run(app, host="0.0.0.0", port=8000)
4. JavaScript 기초: 비동기 통신 및 채팅 기능 구현
4.1. javascript
// 각각의 html의 태그를 변수로 저장
const chatContainer = document.getElementById('chat-container');
const urlInput = document.getElementById('url-input');
const queryInput = document.getElementById('query-input');
// url을 변수로 갖고와서 프로세스 처리-> 벡터 스토어에 임베딩 해서 정하는 과정
async function processURL() {
const url = urlInput.value;
if (!url) return;
addMessage('System', 'Processing URL...', 'system');
try {
const response = await fetch('/process_url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to process URL');
}
const data = await response.json();
addMessage('System', data.message, 'system');
} catch (error) {
console.error('Error in processURL:', error);
addMessage('Error', `Failed to process URL: ${error.message}`, 'error');
}
}
// 사용자의 질문으로 채팅 구현
async function askQuestion() {
const query = queryInput.value;
if (!query) return;
addMessage('You', query, 'user');
queryInput.value = '';
try {
const response = await fetch('/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
credentials: 'same-origin'
});
const data = await response.json();
addMessage('Bot', data.answer, 'bot');
} catch (error) {
console.error('Error in askQuestion:', error);
addMessage('Error', `Failed to get answer: ${error.message}`, 'error');
}
}
// 어떤 사람이 보냈는지 메시지에 표시하면서 메시지를 추가
function addMessage(sender, message, className) {
const messageElement = document.createElement('div');
messageElement.className = `message ${className}`;
messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
chatContainer.appendChild(messageElement);
chatContainer.scrollTop = chatContainer.scrollHeight; // 맨아래에 추가
}
// Enter 키로 질문 제출
queryInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
askQuestion();
}
});
4.2. Html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAG Chatbot</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="container">
<h1>RAG Chatbot</h1>
<div class="url-input-container">
<input type="text" id="url-input" placeholder="Enter website URL">
<button onclick="processURL()">Process URL</button>
</div>
<div id="chat-container"></div>
<div class="query-input-container">
<input type="text" id="query-input" placeholder="Ask a question">
<button onclick="askQuestion()">Ask</button>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>
4.3. CSS
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
#chat-container {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
height: 300px;
overflow-y: auto;
margin-bottom: 10px;
background-color: #fff;
}
.url-input-container, .query-input-container {
display: flex;
margin-bottom: 10px;
}
input[type="text"] {
flex-grow: 1;
padding: 5px;
margin-right: 5px;
}
button {
padding: 5px 10px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
.message {
margin-bottom: 10px;
padding: 5px;
border-radius: 5px;
}
.message.user {
background-color: #e6f2ff;
}
.message.bot {
background-color: #f0f0f0;
}
.message.system {
background-color: #d4edda;
color: #155724;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
}
4.4. 해당 웹사이트
https://www.hankookilbo.com/News/Read/A2024072614260004810?did=NA
4.5. 실행결과
4.6. 백엔드 로그
4.7. 포스트맨으로 확인
1. 프로세스 url연결
2. 쿼리질문
5. 스프링 부트와 비동기 통신
5.1. 포스트맨 확인
프론트에서 직접 AI로 통신하지 않고, 스프링 서버랑 통신을 위해 아래 코드를 작성했다.
스프링 부트의 RestTemplate으로 통신 가능하며 응답으로 "answer"만 잘 전달 받을 수 있음을 확인했다. (이때 시큐리티 설정시 반드시 토큰값을 넘겨줘야한다. )
5.2. 서비스 코드
package com.pado.inflow.chatbot.service;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.pado.inflow.chatbot.dto.AIRequestDTO;
import com.pado.inflow.chatbot.dto.AIResponseDTO;
@Service
public class AIService {
private final RestTemplate restTemplate;
private static final String AI_SERVER_URL = "http://localhost:8000/query"; // AI 서버 주소
public AIService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public AIResponseDTO communicateWithAI(AIRequestDTO chatbotRequest) {
try {
// HTTP Header 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// HTTP Body 설정
HttpEntity<AIRequestDTO> request = new HttpEntity<>(chatbotRequest, headers);
// AI 서버 호출
ResponseEntity<AIResponseDTO> response = restTemplate.postForEntity(
AI_SERVER_URL,
request,
AIResponseDTO.class
);
// 응답 반환
return response.getBody();
} catch (Exception e) {
throw new RuntimeException("AI 서버와 통신 중 오류 발생: " + e.getMessage());
}
}
}