Initial commit
This commit is contained in:
2
.env.chatbot
Normal file
2
.env.chatbot
Normal file
@@ -0,0 +1,2 @@
|
||||
# Replace with your actual Google API key
|
||||
GOOGLE_API_KEY="AIzaSyD37Fp00b_i2DIywwtQu39w0RhkGAJO4YM"
|
||||
32
.gitattributes
vendored
Normal file
32
.gitattributes
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||
*.arrow filter=lfs diff=lfs merge=lfs -text
|
||||
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.ftz filter=lfs diff=lfs merge=lfs -text
|
||||
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||
*.h5 filter=lfs diff=lfs merge=lfs -text
|
||||
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
||||
*.model filter=lfs diff=lfs merge=lfs -text
|
||||
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||
*.ot filter=lfs diff=lfs merge=lfs -text
|
||||
*.parquet filter=lfs diff=lfs merge=lfs -text
|
||||
*.pickle filter=lfs diff=lfs merge=lfs -text
|
||||
*.pkl filter=lfs diff=lfs merge=lfs -text
|
||||
*.pb filter=lfs diff=lfs merge=lfs -text
|
||||
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||
*.pth filter=lfs diff=lfs merge=lfs -text
|
||||
*.rar filter=lfs diff=lfs merge=lfs -text
|
||||
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
||||
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
||||
*.tflite filter=lfs diff=lfs merge=lfs -text
|
||||
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||
*.wasm filter=lfs diff=lfs merge=lfs -text
|
||||
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file into the container at /app
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install any needed packages specified in requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the rest of the application's code into the container at /app
|
||||
COPY . .
|
||||
|
||||
# Make port 8501 available to the world outside this container
|
||||
EXPOSE 8501
|
||||
|
||||
# Define environment variable
|
||||
ENV GOOGLE_API_KEY=""
|
||||
|
||||
# Run main.py when the container launches
|
||||
CMD ["streamlit", "run", "main.py"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lectom
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 🤖 OpenAPI 명세 조회 챗봇 (LLM-Gateway Spec Chatbot)
|
||||
|
||||
이 프로젝트는 특정 URL에서 제공하는 OpenAPI 명세(Specification) 파일을 기반으로 사용자의 질문에 답변하는 Streamlit 기반의 챗봇 애플리케이션입니다. Gemini AI 모델을 활용하여 자연어 질문을 이해하고, API 명세에 근거한 정확한 정보를 한국어로 제공합니다.
|
||||
|
||||
전체 애플리케이션은 Docker 컨테이너 환경에서 실행되도록 구성되어 있어, 간편하게 설치하고 실행할 수 있습니다.
|
||||
|
||||
## ✨ 주요 기능
|
||||
|
||||
- **OpenAPI 명세 기반 답변**: 로컬에 저장된 `api_spec.json` 파일을 분석하여 API의 엔드포인트, 파라미터, 요약 정보 등에 대해 답변합니다.
|
||||
- **자연어 질의응답**: Google의 Gemini 모델을 통해 사용자의 자연어 질문을 이해하고 지능적인 답변을 생성합니다.
|
||||
- **동적 명세 새로고침**: UI의 버튼 클릭 한 번으로 최신 API 명세를 원격 URL에서 다시 가져와 앱에 반영할 수 있습니다.
|
||||
- **한국어 지원**: 모든 답변은 한국어로 제공됩니다.
|
||||
- **직관적인 UI**: Streamlit을 사용하여 사용하기 쉬운 웹 기반 채팅 인터페이스를 제공하며, <20><><EFBFBD>고 및 커스텀 스타일이 적용되어 있습니다.
|
||||
- **컨테이너화**: Docker 및 Docker Compose를 사용하여 개발 환경에 구애받지 않고 일관된 실행 환경을 제공합니다.
|
||||
|
||||
## 🛠️ 기술 스택
|
||||
|
||||
- **언어**: Python 3.9
|
||||
- **프레임워크**: Streamlit
|
||||
- **AI 모델**: Google Gemini 1.5 Flash
|
||||
- **컨테이너**: Docker, Docker Compose
|
||||
- **주요 라이브러리**: `google-generativeai`, `requests`, `pandas`
|
||||
|
||||
## 📂 디렉토리 구조
|
||||
|
||||
```
|
||||
chatbot_app/
|
||||
├── static/
|
||||
│ ├── api_spec.json # API 명세 파일
|
||||
│ └── logo.png # UI에 표시될 로고 이미지
|
||||
├── .env.chatbot # 환경 변수 파일 (API 키 저장)
|
||||
├── docker-compose.yml # Docker Compose 설정 파일
|
||||
├── Dockerfile # Docker 이미지 빌드 파일
|
||||
├── main.py # Streamlit 애플리케이션 메인 코드
|
||||
├── README.md # 프로젝트 설명 파일 (현재 파일)
|
||||
└── requirements.txt # Python 의존성 목록
|
||||
```
|
||||
|
||||
## 🚀 설치 및 실행 방법
|
||||
|
||||
### 사전 준비 사항
|
||||
|
||||
- [Docker](https://www.docker.com/get-started)가 설치되어 있어야 합니다.
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)가 설치되어 있어야 합니다. (최신 버전의 Docker Desktop에는 기본 포함)
|
||||
|
||||
### 설치 절차
|
||||
|
||||
1. **프로젝트 준비**:
|
||||
이 `chatbot_app` 디렉토리를 준비합니다.
|
||||
|
||||
2. **정적 파일 배치**:
|
||||
`chatbot_app/static/` 디렉토리 안에 다음 두 파일을 위치시킵니다.
|
||||
- `api_spec.json`: 조회할 대상의 OpenAPI 명세 파일
|
||||
- `logo.png`: UI 사이드바에 표시할 로고 이미지
|
||||
|
||||
3. **환경 변수 설정**:
|
||||
`chatbot_app/.env.chatbot` 파일을 열고, `YOUR_GEMINI_API_KEY` 부분을 실제 발급받은 Google Gemini API 키로 교체합니다.
|
||||
```env
|
||||
# YOUR_GEMINI_API_KEY를 실제 키로 변경하세요.
|
||||
GOOGLE_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
|
||||
4. **애플리케이션 빌드 및 실행**:
|
||||
터미널에서 `chatbot_app` 디렉토리로 이동한 후, 다음 명령어를 실행합니다.
|
||||
```bash
|
||||
docker-compose up --build -d
|
||||
```
|
||||
- `--build`: 이미지를 새로 빌드합니다. (코드 변경 시 필요)
|
||||
- `-d`: 컨테이너를 백그라운드에서 실행합니다.
|
||||
|
||||
5. **챗봇 접속**:
|
||||
빌드가 완료되고 컨테이너가 실행되면, 웹 브라우저를 열고 다음 주소로 접속합니다.
|
||||
- **URL**: `http://localhost:8501`
|
||||
|
||||
## 💡 사용 방법
|
||||
|
||||
- **질문하기**: 화면 하단의 입력창에 API 명세와 관련된 질문을 입력하고 Enter 키를 누릅니다.
|
||||
- **API 명세 새로고침**: 사이드바의 '🔄 API 명세 새로고침' 버튼을 클릭하면 `API_SPEC_URL`에 지정된 주소에서 최신 명세를 다시 가져옵니다.
|
||||
- **API 상세정보 확인**: 사이드바의 '📘 API 상세정보 보기'를 펼치면 API의 제목, 버전 및 전체 엔드포인트 목록을 확인할 수 있습니다.
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
chatbot:
|
||||
build: .
|
||||
container_name: pgn_spec_chatbot
|
||||
ports:
|
||||
- "8501:8501"
|
||||
env_file:
|
||||
- .env.chatbot
|
||||
volumes:
|
||||
- .:/app
|
||||
197
main.py
Normal file
197
main.py
Normal file
@@ -0,0 +1,197 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import google.generativeai as genai
|
||||
import pandas as pd
|
||||
import requests
|
||||
import streamlit as st
|
||||
|
||||
# --- 설정 ---
|
||||
SPEC_FILE = "static/api_spec.json"
|
||||
LOGO_FILE = "static/logo.png"
|
||||
API_SPEC_URL = "http://172.16.10.176:8888/openapi.json"
|
||||
|
||||
|
||||
# --- 도우미 함수 ---
|
||||
def load_api_spec():
|
||||
"""로컬 파일에서 API 명세를 불러옵니다."""
|
||||
try:
|
||||
with open(SPEC_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
st.error(f"API 명세 파일을 불러오는 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def fetch_and_save_spec():
|
||||
"""URL에서 최신 API 명세를 가져와 로컬에 저장합니다."""
|
||||
try:
|
||||
response = requests.get(API_SPEC_URL)
|
||||
response.raise_for_status() # 잘못된 상태 코드에 대해 예외를 발생시킵니다
|
||||
spec_data = response.json()
|
||||
with open(SPEC_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(spec_data, f, indent=2, ensure_ascii=False)
|
||||
st.success(f"성공적으로 API 명세를 가져와 업데이트했습니다: {API_SPEC_URL}")
|
||||
return spec_data
|
||||
except requests.exceptions.RequestException as e:
|
||||
st.error(f"API 명세를 가져오는 데 실패했습니다: {e}")
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
st.error("응답에서 JSON을 파싱하는 데 실패했습니다.")
|
||||
return None
|
||||
|
||||
|
||||
# --- Gemini AI 설정 ---
|
||||
try:
|
||||
api_key = os.getenv("GOOGLE_API_KEY")
|
||||
if not api_key:
|
||||
st.error(
|
||||
"GOOGLE_API_KEY 환경 변수가 설정되지 않았습니다. .env.chatbot 파일에서 설정해주세요."
|
||||
)
|
||||
st.stop()
|
||||
genai.configure(api_key=api_key)
|
||||
model = genai.GenerativeModel("gemini-1.5-flash")
|
||||
except Exception as e:
|
||||
st.error(f"Gemini AI 설정 실패: {e}")
|
||||
st.stop()
|
||||
|
||||
# --- Streamlit UI 설정 ---
|
||||
st.set_page_config(
|
||||
page_title="API Guide Chatbot",
|
||||
page_icon="🤖",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# --- 커스텀 CSS ---
|
||||
st.markdown(
|
||||
"""
|
||||
<style>
|
||||
/* 다크 모드 기본 테마와 유사하게 맞춤 */
|
||||
.stApp {
|
||||
background-color: #0E117;
|
||||
}
|
||||
/* 채팅 메시지 스타일 */
|
||||
[data-testid="chat-message-container"] {
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
|
||||
# --- 메인 앱 로직 ---
|
||||
# 시작 시 API 명세 불러오기
|
||||
api_spec = load_api_spec()
|
||||
|
||||
# --- 사이드바 ---
|
||||
with st.sidebar:
|
||||
if os.path.exists(LOGO_FILE):
|
||||
st.image(LOGO_FILE, use_container_width=True)
|
||||
st.header("⚙️ Controls")
|
||||
if st.button("🔄 API 명세 새로고침"):
|
||||
with st.spinner("최신 API 명세를 가져오는 중..."):
|
||||
api_spec = fetch_and_save_spec()
|
||||
st.session_state.messages = [] # 새로고침 시 대화 기록 삭제
|
||||
st.rerun()
|
||||
|
||||
if api_spec:
|
||||
with st.expander("📘 API 상세정보 보기", expanded=True):
|
||||
st.info(f"**Title:** {api_spec.get('info', {}).get('title', 'N/A')}")
|
||||
st.text(f"Version: {api_spec.get('info', {}).get('version', 'N/A')}")
|
||||
|
||||
paths = api_spec.get("paths", {})
|
||||
if paths:
|
||||
endpoint_data = []
|
||||
for path, methods in paths.items():
|
||||
for method, details in methods.items():
|
||||
endpoint_data.append(
|
||||
{
|
||||
"Method": method.upper(),
|
||||
"Endpoint": path,
|
||||
"summary": details.get("summary", "요약 없음"),
|
||||
}
|
||||
)
|
||||
df = pd.DataFrame(endpoint_data)
|
||||
st.dataframe(df, use_container_width=True)
|
||||
else:
|
||||
st.warning("API 명세를 불러올 수 없습니다.")
|
||||
|
||||
# --- 메인 채팅 인터페이스 ---
|
||||
st.markdown(
|
||||
"<h1 style='text-align: center;'>💬 LLM-Gateway Guide Chatbot</h1>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
st.caption(f"현재 사용 중인 API 명세: {API_SPEC_URL}")
|
||||
|
||||
|
||||
# 대화 기록 초기화
|
||||
if "messages" not in st.session_state:
|
||||
st.session_state.messages = []
|
||||
|
||||
# 시작 메시지
|
||||
if not st.session_state.messages:
|
||||
st.info("안녕하세요! LLM-Gateway 에 대해 궁금한 점을 물어보세요.")
|
||||
|
||||
# 앱 재실행 시 기록에서 대화 메시지 표시
|
||||
for message in st.session_state.messages:
|
||||
avatar = "🧑💻" if message["role"] == "user" else "👻"
|
||||
with st.chat_message(message["role"], avatar=avatar):
|
||||
st.markdown(message["content"])
|
||||
|
||||
# 사용자 입력 수락
|
||||
if prompt := st.chat_input("LLM-Gateway 에 대해 질문하세요..."):
|
||||
if not api_spec:
|
||||
st.error("질문을 처리할 수 없습니다: API 명세가 로드되지 않았습니다.")
|
||||
else:
|
||||
# 대화 기록에 사용자 메시지 추가
|
||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||
# 채팅 메시지 컨테이너에 사용자 메시지 표시
|
||||
with st.chat_message("user", avatar="🧑💻"):
|
||||
st.markdown(prompt)
|
||||
|
||||
# 채팅 메시지 컨테이너에 어시스턴트 응답 표시
|
||||
with st.chat_message("assistant", avatar="👻"):
|
||||
message_placeholder = st.empty()
|
||||
with st.spinner("답변을 생성하는 중..."):
|
||||
try:
|
||||
# Gemini를 위한 프롬프트 준비
|
||||
full_prompt = f"""
|
||||
당신은 다음 LLM-Gateway API 명세에 대한 전문가 어시스턴트입니다.
|
||||
당신의 임무는 제공된 JSON 데이터를 기반으로만 질문에 답변하는 것입니다.
|
||||
정보를 지어내지 마세요. 만약 명세에 답변이 없다면 없다고 말하세요.
|
||||
모든 답변은 반드시 한국어로 제공해주세요.
|
||||
|
||||
**LLM-Gateway API 명세 (JSON):**
|
||||
```json
|
||||
{json.dumps(api_spec, indent=2, ensure_ascii=False)}
|
||||
```
|
||||
|
||||
**사용자 질문:**
|
||||
{prompt}
|
||||
"""
|
||||
response = model.generate_content(full_prompt)
|
||||
|
||||
if response.parts:
|
||||
response_text = response.text
|
||||
else:
|
||||
# 응답이 차단될 수 있는 경우를 처리합니다
|
||||
response_text = (
|
||||
"죄송합니다, 해당 질문에 대한 답변을 드릴 수 없습니다."
|
||||
)
|
||||
|
||||
message_placeholder.markdown(response_text)
|
||||
st.session_state.messages.append(
|
||||
{"role": "assistant", "content": response_text}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_text = f"오류가 발생했습니다: {e}"
|
||||
message_placeholder.markdown(error_text)
|
||||
st.session_state.messages.append(
|
||||
{"role": "assistant", "content": error_text}
|
||||
)
|
||||
46
pyproject.toml
Normal file
46
pyproject.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
line-length = 120
|
||||
indent-width = 4
|
||||
|
||||
[lint]
|
||||
# 기본적으로 Pyflakes('F')와 pycodestyle('E') 코드의 하위 집합을 활성화합니다.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# 활성화된 모든 규칙에 대한 수정 허용.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# 밑줄 접두사가 붙은 경우 사용하지 않는 변수를 허용합니다.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
docstring-code-format = false
|
||||
docstring-code-line-length = "dynamic"
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
streamlit
|
||||
google-generativeai
|
||||
requests
|
||||
pandas
|
||||
pyarrow
|
||||
1
static/api_spec.json
Normal file
1
static/api_spec.json
Normal file
File diff suppressed because one or more lines are too long
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user