diff --git a/HUONG_DAN_FILE_ANALYZER.md b/HUONG_DAN_FILE_ANALYZER.md new file mode 100644 index 0000000..cb4f18f --- /dev/null +++ b/HUONG_DAN_FILE_ANALYZER.md @@ -0,0 +1,378 @@ +# 🤖 HƯỚNG DẪN SỬ DỤNG AI FILE ANALYZER CHATBOT + +## 📋 Tổng quan + +Chatbot có thể nhận và phân tích: +- 📄 **File TXT**: Đọc trực tiếp nội dung +- 🖼️ **File PNG/JPG**: Dùng OCR nhận diện text (hỗ trợ tiếng Việt + tiếng Anh) + +**Model AI:** `qwen/qwen3-vl-235b-a22b-thinking` - Model AI mạnh mẽ từ Qwen + +--- + +## 🚀 Cách chạy + +### Cách 1: Double-click file BAT (Dễ nhất) +``` +Double-click: run_file_analyzer.bat +``` +- ✅ Tự động cài đặt thư viện nếu thiếu +- ✅ Tự động mở trình duyệt +- ✅ Đơn giản nhất + +### Cách 2: Chạy bằng Python +```bash +# Bước 1: Cài đặt thư viện (chỉ lần đầu) +pip install gradio easyocr openai pillow + +# Bước 2: Chạy chatbot +python chatbot_file_analyzer.py +``` + +--- + +## 💻 Giao diện Chatbot + +### Truy cập chatbot tại: +``` +http://localhost:7860 +``` + +### Layout: +``` +┌─────────────────────────────────────────────────────┐ +│ 🤖 AI File Analyzer Chatbot │ +├──────────────────────────┬──────────────────────────┤ +│ 💬 Chat với AI │ 📝 Nội dung trích xuất │ +│ (Hiển thị kết quả) │ (Text từ file) │ +├──────────────────────────┴──────────────────────────┤ +│ ❓ Câu hỏi của bạn (tùy chọn) │ +├─────────────────────────────────────────────────────┤ +│ 📁 Upload file TXT/PNG │ +├─────────────────────────────────────────────────────┤ +│ [🚀 Gửi & Phân tích] [🗑️ Xóa chat] │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 📖 Cách sử dụng + +### Bước 1: Upload file +- Click nút **"📁 Upload file"** +- Chọn file TXT hoặc PNG/JPG từ máy tính + +### Bước 2: Đặt câu hỏi (Tùy chọn) +- Nhập câu hỏi cụ thể vào ô **"❓ Câu hỏi của bạn"** +- Hoặc để trống để AI tự động phân tích toàn diện + +### Bước 3: Nhấn "🚀 Gửi & Phân tích" +- AI sẽ đọc file và trả lời câu hỏi +- Kết quả hiển thị trong khung chat bên trái +- Nội dung text gốc hiển thị bên phải + +--- + +## 💡 Ví dụ sử dụng + +### Ví dụ 1: Phân tích tổng quát (không đặt câu hỏi) +``` +1. Upload: bao_cao_tai_chinh.png +2. Câu hỏi: [Để trống] +3. Nhấn: 🚀 Gửi & Phân tích + +→ AI sẽ tự động: + - Tóm tắt nội dung + - Trích xuất thông tin quan trọng + - Phân tích cấu trúc tài liệu +``` + +### Ví dụ 2: Trích xuất số liệu cụ thể +``` +1. Upload: bao_cao_tai_chinh.png +2. Câu hỏi: "Trích xuất tất cả các số liệu tài chính" +3. Nhấn: 🚀 Gửi & Phân tích + +→ AI sẽ tìm và liệt kê tất cả số liệu tài chính +``` + +### Ví dụ 3: Tìm thông tin cụ thể +``` +1. Upload: hop_dong.txt +2. Câu hỏi: "Tìm tên công ty, địa chỉ, và số điện thoại" +3. Nhấn: 🚀 Gửi & Phân tích + +→ AI sẽ tìm và trích xuất thông tin đó +``` + +### Ví dụ 4: So sánh và phân tích +``` +1. Upload: bao_cao_quy_2.png +2. Câu hỏi: "So sánh doanh thu và lợi nhuận giữa các tháng" +3. Nhấn: 🚀 Gửi & Phân tích + +→ AI sẽ phân tích và so sánh +``` + +--- + +## 🎯 Các câu hỏi gợi ý + +### Phân tích tài chính +- "Trích xuất tất cả các số liệu tài chính" +- "Tính tổng doanh thu và lợi nhuận" +- "So sánh các chỉ số tài chính" +- "Phân tích xu hướng tăng/giảm" + +### Trích xuất thông tin +- "Tìm tất cả ngày tháng trong tài liệu" +- "Liệt kê tất cả tên công ty và địa chỉ" +- "Trích xuất số điện thoại và email" +- "Tìm các số tài khoản ngân hàng" + +### Tóm tắt +- "Tóm tắt nội dung chính" +- "Liệt kê các điểm quan trọng" +- "Phân tích ý chính của tài liệu" + +### Chuyển đổi format +- "Chuyển nội dung thành bảng markdown" +- "Tạo danh sách từ nội dung" +- "Định dạng lại dữ liệu" + +--- + +## ⚙️ Tính năng + +### ✅ Hỗ trợ file +- TXT (UTF-8, Latin-1) +- PNG, JPG, JPEG, BMP, TIFF, GIF + +### ✅ OCR (Nhận diện ảnh) +- Tiếng Việt ✓ +- Tiếng Anh ✓ +- Độ tin cậy: 80-95% +- Nhận diện: Chữ cái, số, ký tự đặc biệt + +### ✅ AI Model +- Model: qwen/qwen3-vl-235b-a22b-thinking +- Max tokens: 4000 +- Temperature: 0.7 + +### ✅ Giao diện +- Hiển thị chat history +- Preview nội dung file +- Xóa chat và làm mới +- Responsive design + +--- + +## 📊 Workflow + +``` +┌──────────────┐ +│ Upload File │ +└──────┬───────┘ + │ + ├─── TXT ───→ Đọc trực tiếp + │ + └─── PNG ───→ OCR nhận diện + │ + ↓ + ┌──────────────┐ + │ Trích xuất │ + │ Text │ + └──────┬───────┘ + │ + ↓ + ┌──────────────┐ + │ Gửi đến AI │ + │ Model Qwen │ + └──────┬───────┘ + │ + ↓ + ┌──────────────┐ + │ Phân tích & │ + │ Trả lời │ + └──────┬───────┘ + │ + ↓ + ┌──────────────┐ + │ Hiển thị kết │ + │ quả trong │ + │ Chatbot │ + └──────────────┘ +``` + +--- + +## ⚠️ Lưu ý quan trọng + +### 1. Lần đầu chạy +- ⏰ Tải model OCR (100-200MB), mất 3-5 phút +- 💾 Model sẽ được lưu cache, lần sau nhanh hơn + +### 2. Chất lượng ảnh +- ✅ Ảnh rõ nét → Kết quả tốt +- ❌ Ảnh mờ, nhiễu → Kết quả kém +- 💡 Nên chụp ảnh có độ phân giải cao + +### 3. Kích thước file +- Ảnh quá lớn (>10MB) có thể chậm +- Nên resize ảnh xuống 2000-3000px + +### 4. Độ chính xác OCR +- Tiếng Việt có dấu: 80-90% +- Tiếng Anh: 90-95% +- Số liệu: 85-95% +- Chữ viết tay: 50-70% (không khuyến khích) + +### 5. API Key +- Key trong code là ví dụ +- Nên thay bằng key thực của bạn tại [OpenRouter](https://openrouter.ai/) + +--- + +## 🐛 Xử lý lỗi + +### Lỗi: "Không thể tải model OCR" +```bash +pip uninstall easyocr +pip install easyocr --no-cache-dir +``` + +### Lỗi: "API key không hợp lệ" +- Kiểm tra API key trong file `chatbot_file_analyzer.py` +- Đảm bảo có internet để gọi API + +### Lỗi: "Không đọc được file" +- Kiểm tra định dạng file (phải là TXT hoặc PNG/JPG) +- Kiểm tra file có bị hỏng không +- Thử với file khác + +### Lỗi: "Port 7860 đã được sử dụng" +```python +# Sửa trong file chatbot_file_analyzer.py +demo.launch(server_port=7861) # Đổi sang port khác +``` + +--- + +## 🔧 Cấu hình nâng cao + +### Thay đổi model AI +```python +# Trong file chatbot_file_analyzer.py, dòng ~127 +model="qwen/qwen3-vl-235b-a22b-thinking", + +# Có thể thay bằng: +# model="openai/gpt-4" +# model="anthropic/claude-3-opus" +``` + +### Thay đổi ngôn ngữ OCR +```python +# Dòng ~21 +ocr_reader = easyocr.Reader(['vi', 'en'], gpu=False) + +# Thêm ngôn ngữ khác: +# ocr_reader = easyocr.Reader(['vi', 'en', 'ch_sim', 'ja'], gpu=False) +``` + +### Bật GPU cho OCR (nếu có GPU) +```python +# Dòng ~21 +ocr_reader = easyocr.Reader(['vi', 'en'], gpu=True) # Đổi False → True +``` + +### Tạo public link (share) +```python +# Dòng ~298 +demo.launch( + share=True, # Đổi False → True + ... +) +``` + +--- + +## 📁 Cấu trúc file + +``` +FPT-vinaMILF/ +├── chatbot_file_analyzer.py ← File chính (chatbot) +├── run_file_analyzer.bat ← File chạy nhanh (Windows) +├── HUONG_DAN_FILE_ANALYZER.md ← File này +├── image_ocr_reader.py ← Module OCR (tham khảo) +└── pdf_reader.py ← Module PDF (tham khảo) +``` + +--- + +## 🎓 Ví dụ thực tế + +### Kịch bản 1: Phân tích báo cáo tài chính FPT +``` +File: 20250422 - FPT - BCTC cong ty me Quy 1 nam 2025.png + +Câu hỏi: "Trích xuất doanh thu, lợi nhuận, tổng tài sản và nợ phải trả" + +Kết quả AI: +- Doanh thu: XXX tỷ đồng +- Lợi nhuận: XXX tỷ đồng +- Tổng tài sản: XXX tỷ đồng +- Nợ phải trả: XXX tỷ đồng +``` + +### Kịch bản 2: Đọc hợp đồng +``` +File: hop_dong_dich_vu.txt + +Câu hỏi: "Tóm tắt các điều khoản quan trọng" + +Kết quả AI: +1. Bên A: ... +2. Bên B: ... +3. Thời hạn: ... +4. Giá trị hợp đồng: ... +5. Các điều khoản: ... +``` + +### Kịch bản 3: Đọc hóa đơn +``` +File: hoa_don.jpg + +Câu hỏi: "Liệt kê sản phẩm, số lượng, đơn giá" + +Kết quả AI: +Bảng sản phẩm: +| Sản phẩm | Số lượng | Đơn giá | Thành tiền | +|----------|----------|---------|------------| +| ... | ... | ... | ... | +``` + +--- + +## 💪 Công dụng thực tế + +1. ✅ **Phân tích báo cáo tài chính** từ ảnh chụp +2. ✅ **Đọc và tóm tắt văn bản** dài +3. ✅ **Trích xuất dữ liệu** từ form, hóa đơn +4. ✅ **Chuyển ảnh thành text** có thể edit +5. ✅ **Phân tích tài liệu** hợp đồng, biên bản +6. ✅ **So sánh số liệu** giữa các kỳ +7. ✅ **Tìm kiếm thông tin** cụ thể trong tài liệu + +--- + +## 📞 Hỗ trợ + +Nếu gặp vấn đề: +1. Đọc lại phần "Xử lý lỗi" +2. Kiểm tra đã cài đủ thư viện chưa +3. Kiểm tra API key còn hoạt động không +4. Thử với file mẫu khác + +--- + +## 🎉 Chúc bạn sử dụng thành công! diff --git a/api_token_example.py b/api_token_example.py new file mode 100644 index 0000000..d1629f0 --- /dev/null +++ b/api_token_example.py @@ -0,0 +1,33 @@ +from openai import OpenAI + +client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key="", +) + +completion = client.chat.completions.create( + extra_headers={ + "HTTP-Referer": "", # Optional. Site URL for rankings on openrouter.ai. + "X-OpenRouter-Title": "", # Optional. Site title for rankings on openrouter.ai. + }, + extra_body={}, + model="qwen/qwen3-vl-235b-a22b-thinking", + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What is in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://live.staticflickr.com/3851/14825276609_098cac593d_b.jpg" + } + } + ] + } + ] +) +print(completion.choices[0].message.content) \ No newline at end of file diff --git a/chatbot_file_analyzer.py b/chatbot_file_analyzer.py new file mode 100644 index 0000000..5a4a17f --- /dev/null +++ b/chatbot_file_analyzer.py @@ -0,0 +1,321 @@ +import gradio as gr +from openai import OpenAI +import easyocr +import os +from PIL import Image + +# Khởi tạo OpenAI client với OpenRouter +client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key="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", +) + +# Khởi tạo EasyOCR reader +ocr_reader = None + +def init_ocr_reader(): + """Khởi tạo OCR reader một lần duy nhất""" + global ocr_reader + if ocr_reader is None: + print("🔄 Đang tải model OCR (lần đầu có thể mất vài phút)...") + ocr_reader = easyocr.Reader(['vi', 'en'], gpu=False) + print("✅ Đã tải xong model OCR!") + return ocr_reader + +def extract_text_from_image(image_path): + """ + Đọc text từ ảnh bằng OCR + + Args: + image_path: Đường dẫn đến file ảnh + + Returns: + String chứa text đã nhận diện + """ + try: + # Khởi tạo OCR reader + reader = init_ocr_reader() + + # Mở và kiểm tra ảnh + img = Image.open(image_path) + print(f"📷 Đang đọc ảnh: {img.size[0]}x{img.size[1]} pixels") + + # Nhận diện text + print("🔍 Đang nhận diện text trong ảnh...") + results = reader.readtext(image_path) + + # Trích xuất text + text_content = "" + for (bbox, text, confidence) in results: + if confidence > 0.3: # Chỉ lấy text có độ tin cậy > 0.3 + text_content += text + "\n" + + print(f"✅ Đã nhận diện được {len(results)} đoạn text") + return text_content.strip() + + except Exception as e: + return f"❌ Lỗi khi đọc ảnh: {str(e)}" + +def extract_text_from_txt(txt_path): + """ + Đọc nội dung file TXT + + Args: + txt_path: Đường dẫn đến file TXT + + Returns: + String chứa nội dung file + """ + try: + with open(txt_path, 'r', encoding='utf-8') as f: + content = f.read() + print(f"📄 Đã đọc file TXT: {len(content)} ký tự") + return content + except UnicodeDecodeError: + # Thử encoding khác nếu utf-8 không được + try: + with open(txt_path, 'r', encoding='latin-1') as f: + content = f.read() + print(f"📄 Đã đọc file TXT (latin-1): {len(content)} ký tự") + return content + except Exception as e: + return f"❌ Lỗi khi đọc file TXT: {str(e)}" + except Exception as e: + return f"❌ Lỗi khi đọc file TXT: {str(e)}" + +def analyze_with_ai(content, user_question): + """ + Gửi nội dung đến AI để phân tích + + Args: + content: Nội dung text cần phân tích + user_question: Câu hỏi của người dùng + + Returns: + Câu trả lời từ AI + """ + try: + # Tạo prompt + if user_question and user_question.strip(): + prompt = f"""Nội dung tài liệu: +{content} + +Câu hỏi của người dùng: {user_question} + +Hãy trả lời câu hỏi dựa trên nội dung tài liệu trên.""" + else: + prompt = f"""Hãy phân tích nội dung tài liệu sau và cung cấp: +1. Tóm tắt nội dung chính +2. Các thông tin quan trọng (số liệu, ngày tháng, tên công ty, địa chỉ, v.v.) +3. Các điểm đặc biệt cần chú ý + +Nội dung tài liệu: +{content}""" + + # Gửi request đến AI + print("🤖 Đang phân tích với AI...") + completion = client.chat.completions.create( + extra_headers={ + "HTTP-Referer": "https://your-site.com", + "X-OpenRouter-Title": "File Analyzer Chatbot", + }, + model="qwen/qwen3-vl-235b-a22b-thinking", + messages=[ + { + "role": "user", + "content": prompt + } + ], + temperature=0.7, + max_tokens=4000 + ) + + result = completion.choices[0].message.content + print("✅ Đã nhận được phản hồi từ AI") + return result + + except Exception as e: + return f"❌ Lỗi khi gọi API: {str(e)}" + +def process_file(file, user_question, chat_history): + """ + Xử lý file upload và trả lời người dùng + + Args: + file: File object từ Gradio + user_question: Câu hỏi của người dùng + chat_history: Lịch sử chat + + Returns: + Tuple (chat_history, output_text) + """ + if file is None: + error_msg = "⚠️ Vui lòng upload file TXT hoặc PNG!" + chat_history.append({"role": "user", "content": user_question if user_question else "Upload file"}) + chat_history.append({"role": "assistant", "content": error_msg}) + return chat_history, "" + + try: + # Lấy đường dẫn và extension file + file_path = file.name + file_ext = os.path.splitext(file_path)[1].lower() + + print(f"\n{'='*50}") + print(f"📁 Nhận được file: {os.path.basename(file_path)}") + print(f"📋 Định dạng: {file_ext}") + + # Đọc nội dung file dựa vào loại file + if file_ext == '.txt': + # Đọc file TXT + content = extract_text_from_txt(file_path) + file_type = "TXT" + elif file_ext in ['.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.gif']: + # Đọc file ảnh bằng OCR + content = extract_text_from_image(file_path) + file_type = "Image (OCR)" + else: + error_msg = f"❌ Không hỗ trợ định dạng file {file_ext}. Chỉ hỗ trợ TXT hoặc PNG/JPG!" + chat_history.append({"role": "user", "content": user_question if user_question else f"Upload file {file_ext}"}) + chat_history.append({"role": "assistant", "content": error_msg}) + return chat_history, "" + + # Kiểm tra nội dung + if not content or content.startswith("❌"): + chat_history.append({"role": "user", "content": user_question if user_question else "Upload file"}) + chat_history.append({"role": "assistant", "content": content}) + return chat_history, "" + + # Hiển thị nội dung đã trích xuất + preview = content[:500] + "..." if len(content) > 500 else content + + # Phân tích với AI + ai_response = analyze_with_ai(content, user_question) + + # Tạo response cho user + user_msg = user_question if user_question else f"Phân tích file {os.path.basename(file_path)}" + + bot_response = f"""### 📄 File: {os.path.basename(file_path)} ({file_type}) + +**Nội dung đã trích xuất:** +``` +{preview} +``` + +**📊 Phân tích từ AI:** + +{ai_response} +""" + + # Thêm vào chat history + chat_history.append({"role": "user", "content": user_msg}) + chat_history.append({"role": "assistant", "content": bot_response}) + + print(f"✅ Hoàn thành xử lý file!") + print(f"{'='*50}\n") + + return chat_history, content + + except Exception as e: + error_msg = f"❌ Lỗi xử lý file: {str(e)}" + chat_history.append({"role": "user", "content": user_question if user_question else "Upload file"}) + chat_history.append({"role": "assistant", "content": error_msg}) + return chat_history, "" + +def clear_chat(): + """Xóa lịch sử chat""" + return [], "", None + +# Tạo giao diện Gradio +with gr.Blocks(title="🤖 AI File Analyzer Chatbot") as demo: + gr.Markdown(""" + # 🤖 AI File Analyzer Chatbot + + ### 📤 Upload file TXT hoặc PNG để AI phân tích + + **Hỗ trợ:** + - 📄 **File TXT**: Đọc trực tiếp nội dung + - 🖼️ **File PNG/JPG**: Dùng OCR nhận diện text (tiếng Việt + tiếng Anh) + + **Model AI:** `qwen/qwen3-vl-235b-a22b-thinking` + """) + + with gr.Row(): + with gr.Column(scale=2): + # Chatbot interface + chatbot = gr.Chatbot( + label="💬 Trò chuyện với AI", + height=500, + show_label=True + ) + + with gr.Row(): + user_question = gr.Textbox( + label="❓ Câu hỏi của bạn (tùy chọn)", + placeholder="Ví dụ: Trích xuất tất cả số liệu tài chính, Tóm tắt nội dung,...", + scale=4 + ) + + with gr.Row(): + file_input = gr.File( + label="📁 Upload file (TXT hoặc PNG/JPG)", + file_types=[".txt", ".png", ".jpg", ".jpeg", ".bmp"], + type="filepath" + ) + + with gr.Row(): + submit_btn = gr.Button("🚀 Gửi & Phân tích", variant="primary", scale=2) + clear_btn = gr.Button("🗑️ Xóa chat", scale=1) + + with gr.Column(scale=1): + gr.Markdown("### 📝 Nội dung đã trích xuất") + extracted_content = gr.Textbox( + label="", + placeholder="Nội dung text sẽ hiển thị ở đây...", + lines=20, + max_lines=20, + show_label=False + ) + + # Examples + gr.Markdown(""" + ### 💡 Gợi ý câu hỏi: + - "Trích xuất tất cả các số liệu tài chính" + - "Tóm tắt nội dung chính" + - "Liệt kê tất cả ngày tháng và số điện thoại" + - "Phân tích báo cáo này" + - Hoặc để trống để AI tự động phân tích toàn diện + """) + + # Event handlers + submit_btn.click( + fn=process_file, + inputs=[file_input, user_question, chatbot], + outputs=[chatbot, extracted_content] + ) + + clear_btn.click( + fn=clear_chat, + inputs=[], + outputs=[chatbot, user_question, file_input] + ) + + # Enter key to submit + user_question.submit( + fn=process_file, + inputs=[file_input, user_question, chatbot], + outputs=[chatbot, extracted_content] + ) + +# Khởi chạy ứng dụng +if __name__ == "__main__": + print("\n" + "="*50) + print("🚀 Đang khởi động AI File Analyzer Chatbot...") + print("="*50 + "\n") + + demo.launch( + server_name="0.0.0.0", # Cho phép truy cập từ mạng LAN + server_port=7860, + share=False, # Đặt True nếu muốn tạo link public + inbrowser=True, # Tự động mở trình duyệt + theme=gr.themes.Soft() # Theme được chuyển sang launch() trong Gradio 6.0 + ) diff --git a/chatbot_upload.py b/chatbot_upload.py new file mode 100644 index 0000000..3b30771 --- /dev/null +++ b/chatbot_upload.py @@ -0,0 +1,240 @@ +""" +chatbot_upload.py +Chatbot hỗ trợ upload .txt và .png, phân tích bằng qwen/qwen3-vl-235b-a22b-thinking +Tương thích Gradio 6.x +""" + +import base64, os +import gradio as gr +from openai import OpenAI + +# ─── Cấu hình ───────────────────────────────────────────────────────────────── +API_KEY = "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" +MODEL = "qwen/qwen3-vl-235b-a22b-thinking" + +client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=API_KEY, +) + +SYSTEM_PROMPT = ( + "Bạn là trợ lý AI thông minh, có thể đọc hiểu văn bản và phân tích hình ảnh. " + "Hãy trả lời bằng tiếng Việt, rõ ràng, chi tiết và dễ hiểu. " + "Sử dụng markdown để định dạng câu trả lời khi cần thiết." +) + +# ─── Tiện ích file ──────────────────────────────────────────────────────────── + +def read_txt(path: str) -> str: + with open(path, "r", encoding="utf-8", errors="replace") as f: + return f.read() + +def to_b64(path: str) -> str: + with open(path, "rb") as f: + return base64.b64encode(f.read()).decode() + +def ext(path: str) -> str: + return os.path.splitext(path)[1].lower() + +def mime(path: str) -> str: + e = ext(path) + return {"png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg", + "webp":"image/webp","gif":"image/gif"}.get(e.lstrip("."), "image/png") + +# ─── Xây dựng message ──────────────────────────────────────────────────────── + +def build_user_content(text: str, file_path: str | None) -> list[dict]: + """Tạo nội dung message cho API.""" + parts: list[dict] = [] + + if file_path: + e = ext(file_path) + fname = os.path.basename(file_path) + + if e == ".txt": + body = read_txt(file_path) + full = (f"{text}\n\n--- NỘI DUNG FILE: {fname} ---\n{body}\n---" + if text.strip() else + f"--- NỘI DUNG FILE: {fname} ---\n{body}\n---") + parts.append({"type": "text", "text": full}) + + elif e in (".png", ".jpg", ".jpeg", ".webp", ".gif"): + prompt = text.strip() or "Hãy phân tích và mô tả chi tiết hình ảnh này." + parts.append({"type": "text", "text": prompt}) + data_url = f"data:{mime(file_path)};base64,{to_b64(file_path)}" + parts.append({"type": "image_url", "image_url": {"url": data_url}}) + + else: + parts.append({"type": "text", + "text": f"⚠️ File '{fname}' không được hỗ trợ. Chỉ nhận .txt và .png."}) + else: + if not text.strip(): + return [] + parts.append({"type": "text", "text": text}) + + return parts + +# ─── Chat handler ───────────────────────────────────────────────────────────── + +def respond(message: dict, history: list[dict]): + """ + Gradio 6 MultimodalTextbox trả về: + message = {"text": "...", "files": ["path1", ...]} + history = list of {"role": "user"/"assistant", "content": "..."} + """ + text = (message.get("text") or "").strip() + files = message.get("files") or [] + file_path = files[0] if files else None + + # Nhãn hiển thị trong chat + if file_path: + fname = os.path.basename(file_path) + display = f"📎 **{fname}**\n{text}" if text else f"📎 **{fname}**" + else: + display = text + + if not text and not file_path: + return + + # Xây messages gửi API + api_messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}] + + # Lịch sử + for m in history: + api_messages.append({"role": m["role"], "content": m["content"]}) + + # Tin nhắn hiện tại + user_content = build_user_content(text, file_path) + if not user_content: + return + + api_messages.append({"role": "user", "content": user_content}) + + # Thêm tin nhắn user vào history để hiển thị + history.append({"role": "user", "content": display}) + history.append({"role": "assistant", "content": ""}) + yield history + + # Gọi API streaming + try: + stream = client.chat.completions.create( + model=MODEL, + messages=api_messages, + stream=True, + extra_headers={ + "HTTP-Referer": "https://localhost", + "X-Title": "AI-File-Chatbot", + }, + ) + response = "" + for chunk in stream: + delta = chunk.choices[0].delta.content + if delta: + response += delta + history[-1]["content"] = response + yield history + + except Exception as e: + history[-1]["content"] = f"⚠️ **Lỗi kết nối:** {e}" + yield history + +# ─── Gradio UI ──────────────────────────────────────────────────────────────── + +css = """ +#chatbot { height: 540px; } +footer { display: none !important; } +.tip-box { background: #f0f4ff; border-radius: 10px; padding: 12px 16px; margin-top: 8px; font-size: 0.9rem; } +""" + +with gr.Blocks( + title="🤖 AI File Analyzer", + theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="violet"), + css=css, +) as demo: + + gr.Markdown( + """ + # 🤖 AI File Analyzer Chatbot + Upload file **`.txt`** hoặc **`.png`** · AI sẽ phân tích và trả lời câu hỏi của bạn. + + > **Model:** `qwen/qwen3-vl-235b-a22b-thinking`  ·  **Provider:** OpenRouter + """ + ) + + with gr.Row(): + # ── Sidebar ────────────────────────────────────────────────────────── + with gr.Column(scale=1, min_width=240): + gr.Markdown("### 💡 Hướng dẫn") + gr.Markdown( + """ +
+ 1. Nhấn biểu tượng 📎 để upload file
+ 2. Gõ câu hỏi (hoặc để trống)
+ 3. Nhấn Enter hoặc nút Gửi +
+ """ + ) + gr.Markdown("### 💬 Câu hỏi gợi ý") + gr.Markdown( + """ + - *"Tóm tắt nội dung file này"* + - *"Những điểm quan trọng là gì?"* + - *"Mô tả chi tiết hình ảnh"* + - *"Nhận xét về nội dung trên"* + - *"Dịch nội dung sang tiếng Anh"* + """ + ) + gr.Markdown("---") + gr.Markdown("**Định dạng hỗ trợ:**\n- 📄 `.txt` — văn bản\n- 🖼️ `.png` / `.jpg` — hình ảnh") + + # ── Chat area ──────────────────────────────────────────────────────── + with gr.Column(scale=3): + chatbot = gr.Chatbot( + label="Hội thoại", + elem_id="chatbot", + type="messages", # Gradio 6: dùng list[dict] + show_copy_button=True, + bubble_full_width=False, + avatar_images=(None, "https://api.dicebear.com/7.x/bottts/svg?seed=qwen"), + ) + + chat_input = gr.MultimodalTextbox( + placeholder="Nhập câu hỏi... hoặc upload file rồi nhấn Enter", + show_label=False, + file_types=[".txt", ".png", ".jpg", ".jpeg"], + file_count="single", + submit_btn=True, + ) + + clear_btn = gr.Button("🗑️ Xóa lịch sử", size="sm", variant="secondary") + + gr.Markdown( + "---\n*Powered by [OpenRouter](https://openrouter.ai) · " + "Model Qwen3 VL 235B Thinking · Hỗ trợ văn bản & hình ảnh*" + ) + + # ── Events ──────────────────────────────────────────────────────────────── + chat_input.submit( + fn=respond, + inputs=[chat_input, chatbot], + outputs=chatbot, + ).then(fn=lambda: {"text": "", "files": []}, outputs=chat_input) + + clear_btn.click(fn=lambda: [], outputs=chatbot) + + +# ─── Entry point ────────────────────────────────────────────────────────────── +if __name__ == "__main__": + print("=" * 60) + print("🚀 AI File Analyzer Chatbot (Gradio 6)") + print(f" Model : {MODEL}") + print(" URL : http://localhost:7861") + print("=" * 60) + + demo.launch( + server_name="0.0.0.0", + server_port=7861, + share=False, + show_error=True, + inbrowser=True, + ) diff --git a/gradio_chatbot.py b/gradio_chatbot.py new file mode 100644 index 0000000..c7b9c7f --- /dev/null +++ b/gradio_chatbot.py @@ -0,0 +1,222 @@ +import gradio as gr +import pandas as pd +import os +from openai import OpenAI + +# Khởi tạo OpenAI client với OpenRouter +client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key="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", +) + +class FinancialChatbot: + def __init__(self): + self.excel_data = {} + self.data_context = "" + self.load_excel_files() + + def load_excel_files(self): + """Đọc tất cả các file Excel trong thư mục""" + excel_files = { + 'Bang_Can_Doi_Ke_Toan.xlsx': 'Bảng cân đối kế toán', + 'Bao_Cao_Ket_Qua_Hoat_Dong.xlsx': 'Báo cáo kết quả hoạt động kinh doanh', + 'Bang_So_Sanh_KQKD.xlsx': 'Bảng so sánh kết quả kinh doanh' + } + + status_messages = [] + for filename, description in excel_files.items(): + if os.path.exists(filename): + try: + excel_file = pd.ExcelFile(filename) + self.excel_data[description] = {} + + for sheet_name in excel_file.sheet_names: + df = pd.read_excel(filename, sheet_name=sheet_name) + self.excel_data[description][sheet_name] = df + + status_messages.append(f"✓ {description}") + except Exception as e: + status_messages.append(f"✗ {description}: {e}") + + self.data_context = self.format_data_for_ai() + return "\n".join(status_messages) + + def format_data_for_ai(self): + """Chuyển đổi dữ liệu Excel thành text để gửi cho AI""" + context = "Dưới đây là dữ liệu báo cáo tài chính:\n\n" + + for report_name, sheets in self.excel_data.items(): + context += f"### {report_name}\n" + for sheet_name, df in sheets.items(): + context += f"\n**{sheet_name}:**\n" + context += df.to_string(index=False, max_rows=100) + context += "\n\n" + + return context + + def chat(self, message, history): + """Xử lý tin nhắn và trả về phản hồi""" + if not message.strip(): + return history + + # Tạo messages cho API + messages = [ + { + "role": "system", + "content": f"""Bạn là trợ lý AI chuyên về phân tích báo cáo tài chính. +Nhiệm vụ của bạn là trả lời các câu hỏi dựa trên dữ liệu báo cáo tài chính được cung cấp. + +{self.data_context} + +Hãy trả lời bằng tiếng Việt, chính xác, chi tiết và dễ hiểu. +Khi trích dẫn số liệu, hãy nêu rõ nguồn gốc (từ báo cáo nào, kỳ nào). +Nếu câu hỏi không liên quan đến dữ liệu có sẵn, hãy thông báo lịch sự. +Format câu trả lời với markdown để dễ đọc.""" + } + ] + + # Thêm lịch sử hội thoại + for user_msg, assistant_msg in history: + messages.append({"role": "user", "content": user_msg}) + if assistant_msg: + messages.append({"role": "assistant", "content": assistant_msg}) + + # Thêm tin nhắn hiện tại + messages.append({"role": "user", "content": message}) + + try: + # Gọi API + completion = client.chat.completions.create( + model="qwen/qwen-2.5-72b-instruct", + messages=messages, + temperature=0.3, + max_tokens=2000, + stream=True # Enable streaming + ) + + # Stream response + response = "" + for chunk in completion: + if chunk.choices[0].delta.content: + response += chunk.choices[0].delta.content + # Update history với response hiện tại + history_copy = history + [[message, response]] + yield history_copy + + except Exception as e: + error_msg = f"⚠️ Lỗi: {str(e)}" + yield history + [[message, error_msg]] + +# Khởi tạo chatbot +bot = FinancialChatbot() +load_status = bot.load_excel_files() + +# Tạo giao diện Gradio +with gr.Blocks(title="Chatbot Phân Tích Báo Cáo Tài Chính") as demo: + + gr.Markdown( + """ + # 🤖 Chatbot Phân Tích Báo Cáo Tài Chính + + Trợ lý AI giúp bạn phân tích và trả lời câu hỏi về báo cáo tài chính. + """ + ) + + with gr.Accordion("📊 Trạng thái dữ liệu", open=False): + gr.Markdown(f"```\n{load_status}\n```") + gr.Markdown( + """ + **Dữ liệu đã tải:** + - Bảng cân đối kế toán (30/9/2025 và 31/12/2024) + - Báo cáo kết quả hoạt động kinh doanh (Quý 3/2025) + - Bảng so sánh kết quả kinh doanh + """ + ) + + with gr.Accordion("💡 Gợi ý câu hỏi", open=False): + gr.Markdown( + """ + ### Về Doanh thu & Lợi nhuận: + - "Doanh thu quý 3 năm 2025 là bao nhiêu?" + - "Lợi nhuận sau thuế quý 3/2025 tăng bao nhiêu % so với cùng kỳ?" + - "Phân tích biên lợi nhuận gộp của công ty" + + ### Về Tài sản: + - "Tổng tài sản tại ngày 30/9/2025 là bao nhiêu?" + - "Tài sản ngắn hạn và dài hạn chiếm bao nhiêu %?" + - "Các khoản đầu tư tài chính có thay đổi gì?" + + ### Về Nợ & Vốn: + - "Tổng nợ phải trả là bao nhiêu?" + - "Tỷ lệ nợ/vốn chủ sở hữu như thế nào?" + - "Vốn chủ sở hữu có tăng không?" + + ### Phân tích tổng hợp: + - "So sánh tình hình tài chính giữa quý 3/2025 và quý 3/2024" + - "Những điểm nổi bật trong báo cáo tài chính" + - "Đánh giá khả năng thanh toán của công ty" + """ + ) + + # Chatbot interface + chatbot = gr.Chatbot( + height=500, + show_label=False + ) + + with gr.Row(): + msg = gr.Textbox( + placeholder="Nhập câu hỏi của bạn về báo cáo tài chính...", + show_label=False, + scale=9, + container=False + ) + submit = gr.Button("Gửi", variant="primary", scale=1) + + with gr.Row(): + clear = gr.Button("🗑️ Xóa lịch sử", size="sm") + + # Examples + gr.Examples( + examples=[ + "Doanh thu quý 3 năm 2025 là bao nhiêu?", + "Lợi nhuận sau thuế quý 3/2025 so với cùng kỳ thay đổi như thế nào?", + "Tổng tài sản tại ngày 30/9/2025 là bao nhiêu?", + "Phân tích tình hình tài chính của công ty", + "Tỷ lệ nợ/vốn chủ sở hữu là bao nhiêu?", + ], + inputs=msg, + ) + + gr.Markdown( + """ + --- + **Model:** Qwen 2.5 72B Instruct | **Provider:** OpenRouter | **Ngôn ngữ:** Tiếng Việt + """ + ) + + # Event handlers + msg.submit(bot.chat, [msg, chatbot], chatbot).then( + lambda: "", None, msg + ) + submit.click(bot.chat, [msg, chatbot], chatbot).then( + lambda: "", None, msg + ) + clear.click(lambda: None, None, chatbot) + +if __name__ == "__main__": + print("="*70) + print("🚀 Khởi động Chatbot Phân Tích Báo Cáo Tài Chính") + print("="*70) + print(f"\n{load_status}\n") + print("="*70) + print("📱 Mở trình duyệt để sử dụng chatbot...") + print("="*70) + + demo.launch( + server_name="0.0.0.0", # Cho phép truy cập từ mạng local + server_port=7860, + share=False, # Đặt True nếu muốn share link public + show_error=True, + inbrowser=True # Tự động mở trình duyệt + )