Initial commit

This commit is contained in:
kyy
2025-07-18 17:32:56 +09:00
commit e575ec8c80
11 changed files with 416 additions and 0 deletions

2
.env.chatbot Normal file
View File

@@ -0,0 +1,2 @@
# Replace with your actual Google API key
GOOGLE_API_KEY="AIzaSyD37Fp00b_i2DIywwtQu39w0RhkGAJO4YM"

32
.gitattributes vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
streamlit
google-generativeai
requests
pandas
pyarrow

1
static/api_spec.json Normal file

File diff suppressed because one or more lines are too long

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB