|
""" |
|
主UI控制器 |
|
集成所有Tab控制器和事件管理,构建完整的Gradio界面 |
|
""" |
|
|
|
from typing import Any, Dict |
|
import gradio as gr |
|
import os |
|
import sys |
|
import traceback |
|
|
|
from src.presentation.controllers.ui_controller import UIController |
|
|
|
|
|
class MainUIController(UIController): |
|
"""主UI控制器 |
|
|
|
负责协调所有Tab控制器,管理整体UI架构和事件系统 |
|
""" |
|
|
|
def __init__(self, document_service, chat_service, model_service, config_service, logger): |
|
"""初始化主UI控制器 |
|
|
|
Args: |
|
document_service: 文档处理服务 |
|
chat_service: 聊天服务 |
|
model_service: 模型管理服务 |
|
config_service: 配置服务 |
|
logger: 日志服务 |
|
""" |
|
super().__init__("main_ui") |
|
|
|
|
|
self.document_service = document_service |
|
self.chat_service = chat_service |
|
self.model_service = model_service |
|
self.config_service = config_service |
|
self.logger = logger |
|
|
|
|
|
self.demo = None |
|
|
|
def create_components(self) -> Dict[str, Any]: |
|
"""创建主界面组件""" |
|
return {} |
|
|
|
def setup_events(self) -> Dict[str, Any]: |
|
"""设置事件绑定""" |
|
return [] |
|
|
|
def build_interface(self) -> gr.Blocks: |
|
"""构建完整的Gradio界面 |
|
|
|
Returns: |
|
配置完成的Gradio Blocks实例 |
|
""" |
|
try: |
|
|
|
with gr.Blocks( |
|
title="Web RAG 系统", |
|
theme=gr.themes.Soft() |
|
) as self.demo: |
|
|
|
|
|
gr.Markdown("# 🚀 Web RAG 系统") |
|
gr.Markdown("基于 Google Gemini 的智能文档问答系统") |
|
|
|
with gr.Tabs(): |
|
|
|
with gr.TabItem("📄 文档上传", id="upload"): |
|
self._build_upload_tab() |
|
|
|
|
|
with gr.TabItem("💬 智能对话", id="chat"): |
|
self._build_chat_tab() |
|
|
|
|
|
with gr.TabItem("📊 系统状态", id="status"): |
|
self._build_status_tab() |
|
|
|
|
|
self._bind_upload_events() |
|
self._bind_chat_events() |
|
self._bind_status_events() |
|
self._bind_load_events() |
|
|
|
return self.demo |
|
|
|
except Exception as e: |
|
print(f"❌ 构建界面失败: {e}") |
|
print(f"错误详情: {traceback.format_exc()}") |
|
return self._create_error_interface(e) |
|
|
|
def _build_upload_tab(self): |
|
"""构建上传Tab""" |
|
gr.Markdown("## 上传文档") |
|
gr.Markdown("**支持格式**: PDF、Word(.docx)、Excel(.xlsx)、PowerPoint(.pptx)、Markdown(.md)、文本(.txt)") |
|
gr.Markdown("注意: 上传后请等待处理完成,状态会显示在下方") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
self.upload_file = gr.File( |
|
label="📄 选择文档文件", |
|
file_types=[".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md"], |
|
type="filepath" |
|
) |
|
with gr.Column(scale=1): |
|
with gr.Group(): |
|
gr.Markdown("### 🤖 模型配置") |
|
|
|
|
|
available_models = self.config_service.get_value("fallback_models") |
|
current_model = self.model_service.get_current_model() |
|
|
|
self.model_dropdown = gr.Dropdown( |
|
label="选择 Gemini 模型", |
|
choices=available_models, |
|
value=current_model, |
|
interactive=True |
|
) |
|
|
|
gr.Markdown("### 处理状态") |
|
self.upload_status = gr.Textbox( |
|
label="状态", |
|
value="等待文件上传...", |
|
interactive=False |
|
) |
|
|
|
self.uploaded_files_display = gr.Markdown("### 暂无已上传文件") |
|
|
|
def _build_chat_tab(self): |
|
"""构建聊天Tab""" |
|
gr.Markdown("## 与文档内容对话") |
|
gr.Markdown("提示: 请先上传并处理文档文件,然后在此提问") |
|
|
|
self.chatbot = gr.Chatbot( |
|
label="对话历史", |
|
height=400 |
|
) |
|
|
|
with gr.Row(): |
|
self.msg = gr.Textbox( |
|
label="输入您的问题", |
|
placeholder="请输入您想要询问的问题...", |
|
lines=3, |
|
scale=4 |
|
) |
|
with gr.Column(scale=1): |
|
self.send_btn = gr.Button("发送", variant="primary") |
|
self.clear_btn = gr.Button("清除对话") |
|
|
|
def _build_status_tab(self): |
|
"""构建状态Tab""" |
|
gr.Markdown("## 🔧 系统状态") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
self.refresh_btn = gr.Button("🔄 刷新状态", variant="primary") |
|
with gr.Column(scale=2): |
|
pass |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
self.system_status = gr.Markdown("🔄 正在获取系统状态...") |
|
with gr.Column(scale=1): |
|
self.model_info = gr.Markdown("🔄 正在获取模型信息...") |
|
|
|
gr.Markdown("## 🔧 技术栈") |
|
gr.Markdown(""" |
|
**LLM**: Google Gemini (自动选择可用模型) |
|
|
|
**嵌入模型**: Google Embedding-001 |
|
|
|
**向量数据库**: ChromaDB |
|
|
|
**框架**: LangChain + Gradio |
|
""") |
|
|
|
def _bind_upload_events(self): |
|
"""绑定上传Tab事件""" |
|
self.upload_file.upload( |
|
fn=self._process_document_with_model, |
|
inputs=[self.upload_file, self.model_dropdown], |
|
outputs=[self.upload_status, self.uploaded_files_display] |
|
) |
|
|
|
def _bind_chat_events(self): |
|
"""绑定聊天Tab事件""" |
|
self.send_btn.click( |
|
fn=self._chat_with_documents, |
|
inputs=[self.msg, self.chatbot], |
|
outputs=[self.chatbot, self.msg] |
|
) |
|
|
|
self.msg.submit( |
|
fn=self._chat_with_documents, |
|
inputs=[self.msg, self.chatbot], |
|
outputs=[self.chatbot, self.msg] |
|
) |
|
|
|
self.clear_btn.click( |
|
fn=self._clear_chat, |
|
outputs=[self.chatbot] |
|
) |
|
|
|
def _bind_status_events(self): |
|
"""绑定状态Tab事件""" |
|
self.refresh_btn.click( |
|
fn=lambda: (self._get_system_status(), self._get_model_info()), |
|
outputs=[self.system_status, self.model_info] |
|
) |
|
|
|
def _bind_load_events(self): |
|
"""绑定页面加载事件""" |
|
|
|
self.demo.load( |
|
fn=lambda: (self._get_system_status(), self._get_model_info()), |
|
outputs=[self.system_status, self.model_info] |
|
) |
|
|
|
|
|
self.demo.load( |
|
fn=self._get_uploaded_files_display, |
|
outputs=[self.uploaded_files_display] |
|
) |
|
|
|
|
|
self.demo.load( |
|
fn=self._init_model_dropdown, |
|
outputs=[self.model_dropdown] |
|
) |
|
|
|
|
|
def _process_document_with_model(self, file_path, selected_model): |
|
"""处理文档上传并指定模型""" |
|
try: |
|
if not file_path: |
|
return "❌ 请先选择文件", "### 暂无已上传文件" |
|
|
|
if not selected_model: |
|
return "❌ 请先选择模型", "### 暂无已上传文件" |
|
|
|
self.logger.info(f"开始处理文档: {file_path}, 模型: {selected_model}") |
|
|
|
|
|
if hasattr(self.model_service, 'switch_model'): |
|
self.model_service.switch_model(selected_model) |
|
|
|
|
|
result_message = self.document_service.process_document(file_path) |
|
|
|
|
|
updated_files_display = self._get_uploaded_files_display() |
|
|
|
return result_message, updated_files_display |
|
|
|
except Exception as e: |
|
self.logger.error(f"文档处理失败: {e}") |
|
return f"❌ 处理失败: {str(e)}", "### 暂无已上传文件" |
|
|
|
def _chat_with_documents(self, message, history): |
|
"""与文档对话""" |
|
try: |
|
if not message or not message.strip(): |
|
history = history or [] |
|
history.append(["", "❌ 请输入内容"]) |
|
return history, "" |
|
|
|
|
|
from src.shared.state.application_state import get_application_state |
|
app_state = get_application_state() |
|
if app_state.get_uploaded_files_count() == 0: |
|
history = history or [] |
|
history.append([message, "❌ 请先上传文档文件"]) |
|
return history, "" |
|
|
|
self.logger.info(f"处理用户问题: {message}") |
|
|
|
|
|
response, updated_history = self.chat_service.chat_with_pdf(message, history or []) |
|
|
|
return updated_history, "" |
|
|
|
except Exception as e: |
|
self.logger.error(f"对话处理失败: {e}") |
|
error_response = f"抱歉,处理您的问题时发生错误: {str(e)}" |
|
history = history or [] |
|
history.append([message, error_response]) |
|
return history, "" |
|
|
|
def _clear_chat(self): |
|
"""清空对话""" |
|
try: |
|
self.chat_service.clear_conversation_history() |
|
return [] |
|
except Exception as e: |
|
self.logger.error(f"清空对话失败: {e}") |
|
return [] |
|
|
|
def _get_system_status(self): |
|
"""获取系统状态""" |
|
try: |
|
from src.shared.state.application_state import get_application_state |
|
|
|
app_state = get_application_state() |
|
status_info = app_state.get_status_info() |
|
|
|
|
|
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" |
|
|
|
|
|
current_dir = os.getcwd() |
|
|
|
|
|
api_icon = "✅" if os.getenv('GOOGLE_API_KEY') else "❌" |
|
vectorstore_icon = "✅" if status_info['vectorstore_initialized'] else "❌" |
|
qa_chain_icon = "✅" if status_info['qa_chain_initialized'] else "❌" |
|
|
|
|
|
total_chunks = self._get_total_chunks_count() |
|
|
|
status_md = f""" |
|
## 📊 系统状态 |
|
|
|
**Python 版本**: {python_version} |
|
|
|
**工作目录**: {current_dir} |
|
|
|
**API 密钥**: {api_icon} {'已配置' if os.getenv('GOOGLE_API_KEY') else '未配置'} |
|
|
|
**当前模型**: {status_info['current_model']} |
|
|
|
**向量数据库**: {vectorstore_icon} {'已加载' if status_info['vectorstore_initialized'] else '未加载'} ({total_chunks}) |
|
|
|
**QA 链**: {qa_chain_icon} {'已初始化' if status_info['qa_chain_initialized'] else '未初始化'} |
|
|
|
## 📋 使用说明 |
|
|
|
1. 在"文档上传"标签页上传文档文件 (支持PDF、Word、Excel、PPT、Markdown、文本) |
|
2. 等待处理完成(查看状态信息) |
|
3. 在"智能对话"标签页提问 |
|
4. 系统会基于文档内容回答问题 |
|
""" |
|
return status_md |
|
|
|
except Exception as e: |
|
return f"❌ 获取状态失败: {str(e)}" |
|
|
|
def _get_model_info(self): |
|
"""获取模型信息""" |
|
try: |
|
from src.shared.state.application_state import get_application_state |
|
app_state = get_application_state() |
|
status_info = app_state.get_status_info() |
|
|
|
model_md = f""" |
|
## 🤖 可用模型列表 |
|
|
|
**默认模型**: {status_info['current_model']} |
|
|
|
**所有可用模型**: |
|
{chr(10).join([f'- {model}' for model in status_info['available_models']])} |
|
|
|
**模型说明**: |
|
- **2.5 系列**: 最新预览版本,性能最佳 |
|
- **2.0 系列**: 稳定版本,生产推荐 |
|
- **1.5 系列**: 备用版本,确保可用性 |
|
""" |
|
return model_md |
|
|
|
except Exception as e: |
|
return f"❌ 获取模型信息失败: {str(e)}" |
|
|
|
def _get_uploaded_files_display(self): |
|
"""获取已上传文件的显示内容""" |
|
try: |
|
from src.shared.state.application_state import get_application_state |
|
app_state = get_application_state() |
|
files = app_state.get_uploaded_files() |
|
|
|
if not files: |
|
return "### 暂无已上传文件" |
|
|
|
file_list = ["### 📁 已上传文件\n"] |
|
|
|
for i, file_info in enumerate(files, 1): |
|
upload_time = file_info.upload_time.strftime("%Y-%m-%d %H:%M:%S") |
|
file_list.append( |
|
f"**{i}. {file_info.name}**\n" |
|
f"- 📅 上传时间: {upload_time}\n" |
|
f"- 📄 页数: {file_info.pages}\n" |
|
f"- 📝 文档片段: {file_info.chunks}\n" |
|
f"- 🤖 处理模型: {file_info.model}\n" |
|
) |
|
|
|
return "\n".join(file_list) |
|
|
|
except Exception as e: |
|
self.logger.error(f"获取文件列表失败: {e}") |
|
return f"### ❌ 获取文件列表失败: {str(e)}" |
|
|
|
def _get_total_chunks_count(self): |
|
"""获取总文档片段数量""" |
|
try: |
|
from src.shared.state.application_state import get_application_state |
|
app_state = get_application_state() |
|
uploaded_files = app_state.get_uploaded_files() |
|
return sum(f.chunks for f in uploaded_files) if uploaded_files else 0 |
|
except: |
|
return 0 |
|
|
|
def _init_model_dropdown(self): |
|
"""初始化模型下拉框的默认值""" |
|
try: |
|
current_model = self.model_service.get_current_model() |
|
return current_model |
|
except Exception as e: |
|
self.logger.error(f"获取默认模型失败: {e}") |
|
return None |
|
|
|
def _create_error_interface(self, error) -> gr.Blocks: |
|
"""创建错误界面 |
|
|
|
Args: |
|
error: 错误信息 |
|
|
|
Returns: |
|
显示错误的Gradio界面 |
|
""" |
|
with gr.Blocks(title="Web RAG 系统 - 错误") as error_demo: |
|
gr.Markdown("# ❌ 系统错误") |
|
gr.Markdown(f"**错误信息**: {str(error)}") |
|
gr.Markdown("**解决方案**: 请检查依赖配置和环境设置") |
|
|
|
return error_demo |
|
|
|
def launch(self, **kwargs) -> None: |
|
"""启动界面 |
|
|
|
Args: |
|
**kwargs: Gradio launch参数 |
|
""" |
|
if self.demo is None: |
|
print("❌ 界面未构建,请先调用 build_interface()") |
|
return |
|
|
|
try: |
|
print("🚀 启动 Web RAG 系统...") |
|
print(f"📋 API 密钥状态: {'✅ 已配置' if os.getenv('GOOGLE_API_KEY') else '❌ 未配置'}") |
|
print(f"🏗️ 架构: 现代化分层架构 + 性能优化 + 扩展性增强") |
|
print(f"🎯 当前模型: {self.model_service.get_current_model()}") |
|
|
|
|
|
is_spaces = os.getenv("SPACE_ID") is not None |
|
|
|
if is_spaces: |
|
|
|
default_kwargs = {"share": True} |
|
else: |
|
|
|
default_kwargs = { |
|
"server_name": "127.0.0.1", |
|
"server_port": 7860, |
|
"share": False, |
|
"show_error": True, |
|
"inbrowser": False, |
|
"debug": True |
|
} |
|
|
|
|
|
launch_kwargs = {**default_kwargs, **kwargs} |
|
|
|
|
|
self.demo.launch(**launch_kwargs) |
|
|
|
except Exception as e: |
|
print(f"❌ 启动失败: {e}") |
|
print(f"错误详情: {traceback.format_exc()}") |
|
|
|
def get_demo(self) -> gr.Blocks: |
|
"""获取Gradio demo实例 |
|
|
|
Returns: |
|
Gradio Blocks实例 |
|
""" |
|
return self.demo |