From e575ec8c806bbeb7cc7452e28bbd47816375a121 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 18 Jul 2025 17:32:56 +0900 Subject: [PATCH] Initial commit --- .env.chatbot | 2 + .gitattributes | 32 +++++++ Dockerfile | 23 +++++ LICENSE | 21 +++++ README.md | 79 +++++++++++++++++ docker-compose.yml | 10 +++ main.py | 197 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 46 ++++++++++ requirements.txt | 5 ++ static/api_spec.json | 1 + static/logo.png | Bin 0 -> 17141 bytes 11 files changed, 416 insertions(+) create mode 100644 .env.chatbot create mode 100644 .gitattributes create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 static/api_spec.json create mode 100644 static/logo.png diff --git a/.env.chatbot b/.env.chatbot new file mode 100644 index 0000000..c2f0814 --- /dev/null +++ b/.env.chatbot @@ -0,0 +1,2 @@ +# Replace with your actual Google API key +GOOGLE_API_KEY="AIzaSyD37Fp00b_i2DIywwtQu39w0RhkGAJO4YM" \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b60060f --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ece5652 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4af313f --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b58bd3 --- /dev/null +++ b/README.md @@ -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์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด ์›น ๊ธฐ๋ฐ˜ ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ๏ฟฝ๏ฟฝ๏ฟฝ๊ณ  ๋ฐ ์ปค์Šคํ…€ ์Šคํƒ€์ผ์ด ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. +- **์ปจํ…Œ์ด๋„ˆํ™”**: 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์˜ ์ œ๋ชฉ, ๋ฒ„์ „ ๋ฐ ์ „์ฒด ์—”๋“œํฌ์ธํŠธ ๋ชฉ๋ก์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6618d6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + chatbot: + build: . + container_name: pgn_spec_chatbot + ports: + - "8501:8501" + env_file: + - .env.chatbot + volumes: + - .:/app diff --git a/main.py b/main.py new file mode 100644 index 0000000..a0cabbb --- /dev/null +++ b/main.py @@ -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( + """ + +""", + 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( + "

๐Ÿ’ฌ LLM-Gateway Guide Chatbot

", + 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} + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6cc45da --- /dev/null +++ b/pyproject.toml @@ -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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1420878 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +streamlit +google-generativeai +requests +pandas +pyarrow diff --git a/static/api_spec.json b/static/api_spec.json new file mode 100644 index 0000000..baf2980 --- /dev/null +++ b/static/api_spec.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"LLM GATEWAY","description":"LLM ๋ชจ๋ธ์ด ์—…๋กœ๋“œ๋œ ๋ฌธ์„œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๊ตฌ์กฐํ™”๋œ JSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” API ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.","version":"0.1.0"},"paths":{"/metrics":{"get":{"summary":"Metrics","description":"Endpoint that serves Prometheus metrics.","operationId":"metrics_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/schema_file_guide":{"get":{"tags":["Guide Book"],"summary":"schema ํŒŒ์ผ ์ž‘์„ฑ ๊ฐ€์ด๋“œ๋ถ HTML ๋ณด๊ธฐ","description":"๐Ÿ“„ ๋ณธ ๊ฐ€์ด๋“œ๋ถ์€ /general ๋ฐ /extract/structured ์—”๋“œํฌ์ธํŠธ์— ์ฒจ๋ถ€๋˜๋Š” schema_file ์ž‘์„ฑ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

๊ฐ€์ด๋“œ๋ถ์€ ์—ฌ๊ธฐ์—์„œ ํ™•์ธํ•˜์„ธ์š”.","operationId":"schema_guide_schema_file_guide_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/general_guide":{"get":{"tags":["Guide Book"],"summary":"/general ๊ฐ€์ด๋“œ๋ถ HTML ๋ณด๊ธฐ","description":"๊ฐ€์ด๋“œ๋ถ์„ ์—ฌ๊ธฐ์—์„œ ํ™•์ธํ•˜์„ธ์š”.","operationId":"general_guide_general_guide_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/extract_guide":{"get":{"tags":["Guide Book"],"summary":"/extract ๊ฐ€์ด๋“œ๋ถ HTML ๋ณด๊ธฐ","description":"๊ฐ€์ด๋“œ๋ถ์„ ์—ฌ๊ธฐ์—์„œ ํ™•์ธํ•˜์„ธ์š”.","operationId":"extract_guide_extract_guide_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/info":{"get":{"tags":["Model Management"],"summary":"'/extract', '/general' ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก ํ™•์ธ","description":"โœ… 'inner(๋‚ด๋ถ€์šฉ)' ์™€ 'outer(์™ธ๋ถ€์šฉ)' ๋ชจ๋ธ์˜ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชฉ๋ก์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
\n โœ… 'Try it out' โ†’ 'Execute' ์ˆœ์„œ๋กœ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค.
","operationId":"get_model_info_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/default_prompt":{"get":{"tags":["Model Management"],"summary":"๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ","operationId":"download_default_prompt_default_prompt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/structured_prompt":{"get":{"tags":["Model Management"],"summary":"๊ตฌ์กฐํ™” ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ","operationId":"download_structured_prompt_structured_prompt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/structured_schema":{"get":{"tags":["Model Management"],"summary":"๊ตฌ์กฐํ™” ํฌ๋งท ์ •์˜ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ","operationId":"download_structured_schema_structured_schema_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/general/inner":{"post":{"tags":["General"],"summary":"๋‚ด๋ถ€ LLM ๊ธฐ๋ฐ˜ ๋ฒ”์šฉ ์ถ”๋ก  ์š”์ฒญ (๋น„๋™๊ธฐ)","description":"### **์š”์•ฝ**\n๋‚ด๋ถ€๋ง์— ๋ฐฐํฌ๋œ LLM(Ollama ๊ธฐ๋ฐ˜)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ ๊ธฐ๋ฐ˜์˜ ๋ฒ”์šฉ ์ถ”๋ก ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด ์—”๋“œํฌ์ธํŠธ๋Š” ํŒŒ์ผ(PDF, ์ด๋ฏธ์ง€ ๋“ฑ)์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ ์šฉํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n1. **์š”์ฒญ ์ ‘์ˆ˜**: `input_file`, `prompt_file` ๋“ฑ์„ ๋ฐ›์•„ ๊ณ ์œ ํ•œ `request_id`๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n2. **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ**:\n - `input_file`์ด ๋ฌธ์„œ๋‚˜ ์ด๋ฏธ์ง€์ผ ๊ฒฝ์šฐ, **OCR API**๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.\n - ์ถ”์ถœ๋œ ํ…์ŠคํŠธ์™€ `prompt_file`์˜ ๋‚ด์šฉ์„ ์กฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n - ๋‚ด๋ถ€ LLM(Ollama)์— ์ถ”๋ก ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n - `schema_file`์ด ์ œ๊ณต๋˜๋ฉด, LLM์ด ์Šคํ‚ค๋งˆ์— ๋งž๋Š” JSON์„ ์ƒ์„ฑํ•˜๋„๋ก ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n3. **์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ**: ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `GET /general/progress/{request_id}` ์—”๋“œํฌ์ธํŠธ์—์„œ ์ž‘์—… ์ง„ํ–‰ ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `input_file` (**ํ•„์ˆ˜**): ์ถ”๋ก ์˜ ๊ธฐ๋ฐ˜์ด ๋  ๋ฌธ์„œ ํŒŒ์ผ.\n - ์ง€์› ํ˜•์‹: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` ๋“ฑ.\n - ๋‚ด๋ถ€์ ์œผ๋กœ OCR์„ ํ†ตํ•ด ํ…์ŠคํŠธ๊ฐ€ ์ž๋™ ์ถ”์ถœ๋ฉ๋‹ˆ๋‹ค.\n- `prompt_file` (**ํ•„์ˆ˜**): LLM์— ์ „๋‹ฌํ•  ๋ช…๋ น์–ด(ํ”„๋กฌํ”„ํŠธ)๊ฐ€ ํฌํ•จ๋œ `.txt` ํŒŒ์ผ.\n- `schema_file` (์„ ํƒ): ๊ฒฐ๊ณผ๋ฌผ์˜ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•˜๋Š” `.json` ์Šคํ‚ค๋งˆ ํŒŒ์ผ. ์ œ๊ณต ์‹œ, ์ถœ๋ ฅ์€ ์ด ์Šคํ‚ค๋งˆ๋ฅผ ๋”ฐ๋ฅด๋Š” JSON ํ˜•์‹์œผ๋กœ ๊ฐ•์ œ๋ฉ๋‹ˆ๋‹ค.\n- `model` (์„ ํƒ): ์‚ฌ์šฉํ•  ๋‚ด๋ถ€ LLM ๋ชจ๋ธ ์ด๋ฆ„. (๊ธฐ๋ณธ๊ฐ’: `gemma3:27b`)\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ดˆ๊ธฐ ์‘๋‹ต**:\n ```json\n {\n \"message\": \"์ž‘์—…์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.\",\n \"request_id\": \"๊ณ ์œ ํ•œ ์š”์ฒญ ID\",\n \"status_check_url\": \"/general/progress/๊ณ ์œ ํ•œ ์š”์ฒญ ID\"\n }\n ```\n- **์ตœ์ข… ๊ฒฐ๊ณผ**: `GET /general/progress/{request_id}`๋ฅผ ํ†ตํ•ด ํ™•์ธ ๊ฐ€๋Šฅ.","operationId":"general_endpoint_general_inner_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_general_endpoint_general_inner_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/general/outer":{"post":{"tags":["General"],"summary":"์™ธ๋ถ€ LLM ๊ธฐ๋ฐ˜ ๋ฒ”์šฉ ์ถ”๋ก  ์š”์ฒญ (๋น„๋™๊ธฐ)","description":"### **์š”์•ฝ**\n์™ธ๋ถ€ ์ƒ์šฉ LLM(์˜ˆ: GPT, Gemini, Claude)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ ๊ธฐ๋ฐ˜์˜ ๋ฒ”์šฉ ์ถ”๋ก ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ๊ณผ ์ž‘๋™ ๋ฐฉ์‹์€ ๋‚ด๋ถ€ LLM์šฉ ์—”๋“œํฌ์ธํŠธ์™€ ๋™์ผํ•˜๋‚˜, ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n1. **์š”์ฒญ ์ ‘์ˆ˜**: `input_file`, `prompt_file` ๋“ฑ์„ ๋ฐ›์•„ ๊ณ ์œ ํ•œ `request_id`๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n2. **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ**:\n - `input_file`์—์„œ **OCR API**๋ฅผ ํ†ตํ•ด ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.\n - ์ถ”์ถœ๋œ ํ…์ŠคํŠธ์™€ `prompt_file`์˜ ๋‚ด์šฉ์„ ์กฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n - ์™ธ๋ถ€ LLM API(OpenAI, Google, Anthropic ๋“ฑ)์— ์ถ”๋ก ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n - `schema_file`์ด ์ œ๊ณต๋˜๋ฉด, LLM์ด ์Šคํ‚ค๋งˆ์— ๋งž๋Š” JSON์„ ์ƒ์„ฑํ•˜๋„๋ก ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n3. **์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ**: ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `GET /general/progress/{request_id}` ์—”๋“œํฌ์ธํŠธ์—์„œ ์ž‘์—… ์ง„ํ–‰ ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `input_file` (**ํ•„์ˆ˜**): ์ถ”๋ก ์˜ ๊ธฐ๋ฐ˜์ด ๋  ๋ฌธ์„œ ํŒŒ์ผ.\n - ์ง€์› ํ˜•์‹: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` ๋“ฑ.\n- `prompt_file` (**ํ•„์ˆ˜**): LLM์— ์ „๋‹ฌํ•  ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํฌํ•จ๋œ `.txt` ํŒŒ์ผ.\n- `schema_file` (์„ ํƒ): ๊ฒฐ๊ณผ๋ฌผ์˜ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•˜๋Š” `.json` ์Šคํ‚ค๋งˆ ํŒŒ์ผ.\n- `model` (์„ ํƒ): ์‚ฌ์šฉํ•  ์™ธ๋ถ€ LLM ๋ชจ๋ธ ์ด๋ฆ„. (๊ธฐ๋ณธ๊ฐ’: `gemini-2.5-flash`)\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ดˆ๊ธฐ ์‘๋‹ต**:\n ```json\n {\n \"message\": \"์ž‘์—…์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.\",\n \"request_id\": \"๊ณ ์œ ํ•œ ์š”์ฒญ ID\",\n \"status_check_url\": \"/general/progress/๊ณ ์œ ํ•œ ์š”์ฒญ ID\"\n }\n ```\n- **์ตœ์ข… ๊ฒฐ๊ณผ**: `GET /general/progress/{request_id}`๋ฅผ ํ†ตํ•ด ํ™•์ธ ๊ฐ€๋Šฅ.","operationId":"general_endpoint_general_outer_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_general_endpoint_general_outer_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/general/progress/{request_id}":{"get":{"tags":["General"],"summary":"๋ฒ”์šฉ ์ถ”๋ก  ์ž‘์—… ์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ์กฐํšŒ","description":"### **์š”์•ฝ**\n`POST /general/inner` ๋˜๋Š” `POST /general/outer` ์š”์ฒญ ์‹œ ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ํ•ด๋‹น ์ž‘์—…์˜ ์ง„ํ–‰ ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n- `request_id`๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Redis์— ์ €์žฅ๋œ ์ž‘์—… ๋กœ๊ทธ์™€ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n- ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ๋Š” ํ˜„์žฌ๊นŒ์ง€์˜ ๋กœ๊ทธ๋ฅผ, ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ๋Š” ๋กœ๊ทธ์™€ ํ•จ๊ป˜ ์ตœ์ข… ๊ฒฐ๊ณผ(`final_result`)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ**\n- `request_id`: ์กฐํšŒํ•  ์ž‘์—…์˜ ๊ณ ์œ  ID.\n\n### **์ถœ๋ ฅ (application/json)**\n- **์„ฑ๊ณต ์‹œ**:\n ```json\n {\n \"request_id\": \"์š”์ฒญ ์‹œ ์‚ฌ์šฉ๋œ ID\",\n \"progress_logs\": [\n { \"timestamp\": \"...\", \"status\": \"OCR ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"์ž…๋ ฅ ๊ธธ์ด ๊ฒ€์‚ฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์™„๋ฃŒ ๋ฐ ํ›„์ฒ˜๋ฆฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"ํ›„์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜\"\", \"details\": \"...\" }\n ],\n \"final_result\": {\n \"filename\": \"์ž…๋ ฅ ํŒŒ์ผ\",\n \"processed\": \"LLM์˜ ์ตœ์ข… ์‘๋‹ต ๋‚ด์šฉ\"\n }\n }\n ```\n- **ID๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ (404 Not Found)**:\n ```json\n {\n \"message\": \"{request_id}์— ๋Œ€ํ•œ ์ƒํƒœ ๋กœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\"\n }\n ```","operationId":"get_pipeline_status_general_progress__request_id__get","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/extract/inner":{"post":{"tags":["Extraction"],"summary":"๋‚ด๋ถ€ LLM ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ์ •๋ณด ์ถ”์ถœ (๋น„๋™๊ธฐ)","description":"### **์š”์•ฝ**\n๋‚ด๋ถ€๋ง์— ๋ฐฐํฌ๋œ LLM(Ollama ๊ธฐ๋ฐ˜)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ(PDF, ์ด๋ฏธ์ง€ ๋“ฑ)์—์„œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ์—”๋“œํฌ์ธํŠธ๋Š” ์‚ฌ์ „ ์ •์˜๋œ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n1. **์š”์ฒญ ์ ‘์ˆ˜**: `input_file`์„ ๋ฐ›์•„ ๊ณ ์œ  `request_id`๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n2. **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ**:\n - `input_file`์— ๋Œ€ํ•ด **OCR API**๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.\n - ์‹œ์Šคํ…œ์— ๋‚ด์žฅ๋œ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ์™€ ์ถ”์ถœ๋œ ํ…์ŠคํŠธ๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. (`prompt_file`์„ ์—…๋กœ๋“œํ•˜์—ฌ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)\n - ๋‚ด๋ถ€ LLM(Ollama)์— ์ถ”๋ก ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n3. **์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ**: `GET /extract/progress/{request_id}`๋กœ ์ž‘์—… ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `input_file` (**ํ•„์ˆ˜**): ์ •๋ณด ์ถ”์ถœ์˜ ๋Œ€์ƒ์ด ๋  ๋ฌธ์„œ ํŒŒ์ผ.\n - ์ง€์› ํ˜•์‹: `.pdf`, `.docx`, `.jpg`, `.png`, `.jpeg` ๋“ฑ.\n- `prompt_file` (์„ ํƒ): ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ ๋Œ€์‹  ์‚ฌ์šฉํ•  ์‚ฌ์šฉ์ž ์ •์˜ `.txt` ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ.\n- `model` (์„ ํƒ): ์‚ฌ์šฉํ•  ๋‚ด๋ถ€ LLM ๋ชจ๋ธ ์ด๋ฆ„. (๊ธฐ๋ณธ๊ฐ’: `gemma3:27b`)\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ดˆ๊ธฐ ์‘๋‹ต**:\n ```json\n {\n \"message\": \"์ž‘์—…์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.\",\n \"request_id\": \"๊ณ ์œ ํ•œ ์š”์ฒญ ID\",\n \"status_check_url\": \"/extract/progress/๊ณ ์œ ํ•œ ์š”์ฒญ ID\"\n }\n ```\n- **์ตœ์ข… ๊ฒฐ๊ณผ**: `GET /extract/progress/{request_id}`๋ฅผ ํ†ตํ•ด ํ™•์ธ ๊ฐ€๋Šฅ.","operationId":"extract_endpoint_extract_inner_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_extract_endpoint_extract_inner_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/extract/outer":{"post":{"tags":["Extraction"],"summary":"์™ธ๋ถ€ LLM ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ์ •๋ณด ์ถ”์ถœ (๋น„๋™๊ธฐ)","description":"### **์š”์•ฝ**\n์™ธ๋ถ€ ์ƒ์šฉ LLM(์˜ˆ: GPT, Gemini)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ์—์„œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋ถ€ LLM ์—”๋“œํฌ์ธํŠธ์™€ ์ž‘๋™ ๋ฐฉ์‹์€ ๋™์ผํ•˜๋‚˜, ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n1. **์š”์ฒญ ์ ‘์ˆ˜**: `input_file`์„ ๋ฐ›์•„ `request_id`๋ฅผ ์ƒ์„ฑ ํ›„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n2. **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ**:\n - `input_file`์—์„œ **OCR API**๋ฅผ ํ†ตํ•ด ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.\n - ๋‚ด์žฅ๋œ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ(๋˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ `prompt_file`)์™€ ํ…์ŠคํŠธ๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.\n - ์™ธ๋ถ€ LLM API(OpenAI, Google ๋“ฑ)์— ์ถ”๋ก ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n3. **์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ**: `GET /extract/progress/{request_id}`๋กœ ์ž‘์—… ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `input_file` (**ํ•„์ˆ˜**): ์ •๋ณด ์ถ”์ถœ ๋Œ€์ƒ ๋ฌธ์„œ ํŒŒ์ผ.\n- `prompt_file` (์„ ํƒ): ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ ๋Œ€์‹  ์‚ฌ์šฉํ•  `.txt` ํŒŒ์ผ.\n- `model` (์„ ํƒ): ์‚ฌ์šฉํ•  ์™ธ๋ถ€ LLM ๋ชจ๋ธ ์ด๋ฆ„. (๊ธฐ๋ณธ๊ฐ’: `gemini-2.5-flash`)\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ดˆ๊ธฐ ์‘๋‹ต**:\n ```json\n {\n \"message\": \"์ž‘์—…์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.\",\n \"request_id\": \"๊ณ ์œ ํ•œ ์š”์ฒญ ID\",\n \"status_check_url\": \"/extract/progress/๊ณ ์œ ํ•œ ์š”์ฒญ ID\"\n }\n ```\n- **์ตœ์ข… ๊ฒฐ๊ณผ**: `GET /extract/progress/{request_id}`๋ฅผ ํ†ตํ•ด ํ™•์ธ ๊ฐ€๋Šฅ.","operationId":"extract_endpoint_extract_outer_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_extract_endpoint_extract_outer_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/extract/progress/{request_id}":{"get":{"tags":["Extraction"],"summary":"์ •๋ณด ์ถ”์ถœ ์ž‘์—… ์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ์กฐํšŒ","description":"### **์š”์•ฝ**\n`POST /extract/*` ๊ณ„์—ด ์—”๋“œํฌ์ธํŠธ ์š”์ฒญ ์‹œ ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ํ•ด๋‹น ์ •๋ณด ์ถ”์ถœ ์ž‘์—…์˜ ์ง„ํ–‰ ์ƒํƒœ์™€ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n- `request_id`๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Redis์— ์ €์žฅ๋œ ์ž‘์—… ๋กœ๊ทธ์™€ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n- ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ๋Š” ํ˜„์žฌ๊นŒ์ง€์˜ ๋กœ๊ทธ๋ฅผ, ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ๋Š” ๋กœ๊ทธ์™€ ํ•จ๊ป˜ ์ตœ์ข… ๊ฒฐ๊ณผ(`final_result`)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ**\n- `request_id`: ์กฐํšŒํ•  ์ž‘์—…์˜ ๊ณ ์œ  ID.\n\n### **์ถœ๋ ฅ (application/json)**\n- **์„ฑ๊ณต ์‹œ**:\n ```json\n {\n \"request_id\": \"์š”์ฒญ ์‹œ ์‚ฌ์šฉ๋œ ID\",\n \"progress_logs\": [\n { \"timestamp\": \"...\", \"status\": \"OCR ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"์ž…๋ ฅ ๊ธธ์ด ๊ฒ€์‚ฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์™„๋ฃŒ ๋ฐ ํ›„์ฒ˜๋ฆฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"ํ›„์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜\"\", \"details\": \"...\" } \n ],\n \"final_result\": {\n \"filename\": \"์ž…๋ ฅ ํŒŒ์ผ\",\n \"processed\": \"LLM์˜ ์ตœ์ข… ์‘๋‹ต ๋‚ด์šฉ\"\n }\n }\n ```\n- **ID๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ (404 Not Found)**:\n ```json\n {\n \"message\": \"{request_id}์— ๋Œ€ํ•œ ์ƒํƒœ ๋กœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.\"\n }\n ```","operationId":"get_pipeline_status_extract_progress__request_id__get","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dummy/extract/outer":{"post":{"tags":["Dummy"],"summary":"๋”๋ฏธ ์‘๋‹ต ์ƒ์„ฑ","description":"### **์š”์•ฝ**\n์‹ค์ œ ๋ชจ๋ธ ์ถ”๋ก ์ด๋‚˜ ํŒŒ์ผ ์—…๋กœ๋“œ ์—†์ด, ์ง€์ •๋œ ๋ชจ๋ธ์˜ ์‘๋‹ต ํ˜•์‹์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋”๋ฏธ(dummy) ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n- ์š”์ฒญ ์‹œ, ์‹œ์Šคํ…œ์— ๋ฏธ๋ฆฌ ์ €์žฅ๋œ ๋”๋ฏธ ์‘๋‹ต(`dummy_response.json`)์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n- ์‹ค์ œ OCR, LLM ์ถ”๋ก  ๋“ฑ ์–ด๋– ํ•œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n- ๋„คํŠธ์›Œํฌ๋‚˜ ๋ชจ๋ธ ์„ฑ๋Šฅ์— ๊ด€๊ณ„์—†์ด API ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `model` (์„ ํƒ): ์‘๋‹ต ํ˜•์‹์˜ ๊ธฐ์ค€์ด ๋  ๋ชจ๋ธ ์ด๋ฆ„. (๊ธฐ๋ณธ๊ฐ’: `dummy`)\n - ์ด ๊ฐ’์€ ์‹ค์ œ ์ถ”๋ก ์— ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฉฐ, ํ˜•์‹ ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ๋งŒ ๊ธฐ๋Šฅํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ฆ‰์‹œ ๋ฐ˜ํ™˜**:\n ```json\n {\n \"filename\": \"dummy_input.pdf\",\n \"dummy_model\": {\n \"ocr_model\": \"dummy\",\n \"llm_model\": \"dummy\",\n \"api_url\": \"dummy\"\n },\n \"time\": {\n \"duration_sec\": \"0.00\",\n \"started_at\": \"...\",\n \"ended_at\": \"...\"\n },\n \"fields\": {},\n \"parsed\": \"dummy\",\n \"generated\": \"dummy\",\n \"processed\": {\n \"dummy response\"\n }\n }\n ```","operationId":"extract_outer_dummy_extract_outer_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_extract_outer_dummy_extract_outer_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ocr":{"post":{"tags":["OCR"],"summary":"๋ฌธ์„œ OCR ์š”์ฒญ (๋น„๋™๊ธฐ)","description":"### **์š”์•ฝ**\n๋ฌธ์„œ ํŒŒ์ผ(PDF, ์ด๋ฏธ์ง€ ๋“ฑ)์„ ๋ฐ›์•„ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๋Š” OCR(๊ด‘ํ•™ ๋ฌธ์ž ์ธ์‹) ์ž‘์—…์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n1. **์š”์ฒญ ์ ‘์ˆ˜**: `file`์„ ๋ฐ›์•„ ๊ณ ์œ  `request_id`๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.\n2. **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ**:\n - ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ๋‚ด๋ถ€ ์ €์žฅ์†Œ(MinIO)์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.\n - ๋ณ„๋„์˜ OCR ์„œ๋ฒ„์— ํ…์ŠคํŠธ ์ถ”์ถœ ์ž‘์—…์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.\n3. **์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ**: ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ `GET /ocr/progress/{request_id}`๋กœ ์ž‘์—… ์ƒํƒœ๋ฅผ, `GET /ocr/result/{request_id}`๋กœ ์ตœ์ข… ํ…์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ (multipart/form-data)**\n- `file` (**ํ•„์ˆ˜**): ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•  ๋ฌธ์„œ ํŒŒ์ผ.\n - ์ง€์› ํ˜•์‹: `.pdf`, `.jpg`, `.png`, `.jpeg` ๋“ฑ OCR ์„œ๋ฒ„๊ฐ€ ์ง€์›ํ•˜๋Š” ํ˜•์‹.\n\n### **์ถœ๋ ฅ (application/json)**\n- **์ดˆ๊ธฐ ์‘๋‹ต**:\n ```json\n [\n {\n \"request_id\": \"๊ณ ์œ ํ•œ ์š”์ฒญ ID\",\n \"status\": \"์ž‘์—… ์ ‘์ˆ˜\",\n \"message\": \"์•„๋ž˜ URL์„ ํ†ตํ•ด ์ž‘์—… ์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.\"\n }\n ]\n ```\n- **์ตœ์ข… ๊ฒฐ๊ณผ**: `GET /ocr/result/{request_id}`๋ฅผ ํ†ตํ•ด ํ™•์ธ ๊ฐ€๋Šฅ.","operationId":"ocr_only_ocr_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_ocr_only_ocr_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ocr/progress/{request_id}":{"get":{"tags":["OCR"],"summary":"OCR ์ž‘์—… ์ƒํƒœ ์กฐํšŒ","description":"### **์š”์•ฝ**\n`POST /ocr` ์š”์ฒญ ์‹œ ๋ฐ˜ํ™˜๋œ `request_id`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ OCR ์ž‘์—…์˜ ํ˜„์žฌ ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.\n\n### **์ž‘๋™ ๋ฐฉ์‹**\n- `request_id`๋ฅผ OCR ์„œ๋ฒ„์— ์ „๋‹ฌํ•˜์—ฌ ํ•ด๋‹น ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.\n- ์ƒํƒœ๋Š” ๋ณดํ†ต 'PENDING', 'IN_PROGRESS', 'SUCCESS', 'FAILURE' ๋“ฑ์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.\n\n### **์ž…๋ ฅ**\n- `request_id`: ์กฐํšŒํ•  OCR ์ž‘์—…์˜ ๊ณ ์œ  ID.\n\n### **์ถœ๋ ฅ (application/json)**\n- **์„ฑ๊ณต ์‹œ**:\n ```json\n {\n \"request_id\": \"์š”์ฒญ ์‹œ ์‚ฌ์šฉ๋œ ID\",\n \"progress_logs\": [\n { \"timestamp\": \"...\", \"status\": \"OCR ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"์ž…๋ ฅ ๊ธธ์ด ๊ฒ€์‚ฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"LLM ์ถ”๋ก  ์™„๋ฃŒ ๋ฐ ํ›„์ฒ˜๋ฆฌ ์‹œ์ž‘\", \"details\": \"...\" },\n { \"timestamp\": \"...\", \"status\": \"ํ›„์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜\"\", \"details\": \"...\" }\n ],\n \"final_result\": {\n \"filename\": \"์ž…๋ ฅ ํŒŒ์ผ\",\n \"parsed\": \"OCR ๊ฒฐ๊ณผ ๋‚ด์šฉ\"\n }\n }\n ```\n- **ID๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ (404 Not Found)**:\n ```json\n {\n \"detail\": \"Meeting ID {request_id} ์ž‘์—… ์—†์Œ\"\n }\n ```","operationId":"get_pipeline_status_ocr_progress__request_id__get","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio":{"post":{"tags":["STT Gateway"],"summary":"Proxy Audio","operationId":"proxy_audio_audio_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_proxy_audio_audio_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/progress/{request_id}":{"get":{"tags":["STT Gateway"],"summary":"Proxy Progress","operationId":"proxy_progress_progress__request_id__get","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/summary":{"post":{"tags":["summary"],"summary":"Summarize","operationId":"summarize_summary_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummaryRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ollama_summary":{"post":{"tags":["summary"],"summary":"Ollama Summary","operationId":"ollama_summary_ollama_summary_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummaryRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/task_summary":{"post":{"tags":["summary"],"summary":"Task Summary","operationId":"task_summary_task_summary_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SummaryRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/task_summary/{task_id}":{"get":{"tags":["summary"],"summary":"Get Status","operationId":"get_status_task_summary__task_id__get","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"string","title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health/API":{"get":{"summary":"Health Check","description":"์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ ํ™•์ธ","operationId":"health_check_health_API_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/Redis":{"get":{"summary":"Redis Health Check","operationId":"redis_health_check_health_Redis_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/MinIO":{"get":{"summary":"Minio Health Check","operationId":"minio_health_check_health_MinIO_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"Body_extract_endpoint_extract_inner_post":{"properties":{"input_file":{"type":"string","format":"binary","title":"Input File"},"prompt_file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Prompt File","description":"โš ๏ธ prompt_file ์—…๋กœ๋“œํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, **'Send empty value'** ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ฐ˜๋“œ์‹œ ํ•ด์ œํ•ด์ฃผ์„ธ์š”."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","default":"gemma3:27b"}},"type":"object","required":["input_file"],"title":"Body_extract_endpoint_extract_inner_post"},"Body_extract_endpoint_extract_outer_post":{"properties":{"input_file":{"type":"string","format":"binary","title":"Input File"},"prompt_file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Prompt File","description":"โš ๏ธ prompt_file ์—…๋กœ๋“œํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, **'Send empty value'** ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ฐ˜๋“œ์‹œ ํ•ด์ œํ•ด์ฃผ์„ธ์š”."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","default":"gemini-2.5-flash"}},"type":"object","required":["input_file"],"title":"Body_extract_endpoint_extract_outer_post"},"Body_extract_outer_dummy_extract_outer_post":{"properties":{"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"์‹ค์ œ ์ถ”๋ก  ์—†์ด ํฌ๋งท ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.","default":"dummy"}},"type":"object","title":"Body_extract_outer_dummy_extract_outer_post"},"Body_general_endpoint_general_inner_post":{"properties":{"input_file":{"type":"string","format":"binary","title":"Input File"},"prompt_file":{"type":"string","format":"binary","title":"Prompt File"},"schema_file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Schema File"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","default":"gemma3:27b"}},"type":"object","required":["input_file","prompt_file"],"title":"Body_general_endpoint_general_inner_post"},"Body_general_endpoint_general_outer_post":{"properties":{"input_file":{"type":"string","format":"binary","title":"Input File"},"prompt_file":{"type":"string","format":"binary","title":"Prompt File"},"schema_file":{"anyOf":[{"type":"string","format":"binary"},{"type":"null"}],"title":"Schema File"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","default":"gemini-2.5-flash"}},"type":"object","required":["input_file","prompt_file"],"title":"Body_general_endpoint_general_outer_post"},"Body_ocr_only_ocr_post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_ocr_only_ocr_post"},"Body_proxy_audio_audio_post":{"properties":{"audio_file":{"type":"string","format":"binary","title":"Audio File"}},"type":"object","required":["audio_file"],"title":"Body_proxy_audio_audio_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"SummaryRequest":{"properties":{"text":{"type":"string","title":"Text"}},"type":"object","required":["text"],"title":"SummaryRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}} \ No newline at end of file diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46868be622b9d60292295e493ba0b5e1788bc0b0 GIT binary patch literal 17141 zcmeIZ1yh?{7dA?P7KZ{Y?pi2LaVy2$AKYDnyB1p9-J!TUB)C)Do#39}?r_rg`w{1x zGbb~dOfs2$uf6nI7Ot!)g@H*r zI5-ySfA4pmMI!F79|>J0v|QC3EL=T|oz3Ay%^Xb3DP#b~mgcJF#%5kl!{&l;a6f`% z#6{ITSB}@Z97tp`mo8SWJ^&N5gIOJc%kw8dJ*Y8os_Tmm7RIPa`J-CEY#WK6*+rzlYHe6?%L!YzXxWVh!-k zR^#xe?0J_s9H>YCAqScnu(WlAx<>g{d-Y&p1M$QUkHUYPG#;#Bx^e1ia2wkmzT$b9 zabl&9YfZXogL1ZX2p7l0RQbDVN&-HPDXYOJ7GVYHH}mSJ_gzOKqp$}sf7$bo+x)&r z(^LAmQ)G|+A;mxX8xFoeu7e^5`Blz9e!MA}=Af_r_Gpe^Km*8i3?Xumc~C%+Yg$EU zwa_j=+I8SoS;r;k3MxoAZOZaJq7{>maW>Rkm*stoIhA=GV;SjjM@7wvW0{b5>-IBV z0oGp2w;cl;J;M;WyYa}+0VjPkFW+|fj2cf4TA*)Sqi()JcJV`2ro|$;jeWFG}Hqttb>4rr2NOhYI7^6fyR3L1wo{rP09*Qun)e|u~<*X_Gr?)j~>$9sY`&7lhmnVRJ?Hiph&CpTQv)=5n85Rj|XhMEV>!yt<9%@X=NiKU~C+ z*(B&|LL?3El#=t#oO!4&F;9B2DrrOb_ZAn&DH?`?l6*aW;r->4n~R1pa%a^}>R(s| z`s5n~M@Dym-zG4z0@q_cQd(&@0%@y(GrM?$6{=}H8wiIal*yT9uE2v241 zcop?RY8JRsSZ7623?BVAS96kfbmrdOnLbD17`P}qoyj8wh;CNWjz($;6tmehseR3- z@blS|Jc-P)UVmcwJoJ1$j+9JrBD6uAm1_znUeLo!$kvxieSU*y#3;iVXa?erC z?IghbYi94>7|tr?c92(~z9Fv5wDC(BUVPMT%z6z9d5wQKZbMlY;7%kC82BJeGfY+8 zhU98ir3dnq+|^w2mWsDPq43%n=~$7W1z*sHWV5jj>tX1>tk0y zwq^B<>+;SSzZ@F{sg^wg>&@A@KHj0n)0}&|;_)pXxSbF#qB>IP%tZHKaud;TbcKV(C}Db#CYxAr|q3=7C_TL{&M&KEXpFaKmnC+ zA9j27bv(<}H7o3I(caYp>95%gb)=Ot#6FS$#oq5-Zc0T6IWEIhe8+skqpEULgzflv z1t2V3Ui~s^@|&PltzU>pWOS%3q)0(X3LuReiWc^T0&ZXqaY-UEORQJE6o4%ktMp(y z`W!7{tr^>)BMa&hxSblFWL*=Ug;=80?tCs_$oMVa5Lw!py6N^so>eybv$W%;alX;+ z;i8<>(=re(T$tA)mFHeGFq7m>kGRuJrT&SpSe@{Q`CU zJqEK5nrRz*&TbYVfko9G@$)2s$N4@)gcM<@-RZ-1aPPK9+xqr{SZtSV+>lkaFt}t6 zh)hoBs37Lgjdk5SKrOpdUEN7Pd?AEG5+^QN-79C$tMsW+-o9DSg?eu{q@5N>_~#)- z`OD`Yn9geGu21C+l>=L++8ZUB?D)mMus@V3Sd#L$1IwCT6h@QaFt6G(R6`T=}2h~Em1e#iw{5mL8f}a&}D@Vz3S~}6{Jp3Pp zyEy8e_9T8Raudi6V-#OLTlM!AY{YF!rzw8oJ#j6GTb)|xw~L#Uu@}d0$Bp_+ZdHu8 z`wCMx@2ZWTPy1s1i=Z=XI5DLfr&7}M4P4t;J@-G_R<7?8h;{i)*-w0EcxT)pwRr>} z`A=DVg8MR=rv8`D<7Isl5yvUc;u@f%QF6nB98#rW4R##)mSYO@c#kPEM(<~R27BV8 zZz#525u5z%tlX_7MinRIjG)N>FtwG8X%mRScZms7LDVWdu^yPD!W1P69%R#lf~u!@ zpBT$c?c1-05)T{-qm)QKG}7=Gsmfl}y705BfaUC}eIj&&q#o#2Rgl{MoRpk+XdUQx z#_1Q$-HAg?U*>fW=uRx{Gp~6K{;2V_(`#D*Lx!Ipku8}&UYa7=0!_zcl)aRG5uOQ;?wt|(HXR=0Wa!4Xw=0Aa)7YHFF>O;P*FjKT zL3~|0K9@v4xe-MoOfy*7_AWF#T&w;3P|KM`iZyUE@90-8Z?sv;sqpcU`BDMVY5B;n4;QZRP%xWzWL%O51$PG=wNI;)fFa`tKd;r4 zxr#2%zjf4>HmGpGeDlGaJ4N{)3OTFN`q7ws6kSf81I7n3HL3U}!eh5`eT6q|%d~Ui z`%Z%)mIUGsVV~Y8FP zm2Zi`-R9NxzD@tsTS9mEWyb}aG=ltRwa-+RC9eo{Ms{;vVKy?vsX1HYs%p;D!@q`x zLxR%m)!tCD%EH9&;4~(-XRrH(Z@(TQ;(ht7P`nz=o4#s5jz8y4AGjiz=b{i8i{`@4 z)KN0=8)=r%u92P`r>pvxba7dGM5*HA(4af_0MEH!|1{?^r2i%yN)tMsnmEkzpIi{N z^E*~1sB`?p6rR`#hb$defGT2k5=xfo=@xqthx&5pqZ`Dt95sB{mA{6)QK9k0{iNud zo1(~ponff3q@tZd&5O!X6uD}9x{PE0B!ns(@EX5UWd~+q%J_CyXYwcm-UY8p%nHeo|24d)<_g=H4Zgt1hG($E3^G5Jo%M*J{!2YXPZgKB+J|x8f z2hc@U#!>%li}`4v+$h>^anzhs>?xMG8J1ZAFs4nj$&7a8+b*1E7x8-CpW9+5R~`?f zfgjG&7xs1|RPo|Gp?!|YA8iWUpThtF0;ui|hA!`vx}GoGpcfB@T94~AJ0EaJ;#@7qucdQ6;3-M|r+`jwaqQsa2bCVa8XBuBJa98*+S|$)a&o(>JaZ6{^0#f`C!Aqm(|R=E$hlu8 zOn14yw$!Jyi;%fRTz6C<&go(V=O~!Dj2#>avTAbuGFcK z=BE3;O0M1B#q|^WG@t(MJ213nurQ5R=@(_4ft3FM>1(M64aFCjc3QR6md;k$!Q2L< zz4h#T5?5d&EghXAwiOtD1n*Q_c72+_5nHZ+T32u%VPl#+T%|SG+OT3?emepbxwJL9 zdY0`_lR-{Xv_eLS8RA{X`wtIdC(y+VW64B}cGJd4$|g}Qu~G8qmG8~A@%upgT8=$? zYzP6kgCpUo9sfATq{CN1QGqE1|E~Jg$E3D%P*`EW0>^=r~YABaM(?Aks2HW);i{e-SoR|T9vQqeU{fJ6x4H9bDCnnY;nPUx}Avk_#|T$ zqSk3Tntk*I>^~~gu1cTrLsS<4BQxxuCjiuWuR9x0Z%Sqz21{fE%zb?P1oMChlIcYG z?Hp+w&vdK@&V190>d8U*K$gN7s#&$(QnUsx-3GZRYiO`~C7^5ksuj7C$*%FuJc5TC zc6oRkFuvRphSH&Jw57UK@AJZ+N%^9PnKrFB#&5&;fD+@`?A`DU{NOw5-Xo`}rq;T8PHAr*`Rx}d zOZAY;s1lMuWoO6dyLbLZCAA$T4N7_rjwETRg3%#xLw%dO9B2qC0f2|b`X~s4apMcF*kmZ6pIc#IzXef%YX^ooiq`xKPYwTa zN59CuoaI2yWN=Vox*;%$DqN+V`gSttyy~Wcdx4}*+vQoKu%NGI(*_DW9O^S2Nop%o zr_>iax{ch3C&!-3qEQDxYk5%bq&`RS5PqFmp`fv(B3*l$ujVqc;Tm40`GBkUMD z9KBg)4~7gnRR?0&H6Q8oV)f#7YJ^MX4>a9G^2HMHaQ#GB0*W6k>#P6JaFIa*Qm^Hv zbHmkw@L?Apgps17HFp85MN)UE89qg=mtGK9n`YK0`Rz}feiU<-6YRbv_It5#GRoJJ($KK`^#k1;J|92d=j(fW7M?-*@;bg^=w@K+qs;Nly zF27*Yb%)BpSMSiAAh0_Ow!yn!c}gW9R^th_A-n1>{l)l=1n!+FO*j4ez@~%9qVbV> zSOjuCl$(1!iT|cMmgyexU(`1(SNnk3x30$b2rLe!bn6KtZh%Yo(_ZB`A0BnSJ<3ut zPhI_AmStTGXlYUX!+NwuwkuD1Et&3FsJJMFs<9*B1pzTYSLzM$q-7~0yKLKafYT!T+QUa5AZ#7rBf zl*C8$TZ0^@HY?5%VP#cvq1cUlKqy}U&-Wl|aTuaiOm1Vq-ANhD`s(388^dphc4YTe z7a08hc*;RW!PQ0w@KTaURg^b7+>+m9&U{iz!)FI9QO#vxI5Fzj)=IE`%9dBswuzNL zE&S{acKBL%AD)4ULlQV0>Do%9$_J@suB_N}z}pN<{FmzUHIdf-Ro`}o9&Y?yrR2y7 zn?fm~9v#fq%q|hb8chVHXGRM`d@Un>7Xj!-Jk}kZRCECWhq}RFq8=61anT@ZK}*JV zfU!Qy%rZ3X23Xq{7en~HCUC28=ajIxxD1^-lgxPUMAxl~ZsaxPk0qHR;bS7$&qZo9 z(M?@a5A{f%=64&A8urg)$aGlVtGy zWE9&Y@yt*A1zkzaSf%%bq|AR|=WqpLq$Iu{izKQP)wqzpl9=>ps0q@ax!V> z%K&y4gUKke0buMQypaWb1|_UICL7xe;B2#Xn|_!w{I2z%sJii{4?+2ALC(SsLHVhS z-}?7khuup+Gnr_D6Jfit;6nt_&eH;glk#$Z_COeLe`TK#@LP(Ojp(ne zla5fm(BqN~--pBwUp7$fboSaPPx(UPaMppnV9q>A@G zQX!p|`C94=7GM#s_D%#qmlydL_^@0C_%4VwSv4?NXyc#D=PeUNM4e|ErVI#ZKk5C` zYNtqsh?HlKCIE|t3+c#+tmx#4u4D4@d!~p|2s0{W{0#}ap0*8Yw7P>(Z0cG|cKDh8 zYt?YjqXF9J+;vi&2|Zq>xv%+s+B#vY!OShWV|btgLC=ZZfH~vnH;2VogLN+@qC>i7b z(mehx_OlE!%6e6`O=ZAfwOs+&qCXs_0Q9iuT=x!Aj-UnS0SzQe*uZ z7ny3wYk2+Lt34g*_DhnP>2b9LhLlYc%Z_~p^=2E!7 zA|ofz0lK7LaTgPTyn=49(sSV|;4NuPPq+hS>{WKGOL*LbSidH@(Trb;I|D|U zWcWgg>=)zDoxMj?kpMcR1>rRbc_Vz;+54os<*O6SB4}nuOCEZ}Pm>z+eomU&0_H~6 zjSKbhJuZKtKoK=EMJ2CudGYgpie~+3bmPmACquR7d_VGflJJ-{b zC9U+Gc@sLK{BD4`y_=|+TM_}{6LHm&N6b&DDJjbv;ZO}$baeril&=aol>6Lh8=m}# zD6h{xoaWG%GLP~0l*}a1RvzJV**gKg;2l*kxib~SgoI3Rg*B_;s!0mF3D&AJH1sOn@Ixzs4!d(42`YyvOaf`9|-o8{DdQFih>Ga7xsOH=PuM_5UT{pF^2@$>rRJ1Jt8^yWYbl7qB5Dj_;Q=n3b zi|aXPmn)T&qGv0a02quBxQ@3lPXyCaV*HI7+P?4Oh`tg=H{sTdCj!LIlO)=&T#;3# zi^I)0+35rW>@(EezIKWKg|LtU0`mG0 z`XvAgiBovVc{fY3GAls0l#`I8xp?S0KFLuX6mP3VbkA6D7q6I>c0Unh{Mxi9_|294 zM&obsf{{rC)gNg`&psHIkt$2#1oy(ru-|Z3)KDz$o_P)Lor&X-G`(;?i+l7Ng@(W9 zRls`S`5aKoWhbOI8~I3t&3Sdc8fa9fP>FO6thI!hX5$I47FME-rv&DfE4IZ?lk-1N zCmj*@?2|1I)S$Ow7s8{M<<6T#DhYDnH%+Pm0Y;< zerA>J*c-FdKYXndWq%FgR>b#k^ECqvX&b4jYEe-zMMxNV)ab-v3ho^_AOGs$rN(QY z7*_gPYNsCX>57G*NrvzCpj3Ty#&5LOobS?dH|F^^U8wh0NG>d2G4iCYUdRP>#PYZ zmtJ{qciql}ahr#dI9P^EZwi!BnL&JzT3<%V5$i_Z8nhS#QWuTBIG|`p)d~rr8dV;5g*8|DE5&!Kf1V}H zz#^{RXk=WR>Gr#C9c|EyWn`PilTV*$WKaJNc3@$!Y$J!)>Y*yYguu`O0OC4lp}X`! zx6>;*%)PW3(5!t=Z5*U1(CX6E4CAyA{}Y4(z`FAttcOn1?#=Y20=)wx;nsTVADy z6o(|{AJ{P``d98?@SvvDsWMd|ZAhy7cWMyQ_0Z+Vy^k2zVR){}ZcJ$AMtQ@`Y5o@% zLAD+|@?C7_8uUVMZK2i8xVB*ba1|HXZPxSyu=1OI9T|la4KE3@o32*qAQL9MfCUUf zuKwYCD~MaSd0eGTO+}!o>NBI_i{nzjV>wBho)?_fXU`x|K@{=K-sH2?bC26}?3)MH zR+Y#fGXJOVMlu1*??>?l+e?=4@F@QD84TVa{COop=_W!pHfRgyNX+sEJe<{tb%6h6 zT}>~+MQ7%um@q}kghfi$e=yb=l#ISzL+;4_}G8r%&`*#$1| z+JR~YmCoHM6g8RuI;IZN=CAK|sdcTCQ~?46J=4^%lq*KyAIYSVS1E(XP-Bk?V6DtB z1AI;G>tF9qR$lxlGJKNYic^I5Xt>%A;TRp$vg0}u(L=>&A~!||hBs6kg!T>szF zzC~%fYxX955*l7_=PS>HDdY3C^4XIfV68-?DH)@O%lIG+uJHE3yJQvtmr9#l%zLJd z(q-`~NeA*0t6K6l#7M!%v=X%~CwJG)vT_@lq5eJB4UbiM2TR*+38Fy-NdM|n;Xx?i z-rHGeP`V<5R#K63KL1CfuuFGQMcF;Gn`jLRw3x@+g$W3EH$S^MBT>S z`Zzk6kMjx)H_T!59t98e!9Zy~3|TXmp1=j_!i6xf@LEI~I40eSFsYf_za-&C)+Mq= zMn0|@#MQ2?=Qsem>_w{PmXQ~-hRW(s;x}t2HS+(cpMZkhS`3C%M%|e7$T$ml;6(>n zR%~S{zL#{<=Dd41T|gC8GobQf&4J0l?EY$E{S+DzTtvL6i4_~JLc(@x8PQGl4u?`+wI0^SU+?sA2{ z4CWO$CsW*o?RRU1wp6YJ*7aG^XZTpwHh8G&zft!s<_<{o**p~`zFLlN1T@qAt^gWJ z3gmdZlcJehLVss{wcVkG!5961>BW62KT4AkSbe#lBn^wzKS(Fnun655w{LX zKDK7I8@BMQ0bg9!P6HNiGyT)^q~4eU48F7t$By--ascs&k|Ak*P(8~$jpI3>YZEM% z(EdwwNk8yc;k@^z`CW1IgN_*#JpTC~Aei|Ylxc4%=k|8nc=(lVW;nq_A&&ase&t8^ zQ-osDvTdo9lb*L{?)_XIyMAn&DRwuugK1M`>_s{uaWGBMXIqoqQd^~^vPVb&8 zQAp=I`*b~Xy_LLQjGMmdeiMG4N7RFVKvaH(W0PtVLi&kxOPCpC*lLctI&#u!?t9jR z`Vy+0-06B+IxDYh&{w~GZE%2D*$MeW67uY@pssl*zSYd+0yoH<5sL_i#663_Lyc}F zW_>8R?dAuI4lG-8_8c2CL?W3H9Ua;gIyp+hAcZ^Cy|*F9C&uXb^|yiYV=MKMZZ}L* z6uNZ=B}Hu&QZqcIcH9H9H50X ztzG(qh~#utx)e*-yD+Ukg^sTUx{|~r;*jjOKwVS+jfH@p`(Vg5tGs&wP34Yo8P))h z$Z}SS{d(K7M2FC*%wJfH&qa3Fa?JJ?b#Cof93_V%|HHVKc6n&`I!W2Rf_?3FgP9vT zI;>5muy+|;dbDJr9j{6aBmNJJcHRPbU1*UNX@f{@MFEZdn{kr{J+Z+omOkJ|s1zjoR1k(ZEB+mSOKt;w4J4 z+idzg243zN+)`@MzOsjNa}JQi<%}GhReKKoxJ@>3SRzN2`%+fRH->RqN>(nprlGU0Qi*^7!X>=? z;>2R=$zuB@MO4ENY#Mw0CRvY^MWGldm@crUWxFmdDv87Y3dkya`>WIJgJ;vSRJESevdeQ5=uoPVlR`IC*Uu~+Ih(2ABGY?4pksYap zx-Wa-s65rcF+2ZfMWE~qpI=o58c~~OV}~=6?Sx86kye& zxlUXx{KXXiUA8~oJFsp!>6+krJWXIlx8o{I{)4)eKq8baPxd)jJjxG+1A9ztz#s8E z(}BU-Vc=qI9jDjNp!z0Tt~{DkGcxkb(TP*0+9nFU#+PFJ6ikldSsB7vhiyMF2QU^t zBDIr-uY4!Ynt?mkyt+3tW}XlQ6M-kZ0%?2VAQOmY(7I~U1nAqdVOO^%NRx`5T58tx zJ$4kEfj2?(3l}gQq+W;}%Y+-ak6g`K~SXQi_kjT~|apx!D z$0rg=9`C^ZTY@qG?+A*X=^mJrQqdxTYk2>~EW)sSmE;dz1N57qUtRQaJ%^B2`+JS-xW^0CTt4LdxXyaO_Cw!@A4r_^*X$n+g~UI+;B1LC6Fje zHXb#7fAo~`&%+T9Dxigd8AVv#>l47iLTYf;9OiBz-}Yd&KZYhD#>!OeFCCJPU_f~xH-*rMA8)Eq;H13c$2b0GW8`rzI;NT*SO|>99K~Q@}alVw( zwOcf11^;bb4R+L}Px<6i&SPBS$C9m#??D9}o5k{iUayk;$SnnzL?At_YvmRnVpvD^ zeyQ0q^ph>CJSz)-q|s6_1Wp;gCac`yj87U{%ec^V+bUy3fMr9n7`H}ZC^+w^3P(?U#*n-`MskCD`3{lNj$M^Rd8MKI|-vyOkR0Viv*&CscA&?s0sraOLN zKHfF1aN%}Uci=?J?ujd*Tk-YxYMN>!EAm|5snLrXOBEl?D-#cm60Q>F@P3VaeeGQ7 zaf+~YV2azcl2%f1;ueEe zBPXsI3FVlceUfp+eZVN-glG;SOHjcfF3IJ8^{C!Orfq`&K%(pFLe$aEPhp*oKJ0{ zx#S|xHR&!O*v_n$cC>@+U&J69uy{~dXGPB***9nH*KN2Ot7OM{9i6L^*D~A9*}Z=t zl(3DC{#g-L>T8F{(ZR}l;HVQ7`BR@CSVlY3@UuF0Rjp{x(A4z$FI)YQWx+{o*^cFo zo-?`O(xD11(@gtve+f1(OZ?_ikb%ceN1xb|11FaRJ*wWg7b#D6#{ZjsYLM}4w zA!fH9SHeoYGu!!yEntWbxI1sadv|9A3#alfUH*-E`*z}Q%W{rzFV+6dot*`Q@0KCo zgpt|rY-liXUKcRyWmr=>5_W^Yg+oHjM9ZV+);b6avJu5^hp^l}b!070eK~NqaqEI* zWLblP-@0KI4X(m&{8uc)(97)1-RX^BIaR~`+IlqRbl)y36~hK#H!wSM9I?ND<{L_; z`Rbj$I1s0jofZTT8Z>zD?rlX7q5P(!U313};s9gie&cM%)x&-xL}X?-TE&PbeRP_x zKAKru>G!HvSaQ|(im1{k)JU1Y4(Zdp)mdAQbyIyafg=mjvYG9Un;0B}$?c&Ii6|m8 zU4RswZl<@dL&ENTo#I(>JMA$e4X@3r;ChSe5`@KFc%xCEAb zdI&=k+yj)gFMY!1@L?hTfcIJ9FI#Y8W4oecu4r`?oGqCe8wEIK<(T)jc~dWCyQ2pG zcVyXYlVTXPba9}sCAs;kC3x~W>14-V_tw|&UK^DCRKC#^m(0YGe ziXA!0AdXHT_KpapL6w9Vz4~|}am$V~UCX<|uYaG`EgTmIbp^qbL4mQl*``TOE?>5{vO4?VXPpb!FT)jORX96Bj;%`+lF*nN-#)q!>@9NiL&2@jMe6d4}nE zVeVvi9@cxL$0S~Tp=&ET6z?h85$`FK$^XN@9^a}&0ULi?J(#~pSd~sP&>LGo@cHXS zBox{*VB|_fR_yFQ83eERQ=YEXhio9*$OEGfUpVcBgtKu z6_!B#=l;Ul&!gjR6yS3E#4m9wbl#}rMSac&mDtYx%(;TZZblYP8qZ0Svy!2eQP(G- zRTDcZvpOoHIUk(Dwgx?mXoif4t4@5ZL}3AyF%KbN(yO;8P^RJA zOIMpDmyCA&V z0qSp80Y_TBcWYU)YYy?;JF+<^;-@Et{6T$tuM?_HgPR$La5c?f-XA6J8LHg$VCr@k0>qFpIKe0X6>a!9V`QU=1L>kEMx?k~`Ty73ZWMRmv zt?H{()Y9Q;$tR^pSlFULn;@a#Hlv*WFZmXxSu8PLPODOY1VlM%-bY0{J@r9>t=qqJ z-j?sZz&Gk^UCcVETdX%y(A4&U%#b;29sWuD$LAn{NhrzgyF zYPq(vSG9oHnPA(xm>*fB4`)Tosf)hlf!vKdUe8U^RY1|%8em;;;=1X1bE~nX)waD% zUsY~V^EGLW4V4@Uow;r7+uOO$dxhK@IbmV&tcvZ;SUO=P=gcL>S5qsj`184I&)o>d z0|QE*kQG*5oy$6;Su7Ur?5lSogbgWJb6O(Zqa?&+90`=1v%a+TCF4b2H!9jO(W340 zZj}2=N<6!*!lvc)V+AyC7dNy>&NSTG--Eb5`WbO(p-NwJW#@6scNc@RA`$JL zVo*UW&ER()MAqaMzR05L{<91wvK0zrw(?ics$(p`eZ(g5vbKcZ=!(#oKMSKrS!b@mqv#t$_VF?-H!xtlg$)yN z+`LTn6-OR?f`T2_*Ao%>w=UQa-nNa!@=hCaM)!nG6)^st+ARi~NGrf4U(O((Bxb?+ zw`KHy>ua+Lw!;D^4KC<})y*yMsnsEhm zR0_lPM=Xm+$g67Bz8iuxb|1$u=3_P7@+&*Lb{XLuS26dGay02yN;BNub9y)x!kD)#c8ZNHN?*Kh^w{ozjy58OO_$sj-bo3=ZZ zz0d*M?I`}$&diYOF>gtv!={$pr>l=Cx=zhCSmWyWFxGt*_vD}vq*8t7|9w&C^kTJF z6KP&jeee9u&euWXgUEaJ4+YTJQQ+jH#6i`gUhsNP)yQ`3;||kYYsZ$(NR#)%O^fr{_YB4q6k^2N4sJWBZ!Uxe8HlD+52fze#3!A@ z#P9`FciDcL7UB;36HNe}kb{~d3MRR=lNXLB`W%_M#rjQW+ij!2RTpJ}Y3DmsqT-Is z&ZS=DC(-9vWqJ)4q8eo|Q83}Ui$f162WmAooy$*giK%6&3F&Fz-W`$JLE8Q1sCPC~ zvtn-kyAUmwLR=w&(CsH(!%}0op8$R*#FU%B$o6Qyy5|a1S+Kw_vTA{!Pt%U%H&g!i z!73y->_JhraRkPb`)9`E)iAgR2ZwC5g2WQ(ruT^4>LsvKBF3-(os}l6S-C4H53Ulq;G?pR(H0O`+%}Rrbmq20T8>_rhnh)TCuqA#DTE z*Rodfgcq^UbP%UOBA2Q=!-sBvOP#gY4F3Et?Nro4^mOhi5nm(;$}CtzE%+tx)%-+E zIMp^5pq>|-0jz`gQ!ui+QR}K#*Wv|B$yLw;PJ|l(&Zeq z=YmRMO&t1f%h8fb#3??z!7TQG|t@ps|9t;6wi+NO7|$H3VF8x zJMY^C|INdscePXJRt>58yCHw@?uVcxNVY4St9ny5?%F%OgZD+dMVt|v)#%V|8W}6W z#8io>AN8;e3AjJuq9gC36i@9|xOToH9Y{~wx|6_1l@aIl^mh7>+#F`AoBr0DSNfX# z`!9>F&9X!g0^HQxmo2|D^J~eq%yjAmVi{AgR`oNjL%6b!9vHMUQKG(3-@u)}L7 z#H%*EnOclXAAN159F)xyoK>zk_wx`eo@KBRmp|mt@amswj89s$!$GiM`uknqNEkY~ujyG~!<03B>#3E%oWRTdb`mxr9tW8W!^Bcq!D z0XoR0|9|suGB>#eTmcd|Kig37rz^^R4H}<8n%ICgCbg7(=C>2x%iEi+E6o)!s9db~-GP+%HMnY*wWB&Xs+baE$>N~M%XqUTt3E?kE2NG{ z?BaPYriFgR$Z1}rc#z9c&a#5(ZJ~FC>jF zkYbVc2OKY9drCT>DGda2RMW z8zR3*tRh|;4YQ?0@?!Nn9LDx-z1Fr0TT)aaJ)wQ4k9|iu` zclYX74O!(=tp8#V3wQK(7}b|~FithvTnOTZmWm}^x&I*Lx-7!RDZaHKD>R2bN!4+& zan(`5lxRR@?l~%zLWmkB)kB=_Au+!na0QZcA1al3B7T8py6>QpPEpNhpPJKGq~|LKu9oJ%>>gD8B9g!V9J?l~D3o}jXf2R{k1_U1+P?(bWaDyr%l z7pb_aMKB?X5Y*$T^Im>me}sLq8n;_2bu{kj`1-1i@rp+WLeFwZ-^ z5;ytnRu8?(p$YZ-M_@60TB~tsUDi%9=mLSXJ8rjI4C&K5qr!qA?+;rBvtGOGy=G!0 z-_&XND*y3~jp0#+(xnCo_d0B^Qq=;fttL}jM*$e(7+o}rF;&Yk`iLrs2jC~X? zuZE1pBCRvc@I_I1AGJ*|>A!$}PL0qrD=#vphGt};u{_K+w*;FvhTHEb&lO;DAT;;* zOnAXQ9a(1|*)QFGvoB|I@T@y{+i2%`b}tKz*iD~_at&d%U0DpGqirVGYH!6dsCa@9 z)#aFKXYa(9Z_vu+T%@)#(MYBh6L7Bl_-6t>4iszx3XR9Rt5WP9Gj3FtngB&-bXN)% zw78K{tcU07(+&9)xd2P{qCK9M4SA<Xl~C~SYcQ1U|tG`KQ^g@ayDU!cM9e6?^qR*?%T-(w`?)O0VipO_YlEk%oZBg z^4NCdWsO@LQq`0vb=dYI+zf2juuUOjo}?n&g?HurMI~BtN;C>x%CB(J&tmgYe(vSt zqSQNGwdFmvAk literal 0 HcmV?d00001