openfree's picture
Rename app-backup2.py to app.py
3e1eea4 verified
import spaces
import json
import subprocess
import os
from llama_cpp import Llama
from llama_cpp_agent import LlamaCppAgent, MessagesFormatterType
from llama_cpp_agent.providers import LlamaCppPythonProvider
from llama_cpp_agent.chat_history import BasicChatHistory
from llama_cpp_agent.chat_history.messages import Roles
import gradio as gr
from huggingface_hub import hf_hub_download
import tempfile
from typing import List, Tuple, Optional
# PDF 처리 라이브러리 조건부 import
try:
from docling.document_converter import DocumentConverter
DOCLING_AVAILABLE = True
except ImportError:
DOCLING_AVAILABLE = False
print("Docling not available, using alternative PDF processing")
try:
import PyPDF2
import pdfplumber
except ImportError:
print("Warning: PDF processing libraries not fully installed")
# 환경 변수에서 HF_TOKEN 가져오기
HF_TOKEN = os.getenv("HF_TOKEN")
# 전역 변수 초기화 (중요!)
llm = None
llm_model = None
document_context = "" # PDF에서 추출한 문서 컨텍스트 저장
document_filename = "" # 현재 로드된 문서의 파일명
print("전역 변수 초기화 완료")
print(f"document_context 초기값: '{document_context}'")
print(f"document_filename 초기값: '{document_filename}'")
# 모델 이름과 경로를 정의
MISTRAL_MODEL_NAME = "Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503.gguf"
# 모델 다운로드 (HF_TOKEN 사용)
model_path = hf_hub_download(
repo_id="ginigen/Private-BitSix-Mistral-Small-3.1-24B-Instruct-2503",
filename=MISTRAL_MODEL_NAME,
local_dir="./models",
token=HF_TOKEN
)
print(f"Downloaded model path: {model_path}")
css = """
.bubble-wrap {
padding-top: calc(var(--spacing-xl) * 3) !important;
}
.message-row {
justify-content: space-evenly !important;
width: 100% !important;
max-width: 100% !important;
margin: calc(var(--spacing-xl)) 0 !important;
padding: 0 calc(var(--spacing-xl) * 3) !important;
}
.flex-wrap.user {
border-bottom-right-radius: var(--radius-lg) !important;
}
.flex-wrap.bot {
border-bottom-left-radius: var(--radius-lg) !important;
}
.message.user{
padding: 10px;
}
.message.bot{
text-align: right;
width: 100%;
padding: 10px;
border-radius: 10px;
}
.message-bubble-border {
border-radius: 6px !important;
}
.message-buttons {
justify-content: flex-end !important;
}
.message-buttons-left {
align-self: end !important;
}
.message-buttons-bot, .message-buttons-user {
right: 10px !important;
left: auto !important;
bottom: 2px !important;
}
.dark.message-bubble-border {
border-color: #343140 !important;
}
.dark.user {
background: #1e1c26 !important;
}
.dark.assistant.dark, .dark.pending.dark {
background: #16141c !important;
}
.upload-container {
margin-bottom: 20px;
padding: 15px;
border: 2px dashed #666;
border-radius: 10px;
background-color: #f0f0f0;
}
.dark .upload-container {
background-color: #292733;
border-color: #444;
}
"""
def get_messages_formatter_type(model_name):
if "Mistral" in model_name or "BitSix" in model_name:
return MessagesFormatterType.MISTRAL # CHATML 대신 MISTRAL 형식 사용
else:
raise ValueError(f"Unsupported model: {model_name}")
@spaces.GPU
def convert_pdf_to_markdown(file):
"""PDF 파일을 Markdown으로 변환"""
global document_context, document_filename
if file is None:
return "파일이 업로드되지 않았습니다.", {}
try:
print(f"\n=== PDF 변환 시작 ===")
print(f"파일 경로: {file.name}")
# DocumentConverter 인스턴스 생성
converter = DocumentConverter()
# 파일 변환
result = converter.convert(file.name)
# Markdown으로 내보내기
markdown_content = result.document.export_to_markdown()
# 문서 컨텍스트 업데이트 (중요!)
document_context = markdown_content
document_filename = os.path.basename(file.name)
# 메타데이터 추출
metadata = {
"filename": document_filename,
"conversion_status": "success",
"content_length": len(markdown_content),
"preview": markdown_content[:500] + "..." if len(markdown_content) > 500 else markdown_content
}
print(f"✅ PDF 변환 성공!")
print(f"📄 파일명: {document_filename}")
print(f"📏 문서 길이: {len(markdown_content)} 문자")
print(f"📝 문서 시작 300자:\n{markdown_content[:300]}...")
print(f"=== PDF 변환 완료 ===\n")
# 전역 변수 확인 및 강제 설정
print(f"\n=== 전역 변수 설정 전 ===")
print(f"global document_context 길이: {len(document_context)}")
print(f"global document_filename: {document_filename}")
# globals() 함수를 사용하여 강제로 전역 변수 설정
globals()['document_context'] = markdown_content
globals()['document_filename'] = document_filename
print(f"\n=== 전역 변수 설정 후 ===")
print(f"global document_context 길이: {len(globals()['document_context'])}")
print(f"global document_filename: {globals()['document_filename']}")
return markdown_content, metadata
except Exception as e:
error_msg = f"PDF 변환 중 오류 발생: {str(e)}"
print(f"❌ {error_msg}")
document_context = ""
document_filename = ""
return error_msg, {"error": str(e)}
def find_relevant_chunks(document, query, chunk_size=1500, overlap=300):
"""문서에서 질문과 관련된 청크 찾기"""
if not document:
return ""
print(f"관련 청크 찾기 시작 - 쿼리: {query}")
# 간단한 키워드 기반 검색
query_words = query.lower().split()
chunks = []
# 문서를 청크로 나누기
for i in range(0, len(document), chunk_size - overlap):
chunk = document[i:i + chunk_size]
chunks.append((i, chunk))
print(f"총 {len(chunks)}개의 청크로 분할됨")
# 각 청크의 관련성 점수 계산
scored_chunks = []
for idx, chunk in chunks:
chunk_lower = chunk.lower()
score = sum(1 for word in query_words if word in chunk_lower)
if score > 0:
scored_chunks.append((score, idx, chunk))
# 상위 2개 청크 선택 (메모리 절약)
scored_chunks.sort(reverse=True, key=lambda x: x[0])
relevant_chunks = scored_chunks[:2]
if relevant_chunks:
result = ""
for score, idx, chunk in relevant_chunks:
result += f"\n[문서의 {idx}번째 위치에서 발췌 - 관련도: {score}]\n{chunk}\n"
print(f"{len(relevant_chunks)}개의 관련 청크 찾음")
return result
else:
# 관련 청크를 찾지 못한 경우 문서 시작 부분 반환
print("관련 청크를 찾지 못함, 문서 시작 부분 반환")
return document[:2000]
@spaces.GPU(duration=120)
def respond(
message,
history: list[dict],
system_message,
max_tokens,
temperature,
top_p,
top_k,
repeat_penalty,
):
global llm, llm_model
# globals()를 사용하여 전역 변수에 접근
document_context = globals().get('document_context', '')
document_filename = globals().get('document_filename', '')
# 디버깅을 위한 상세 로그
print(f"\n=== RESPOND 함수 시작 ===")
print(f"사용자 메시지: {message}")
print(f"문서 컨텍스트 존재 여부: {bool(document_context)}")
if document_context:
print(f"문서 길이: {len(document_context)}")
print(f"문서 파일명: {document_filename}")
print(f"문서 시작 100자: {document_context[:100]}...")
else:
print("⚠️ document_context가 비어있습니다!")
print(f"globals()의 키들: {list(globals().keys())[:20]}...") # 처음 20개 키만
chat_template = get_messages_formatter_type(MISTRAL_MODEL_NAME)
# 모델 파일 경로 확인
model_path_local = os.path.join("./models", MISTRAL_MODEL_NAME)
if llm is None or llm_model != MISTRAL_MODEL_NAME:
print("LLM 모델 로딩 중...")
llm = Llama(
model_path=model_path_local,
flash_attn=True,
n_gpu_layers=81,
n_batch=1024,
n_ctx=16384, # 컨텍스트 크기
verbose=True # 디버깅을 위한 상세 로그
)
llm_model = MISTRAL_MODEL_NAME
print("LLM 모델 로딩 완료!")
provider = LlamaCppPythonProvider(llm)
# 한국어 답변을 위한 기본 시스템 메시지
korean_system_message = system_message # 사용자가 설정한 시스템 메시지 사용
# 문서 컨텍스트가 있으면 시스템 메시지와 사용자 메시지 모두에 포함
if document_context and len(document_context) > 0:
doc_length = len(document_context)
print(f"📄 문서 컨텍스트를 메시지에 포함합니다: {doc_length} 문자")
# 시스템 메시지에도 문서 정보 추가
korean_system_message += f"\n\n현재 '{document_filename}' PDF 문서가 로드되어 있습니다. 사용자의 모든 질문에 대해 이 문서의 내용을 반드시 참조하여 답변하세요."
# 문서 내용을 적절한 크기로 제한
max_doc_length = 4000 # 최대 4000자로 제한
if doc_length > max_doc_length:
# 문서가 너무 긴 경우 처음과 끝 부분만 포함
doc_snippet = document_context[:2000] + "\n\n[... 중간 내용 생략 ...]\n\n" + document_context[-1500:]
enhanced_message = f"""업로드된 PDF 문서 정보:
- 파일명: {document_filename}
- 문서 길이: {doc_length} 문자
문서 내용 (일부):
{doc_snippet}
사용자 질문: {message}
위 문서를 참고하여 한국어로 답변해주세요."""
else:
# 짧은 문서는 전체 포함
enhanced_message = f"""업로드된 PDF 문서 정보:
- 파일명: {document_filename}
- 문서 길이: {doc_length} 문자
문서 내용:
{document_context}
사용자 질문: {message}
위 문서를 참고하여 한국어로 답변해주세요."""
print(f"강화된 메시지 길이: {len(enhanced_message)}")
print(f"메시지 미리보기 (처음 300자):\n{enhanced_message[:300]}...")
# 디버그: 최종 메시지 파일로 저장 (확인용)
with open("debug_last_message.txt", "w", encoding="utf-8") as f:
f.write(f"=== 디버그 정보 ===\n")
f.write(f"문서 길이: {len(document_context)}\n")
f.write(f"파일명: {document_filename}\n")
f.write(f"사용자 질문: {message}\n")
f.write(f"\n=== 전송될 메시지 ===\n")
f.write(enhanced_message)
else:
# 문서가 없는 경우
enhanced_message = message
if any(keyword in message.lower() for keyword in ["문서", "pdf", "업로드", "파일", "내용", "요약"]):
enhanced_message = f"{message}\n\n[시스템 메시지: 현재 업로드된 PDF 문서가 없습니다. PDF 파일을 먼저 업로드해주세요.]"
print("문서 관련 질문이지만 문서가 없음")
# 디버그 메시지
print("⚠️ 경고: document_context가 비어있습니다!")
print(f"document_context 타입: {type(document_context)}")
print(f"document_context 값: {repr(document_context)}")
print(f"document_filename: {document_filename}")
settings = provider.get_provider_default_settings()
settings.temperature = temperature
settings.top_k = top_k
settings.top_p = top_p
settings.max_tokens = max_tokens
settings.repeat_penalty = repeat_penalty
settings.stream = True
# 시스템 프롬프트에 문서 내용 직접 포함 (문서가 있는 경우)
if document_context and len(document_context) > 0:
doc_snippet = document_context[:3000] # 처음 3000자만 사용
enhanced_system_prompt = f"""{korean_system_message}
현재 로드된 PDF 문서:
파일명: {document_filename}
문서 내용:
{doc_snippet}
{'' if len(document_context) <= 3000 else '... (이하 생략)'}
위 문서의 내용을 바탕으로 사용자의 질문에 답변하세요."""
# 사용자 메시지는 단순하게
final_message = message
else:
enhanced_system_prompt = korean_system_message
final_message = enhanced_message
agent = LlamaCppAgent(
provider,
system_prompt=enhanced_system_prompt,
predefined_messages_formatter_type=chat_template,
debug_output=True
)
messages = BasicChatHistory()
# 이전 대화 기록 추가 (수정됨)
for i in range(0, len(history)):
# 현재 메시지는 제외
if i < len(history) - 1 and history[i][1] is not None:
# 사용자 메시지
messages.add_message({
'role': Roles.user,
'content': history[i][0]
})
# 어시스턴트 메시지
messages.add_message({
'role': Roles.assistant,
'content': history[i][1]
})
print(f"최종 메시지 전송 중: {final_message}")
# 스트림 응답 생성
try:
stream = agent.get_chat_response(
final_message, # 단순한 메시지 사용
llm_sampling_settings=settings,
chat_history=messages,
returns_streaming_generator=True,
print_output=False
)
outputs = ""
for output in stream:
outputs += output
yield outputs
except Exception as e:
print(f"스트림 생성 중 오류: {e}")
yield "죄송합니다. 응답 생성 중 오류가 발생했습니다. 다시 시도해주세요."
def clear_document_context():
"""문서 컨텍스트 초기화"""
global document_context, document_filename
document_context = ""
document_filename = ""
return "📭 문서 컨텍스트가 초기화되었습니다. 새로운 PDF를 업로드해주세요."
def check_document_status():
"""현재 문서 상태 확인"""
global document_context, document_filename
print(f"\n=== 문서 상태 확인 ===")
print(f"document_context 타입: {type(document_context)}")
print(f"document_context 길이: {len(document_context) if document_context else 0}")
print(f"document_filename: '{document_filename}'")
if document_context and len(document_context) > 0:
status = f"✅ 문서가 로드되어 있습니다.\n📄 파일명: {document_filename}\n📏 문서 길이: {len(document_context):,} 문자"
print(f"문서 첫 100자: {document_context[:100]}")
return status
else:
return "📭 로드된 문서가 없습니다. PDF 파일을 업로드해주세요."
# Gradio 인터페이스 구성
with gr.Blocks(theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="cyan",
neutral_hue="gray",
font=[gr.themes.GoogleFont("Exo"), "ui-sans-serif", "system-ui", "sans-serif"]
).set(
body_background_fill="#f8f9fa",
block_background_fill="#ffffff",
block_border_width="1px",
block_title_background_fill="#e9ecef",
input_background_fill="#ffffff",
button_secondary_background_fill="#e9ecef",
border_color_accent="#dee2e6",
border_color_primary="#ced4da",
background_fill_secondary="#f8f9fa",
color_accent_soft="transparent",
code_background_fill="#f1f3f5",
), css=css) as demo:
gr.Markdown("# 온프레미스 최적화 'LLM+RAG 모델' 서비스 by VIDraft")
gr.Markdown("📄 PDF 문서를 업로드하면 AI가 문서 내용을 분석하여 질문에 답변합니다.")
gr.Markdown("💡 사용법: 1) 아래에서 PDF 업로드 → 2) 문서에 대한 질문 입력 → 3) AI가 한국어로 답변")
# 채팅 인터페이스를 위쪽에 배치
with gr.Row():
with gr.Column():
# 채팅 인터페이스
chatbot = gr.Chatbot(elem_id="chatbot", height=500)
msg = gr.Textbox(
label="메시지 입력",
placeholder="질문을 입력하세요... (PDF를 업로드하면 문서 내용에 대해 질문할 수 있습니다)",
lines=2
)
with gr.Row():
submit = gr.Button("전송", variant="primary")
clear_chat = gr.Button("대화 초기화")
# 예제를 중간에 배치
gr.Examples(
examples=[
["이 문서는 무엇에 관한 내용인가요?"],
["업로드한 PDF 문서의 주요 내용을 한국어로 요약해주세요."],
["문서에 나온 일정을 알려주세요."],
["문서에서 가장 중요한 3가지 핵심 포인트는 무엇인가요?"],
["이 행사의 개요를 설명해주세요."]
],
inputs=msg
)
# PDF 업로드 섹션을 아래쪽에 배치
with gr.Accordion("📄 PDF 문서 업로드", open=True):
with gr.Row():
with gr.Column(scale=1):
file_input = gr.File(
label="PDF 문서 선택",
file_types=[".pdf"],
type="filepath"
)
with gr.Row():
convert_button = gr.Button("문서 변환", variant="primary")
clear_button = gr.Button("문서 초기화", variant="secondary")
test_button = gr.Button("문서 테스트", variant="secondary")
status_text = gr.Textbox(
label="문서 상태",
interactive=False,
value=check_document_status(),
lines=3
)
with gr.Column(scale=1):
with gr.Accordion("변환된 문서 미리보기", open=False):
converted_text = gr.Textbox(
label="Markdown 변환 결과",
lines=10,
max_lines=20,
interactive=False
)
metadata_output = gr.JSON(label="메타데이터")
# 고급 설정을 가장 아래에 배치
with gr.Accordion("⚙️ 고급 설정", open=False):
system_message = gr.Textbox(
value="당신은 한국어로 답변하는 AI 어시스턴트입니다. PDF 문서가 제공되면 그 내용을 정확히 분석하여 답변합니다.",
label="시스템 메시지",
lines=3
)
max_tokens = gr.Slider(minimum=1, maximum=4096, value=2048, step=1, label="최대 토큰 수")
temperature = gr.Slider(minimum=0.1, maximum=4.0, value=0.3, step=0.1, label="Temperature (낮을수록 일관성 있음)")
top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.90, step=0.05, label="Top-p")
top_k = gr.Slider(minimum=0, maximum=100, value=40, step=1, label="Top-k")
repeat_penalty = gr.Slider(minimum=0.0, maximum=2.0, value=1.1, step=0.1, label="Repetition penalty")
# 이벤트 핸들러
def user_submit(message, history):
return "", history + [[message, None]]
def bot_response(history, system_msg, max_tok, temp, top_p_val, top_k_val, rep_pen):
if history and history[-1][1] is None:
user_message = history[-1][0]
# 디버깅: 문서 컨텍스트 상태 확인
global document_context, document_filename
print(f"\n=== BOT RESPONSE 시작 ===")
print(f"사용자 메시지: {user_message}")
if document_context:
print(f"📄 문서 컨텍스트 활성: {document_filename} ({len(document_context)} 문자)")
print(f"문서 첫 200자: {document_context[:200]}...")
else:
print("📭 문서 컨텍스트 없음")
# 단순한 형식 사용 - [user_message, assistant_message]
previous_history = []
for i in range(len(history) - 1):
if history[i][1] is not None:
previous_history.append({
"user": history[i][0],
"assistant": history[i][1]
})
print(f"이전 대화 수: {len(previous_history)}")
# 문서가 있는 경우 특별 처리
if document_context and len(document_context) > 0:
print(f"📄 문서 기반 응답 생성 중... (문서 길이: {len(document_context)})")
bot_message = ""
try:
for token in respond(
user_message,
previous_history,
system_msg,
max_tok,
temp,
top_p_val,
top_k_val,
rep_pen
):
bot_message = token
history[-1][1] = bot_message
yield history
except Exception as e:
print(f"❌ 응답 생성 중 오류: {e}")
import traceback
traceback.print_exc()
history[-1][1] = "죄송합니다. 응답 생성 중 오류가 발생했습니다. 다시 시도해주세요."
yield history
# PDF 변환 이벤트
def on_pdf_convert(file):
"""PDF 변환 및 상태 업데이트"""
global document_context, document_filename
if file is None:
return "", {}, "❌ 파일이 선택되지 않았습니다."
markdown_content, metadata = convert_pdf_to_markdown(file)
if "error" in metadata:
status = f"❌ 변환 실패: {metadata['error']}"
else:
# 전역 변수 다시 한번 확인 및 설정 (globals() 사용)
globals()['document_context'] = markdown_content
globals()['document_filename'] = metadata['filename']
status = f"✅ PDF 문서가 성공적으로 변환되었습니다!\n📄 파일명: {metadata['filename']}\n📏 문서 길이: {metadata['content_length']:,} 문자\n\n이제 문서 내용에 대해 한국어로 질문하실 수 있습니다.\n\n예시 질문:\n- 이 문서의 주요 내용을 요약해주세요\n- 문서에 나온 핵심 개념을 설명해주세요"
print(f"\n✅ 문서 로드 완료 확인:")
print(f"- globals()['document_context'] 길이: {len(globals()['document_context'])}")
print(f"- globals()['document_filename']: {globals()['document_filename']}")
# 최종 확인
if len(globals()['document_context']) > 0:
print("✅ 문서가 성공적으로 전역 변수에 저장되었습니다!")
else:
print("❌ 경고: 문서가 전역 변수에 저장되지 않았습니다!")
return markdown_content, metadata, status
# 파일 업로드 시 자동 변환
file_input.change(
fn=on_pdf_convert,
inputs=[file_input],
outputs=[converted_text, metadata_output, status_text]
)
# 수동 변환 버튼
convert_button.click(
fn=on_pdf_convert,
inputs=[file_input],
outputs=[converted_text, metadata_output, status_text]
)
# 문서 테스트 함수
def test_document():
"""현재 로드된 문서 테스트"""
global document_context, document_filename
if document_context:
test_msg = f"✅ 문서 테스트 결과:\n"
test_msg += f"📄 파일명: {document_filename}\n"
test_msg += f"📏 전체 길이: {len(document_context):,} 문자\n"
test_msg += f"📝 첫 500자:\n{document_context[:500]}..."
return test_msg
else:
return "❌ 현재 로드된 문서가 없습니다."
test_button.click(
fn=test_document,
outputs=[status_text]
)
clear_button.click(
fn=clear_document_context,
outputs=[status_text]
).then(
fn=lambda: ("", {}, check_document_status()),
outputs=[converted_text, metadata_output, status_text]
)
# 채팅 이벤트
msg.submit(user_submit, [msg, chatbot], [msg, chatbot]).then(
bot_response,
[chatbot, system_message, max_tokens, temperature, top_p, top_k, repeat_penalty],
chatbot
)
submit.click(user_submit, [msg, chatbot], [msg, chatbot]).then(
bot_response,
[chatbot, system_message, max_tokens, temperature, top_p, top_k, repeat_penalty],
chatbot
)
clear_chat.click(lambda: [], None, chatbot)
if __name__ == "__main__":
# 필요한 디렉토리 생성
os.makedirs("./models", exist_ok=True)
# 환경 변수 확인
if not HF_TOKEN:
print("⚠️ 경고: HF_TOKEN이 설정되지 않았습니다. 모델 다운로드에 제한이 있을 수 있습니다.")
print("환경 변수를 설정하려면: export HF_TOKEN='your_huggingface_token'")
demo.launch(
server_name="0.0.0.0", # 로컬 네트워크에서 접근 가능
server_port=7860,
share=False # 온프레미스 환경이므로 공유 비활성화
)