This commit is contained in:
letai2001 2025-10-13 01:32:17 +07:00
parent feee56dc56
commit a4158929cc
32 changed files with 1932 additions and 59 deletions

6
.env
View File

@ -13,3 +13,9 @@ QDRANT_URL=http://localhost:6333
QDRANT_COLLECTION=text_chunks
GEMINI_API_KEY = "AIzaSyDWqNUBKhaZjbFI8CW52_hKr46JtWABkGU"
GEMINI_MODEL = "models/gemini-2.0-flash-001"
SERPER_API_KEY = "14ea1e5de7b68084d3e41a4efe204108a8fa76c6fab062b00fa1912a97d3c28b"
GOOGLE_API_KEY=AIzaSyCOtXgt7M6qKlj5srF8hF_7iyvZbBrVnhA
GOOGLE_CX=9379e94eed0f145b4
TRUSTED_DOMAINS = "wikipedia.org,chinhphu.vn,vnexpress.net,bbc.com,nhandan.vn,tuoitre.vn,thanhnien.vn,zingnews.vn,vov.vn,vietnamnet.vn"
ALLOW_WEB_SEARCH=true
SECOND_PASS=true

Binary file not shown.

BIN
image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

257
logs/api_2025-10-12.log Normal file
View File

@ -0,0 +1,257 @@
2025-10-12 18:14:03,660 [INFO] numexpr.utils - NumExpr defaulting to 12 threads.
2025-10-12 18:15:29,187 [INFO] httpx - HTTP Request: GET http://localhost:6333 "HTTP/1.1 200 OK"
2025-10-12 18:15:29,199 [INFO] httpx - HTTP Request: GET http://localhost:6333/collections/text_chunks "HTTP/1.1 200 OK"
2025-10-12 18:15:29,211 [INFO] sentence_transformers.SentenceTransformer - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
2025-10-12 18:15:43,063 [INFO] src.chatbot.llm_client - 🔮 LLMClient khởi tạo với model: models/gemini-2.0-flash-001
2025-10-12 18:15:43,063 [INFO] function_router - 🔧 Đã đăng ký tool: create_txt
2025-10-12 18:15:43,064 [INFO] rag_api - ✅ RAGPipeline đã khởi tạo thành công.
2025-10-12 18:15:46,719 [INFO] rag_api - 📥 Câu hỏi: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên?
2025-10-12 18:16:03,855 [INFO] httpx - HTTP Request: POST http://localhost:6333/collections/text_chunks/points/search "HTTP/1.1 200 OK"
2025-10-12 18:19:21,649 [INFO] src.chatbot.llm_client - 🧠 Gửi prompt tới Gemini...
2025-10-12 18:19:27,724 [INFO] src.chatbot.llm_client - 💬 Gemini output: Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư và 1.560 nhân viên...
2025-10-12 18:19:40,751 [INFO] root - 📤 Gửi prompt tới LLM: ### Vai trò hệ thống:
Bạn là trợ lý AI chuyên nghiệp, hiểu tiếng Việt, có khả năng trả lời tự nhiên, chính xác và ngắn gọn. Bạn có thể vừa trả lời, vừa thực hiện hành động theo yêu cầu người dùng. Nếu người dùng chỉ đặt câu hỏi, hãy trả lời tự nhiên. Nếu người dùng yêu cầu hành động, hãy trả về **đúng JSON hợp lệ**.
### Dữ liệu ngữ cảnh:
Dưới đây là các thông tin có liên quan:
(Đoạn 1 - score=0.886)
Đại học Hanover
Đại học Hanover, chính thức là Gottfried Wilhelm Leibniz Universität Hannover hoặc Luh, là một trường đại học nằm ở Hanover, Đức. Trường được thành lập vào năm 1831 và là tổ chức đào tạo đại học lớn thứ hai ở Niedersachsen. Đại học Leibniz Hannover là một thành viên của TU9, một hiệp hội của chín Viện Công nghệ hàng đầu tại Đức.
Lịch sử.
Trường đại học này được thành lập vào năm 1831 là một trường cao đẳng thương mại. Trường đã bắt đầu nghiên cứu toán học, kiến trúc, kỹ thuật, lịch sử tự nhiên, vật lý, hóa học, vẽ, công nghệ, nghiên cứu và kế toán. Năm 1879 trường đã được nâng cấp thành Trường Cao đẳng Công nghệ Hoàng gia, năm 1898 nó đã được trao quyền đào tạo tiến sĩ.
Lĩnh vực của trường đại học này, từ đầu của nó, tập trung vào khoa học và công nghệ. Trong thế kỷ 20, các ngành nghệ thuật và nhân văn đã được bổ sung, và trường đã được sáp nhập thêm Trường Cao đẳng Sư phạm trước đó là một trường độc lập.
Khoa.
Trường có 9 khoa với hơn 150 cấp độ độ đầu tiên toàn thời gian và các khóa học trình độ bán thời gian, khiến cho trường này là trường đại học lớn thứ hai của giáo dục đại học tổ chức ở Lower Saxony. Đội ngũ cán bộ trường đại học này bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong các chức năng hành chính, và có thêm 900 người được tài trợ của bên thứ ba.
(Đoạn 2 - score=0.630)
Viện Thiên văn học của Đại học Hawaii
Viện Thiên văn học của Đại học Hawaii (, viết tắt: IfA) là một đơn vị nghiên cứu trong hệ thống Đại học Hawaii, do Günther Hasinger làm giám đốc. Trụ sở chính của IfA đặt tại 2680 Woodlawn Drive ở Honolulu, Hawaii, , trong khuôn viên Đại học Hawaii tại Mānoa. Các cơ sở khác đặt tại Pukalani, Maui và Hilo trên đảo Hawaiʻi (Đảo Lớn). IfA tuyển dụng hơn 150 nhà thiên văn học và tình nguyện viên. Các nhà thiên văn IfA thực hiện nghiên cứu vật thể, sao, thiên hà và Hệ Mặt Trời.
Viện Thiên văn học được thành lập năm 1967 để nghiên cứu và quản lý các khu phức hợp quan sát tại Haleakalā, Maui và Đài Quan sát Mauna Kea trên đỉnh Mauna Kea. Nó có khoảng 55 giảng viên và hơn 300 nhân viên.
(Đoạn 3 - score=0.607)
Đại học Khoa học Đời sống Warszawa
Đại học Khoa học Đời sống Warszawa (, SGGW) là trường đại học nông nghiệp lớn nhất Ba Lan, được thành lập năm 1816 tại Warszawa. Trường có hơn 2.600 nhân viên bao gồm hơn 1.200 nhà giáo dục học thuật. Từ năm 2005, trường đại học này là một thành viên của tổ chức Euroleague cho Khoa học sự sống (ELLS) được thành lập năm 2001. SGGW cung cấp khoảng 37 lĩnh vực nghiên cứu khác nhau, 13 khoa Khoa học Nông nghiệp, Khoa học Kinh tế, Nhân văn, Kỹ thuật cũng như Khoa học Đời sống.
Khuôn viên.
Khuôn viên trường nằm ở quận cực nam của Warszawa, Ursynów. Khuôn viên có một phần lịch sử, với một cung điện từ thế kỷ 18, và một phần hiện đại nơi có hầu hết các tòa nhà văn phòng khoa và ký túc xá. Trên khuôn viên chính rộng 70 ha, có 12 ký túc xá, thư viện hiện đại, trung tâm thể thao (có sân tennis, phòng thể thao và hồ bơi) một trung tâm ngôn ngữ, phòng khám thú y.
(Đoạn 4 - score=0.591)
Hán học
Hán học (chữ Hán: 漢學) hay Trung Quốc học (chữ Hán: 中國學) là ngành khoa học chuyên nghiên cứu về Trung Quốc, bao gồm lịch sử, chính trị, xã hội, triết học, kinh tế, thậm chí nghiên cứu cả về cộng đồng người Hoa ở nước ngoài. Đây là khái niệm do người nước ngoài đặt ra, tiếng Anh gọi môn khoa học này là Sinology hay Chinese Studies, còn người Trung Quốc gọi khoa học nghiên cứu về Trung Quốc là Quốc học 國學.
Lịch sử.
Ban đầu Hán học chỉ nghiên cứu về văn hoá cổ đại Trung Quốc, chủ yếu nghiên cứu cổ văn, triết học, văn học, hầu như không bao quát hết xã hội Trung Quốc hiện đại. Sau Chiến tranh thế giới thứ hai, Hán học mới bắt đầu nghiên cứu đến Trung Quốc hiện đại.
Hán học thường được chia làm hai thời kỳ là "Hán học cổ đại" và "Hán học hiện đại" …
### Công cụ có thể sử dụng:
Hiện bạn có thể sử dụng công cụ sau:
1. create_txt(text: str) — Tạo file .txt chứa nội dung văn bản.
Nếu người dùng yêu cầu hành động (như tạo, lưu, ghi vào file),
hãy trả về đúng JSON theo mẫu sau:
{"action": "create_txt", "params": {"text": "<nội dung cần lưu>"}}
Nếu người dùng chỉ hỏi thông tin, hãy trả lời văn bản tự nhiên, không JSON.
### Câu hỏi của người dùng:
Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên?
### Hướng dẫn cho AI:
- Nếu người dùng chỉ hỏi → trả lời tự nhiên, ngắn gọn, chính xác.
- Nếu người dùng yêu cầu lưu/tạo file → hãy dùng create_txt.
- Khi trả về JSON, không thêm giải thích, chỉ trả JSON hợp lệ duy nhất.
- Nếu không đủ dữ liệu, hãy nói rõ ràng rằng bạn chưa có thông tin chính xác.
- Không bịa thêm, không suy diễn.
### Trả lời:
...
2025-10-12 18:19:45,211 [INFO] rag_api - ✅ Đã trả lời: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nh...
2025-10-12 18:20:13,978 [INFO] rag_api - 📥 Câu hỏi: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
2025-10-12 18:20:16,935 [INFO] httpx - HTTP Request: POST http://localhost:6333/collections/text_chunks/points/search "HTTP/1.1 200 OK"
2025-10-12 18:21:46,603 [INFO] src.chatbot.llm_client - 🧠 Gửi prompt tới Gemini...
2025-10-12 18:22:08,879 [INFO] src.chatbot.llm_client - 💬 Gemini output: Đại học Hanover có 9 khoa. Số lượng nhân viên là 1.120, bao gồm 340 giáo sư, 1.560 nhân viên hành chính, và 900 người đư...
2025-10-12 18:22:21,101 [INFO] src.chatbot.llm_client - 🤖 Function call JSON: {'action': 'create_txt', 'params': {'text': 'Đại học Hanover có 9 khoa. Số lượng nhân viên là 1.120, bao gồm 340 giáo sư, 1.560 nhân viên hành chính, và 900 người được tài trợ.'}}
2025-10-12 18:22:34,885 [WARNING] function_executor - Không thể parse JSON từ LLM output: 'dict' object has no attribute 'find'
2025-10-12 18:22:47,884 [INFO] root - 📤 Gửi prompt tới LLM: ### Vai trò hệ thống:
Bạn là trợ lý AI chuyên nghiệp, hiểu tiếng Việt, có khả năng trả lời tự nhiên, chính xác và ngắn gọn. Bạn có thể vừa trả lời, vừa thực hiện hành động theo yêu cầu người dùng. Nếu người dùng chỉ đặt câu hỏi, hãy trả lời tự nhiên. Nếu người dùng yêu cầu hành động, hãy trả về **đúng JSON hợp lệ**.
### Dữ liệu ngữ cảnh:
Dưới đây là các thông tin có liên quan:
(Đoạn 1 - score=0.818)
Đại học Hanover
Đại học Hanover, chính thức là Gottfried Wilhelm Leibniz Universität Hannover hoặc Luh, là một trường đại học nằm ở Hanover, Đức. Trường được thành lập vào năm 1831 và là tổ chức đào tạo đại học lớn thứ hai ở Niedersachsen. Đại học Leibniz Hannover là một thành viên của TU9, một hiệp hội của chín Viện Công nghệ hàng đầu tại Đức.
Lịch sử.
Trường đại học này được thành lập vào năm 1831 là một trường cao đẳng thương mại. Trường đã bắt đầu nghiên cứu toán học, kiến trúc, kỹ thuật, lịch sử tự nhiên, vật lý, hóa học, vẽ, công nghệ, nghiên cứu và kế toán. Năm 1879 trường đã được nâng cấp thành Trường Cao đẳng Công nghệ Hoàng gia, năm 1898 nó đã được trao quyền đào tạo tiến sĩ.
Lĩnh vực của trường đại học này, từ đầu của nó, tập trung vào khoa học và công nghệ. Trong thế kỷ 20, các ngành nghệ thuật và nhân văn đã được bổ sung, và trường đã được sáp nhập thêm Trường Cao đẳng Sư phạm trước đó là một trường độc lập.
Khoa.
Trường có 9 khoa với hơn 150 cấp độ độ đầu tiên toàn thời gian và các khóa học trình độ bán thời gian, khiến cho trường này là trường đại học lớn thứ hai của giáo dục đại học tổ chức ở Lower Saxony. Đội ngũ cán bộ trường đại học này bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong các chức năng hành chính, và có thêm 900 người được tài trợ của bên thứ ba.
(Đoạn 2 - score=0.584)
Viện Thiên văn học của Đại học Hawaii
Viện Thiên văn học của Đại học Hawaii (, viết tắt: IfA) là một đơn vị nghiên cứu trong hệ thống Đại học Hawaii, do Günther Hasinger làm giám đốc. Trụ sở chính của IfA đặt tại 2680 Woodlawn Drive ở Honolulu, Hawaii, , trong khuôn viên Đại học Hawaii tại Mānoa. Các cơ sở khác đặt tại Pukalani, Maui và Hilo trên đảo Hawaiʻi (Đảo Lớn). IfA tuyển dụng hơn 150 nhà thiên văn học và tình nguyện viên. Các nhà thiên văn IfA thực hiện nghiên cứu vật thể, sao, thiên hà và Hệ Mặt Trời.
Viện Thiên văn học được thành lập năm 1967 để nghiên cứu và quản lý các khu phức hợp quan sát tại Haleakalā, Maui và Đài Quan sát Mauna Kea trên đỉnh Mauna Kea. Nó có khoảng 55 giảng viên và hơn 300 nhân viên.
(Đoạn 3 - score=0.573)
Đại học Khoa học Đời sống Warszawa
Đại học Khoa học Đời sống Warszawa (, SGGW) là trường đại học nông nghiệp lớn nhất Ba Lan, được thành lập năm 1816 tại Warszawa. Trường có hơn 2.600 nhân viên bao gồm hơn 1.200 nhà giáo dục học thuật. Từ năm 2005, trường đại học này là một thành viên của tổ chức Euroleague cho Khoa học sự sống (ELLS) được thành lập năm 2001. SGGW cung cấp khoảng 37 lĩnh vực nghiên cứu khác nhau, 13 khoa Khoa học Nông nghiệp, Khoa học Kinh tế, Nhân văn, Kỹ thuật cũng như Khoa học Đời sống.
Khuôn viên.
Khuôn viên trường nằm ở quận cực nam của Warszawa, Ursynów. Khuôn viên có một phần lịch sử, với một cung điện từ thế kỷ 18, và một phần hiện đại nơi có hầu hết các tòa nhà văn phòng khoa và ký túc xá. Trên khuôn viên chính rộng 70 ha, có 12 ký túc xá, thư viện hiện đại, trung tâm thể thao (có sân tennis, phòng thể thao và hồ bơi) một trung tâm ngôn ngữ, phòng khám thú y.
(Đoạn 4 - score=0.545)
Đại học Eötvös Loránd
Đại học Eötvös Loránd (phiên âm: Ết-vêx Lô-ran), thành lập năm 1635, là trường đại học lớn nhất ở Hungary. Trường tọa lạc tại Thủ đô Budapest.
Lịch sử.
Trường được Đức Tổng Giám mục và nhà thần học Péter Pázmány thành lập năm 1635 tại Nagyszombat (Trnava, Slovakia hiện nay). Các tu sĩ Dòng Tên đã nắm quyền lãnh đạo trường. Ban đầu, trường chỉ có hai trường thành viên (Trường Nghệ thuật và Trường Thần học). Đại học Luật đã được bổ sung năm 1667 và Đại học Y khoa đã được bắt đầu năm 1769. Sau đợt giải thể của trật tự dòng Tên, trường đại học đã được chuyển đến Buda (ngày nay là một phần của Budapest) vào năm 1777 phù hợp với ý định của người sáng lập. Trường đã được di chuyển đến vị trí cuối cùng của nó trong Pest (cũng là một phần của Budapest) năm 1784 …
### Công cụ có thể sử dụng:
Hiện bạn có thể sử dụng công cụ sau:
1. create_txt(text: str) — Tạo file .txt chứa nội dung văn bản.
Nếu người dùng yêu cầu hành động (như tạo, lưu, ghi vào file),
hãy trả về đúng JSON theo mẫu sau:
{"action": "create_txt", "params": {"text": "<nội dung cần lưu>"}}
Nếu người dùng chỉ hỏi thông tin, hãy trả lời văn bản tự nhiên, không JSON.
### Câu hỏi của người dùng:
Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
### Hướng dẫn cho AI:
- Nếu người dùng chỉ hỏi → trả lời tự nhiên, ngắn gọn, chính xác.
- Nếu người dùng yêu cầu lưu/tạo file → hãy dùng create_txt.
- Khi trả về JSON, không thêm giải thích, chỉ trả JSON hợp lệ duy nhất.
- Nếu không đủ dữ liệu, hãy nói rõ ràng rằng bạn chưa có thông tin chính xác.
- Không bịa thêm, không suy diễn.
### Trả lời:
...
2025-10-12 18:22:50,804 [INFO] rag_api - ✅ Đã trả lời: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nh...
2025-10-12 18:23:12,573 [INFO] rag_api - 📥 Câu hỏi: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
2025-10-12 18:23:14,740 [INFO] httpx - HTTP Request: POST http://localhost:6333/collections/text_chunks/points/search "HTTP/1.1 200 OK"
2025-10-12 18:23:26,449 [INFO] src.chatbot.llm_client - 🧠 Gửi prompt tới Gemini...
2025-10-12 18:23:35,323 [INFO] src.chatbot.llm_client - 💬 Gemini output: Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong...
2025-10-12 18:23:56,910 [INFO] src.chatbot.llm_client - 🤖 Function call JSON: {'action': 'create_txt', 'params': {'text': 'Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong các chức năng hành chính, và có thêm 900 người được tài trợ của bên thứ ba.'}}
2025-10-12 18:27:48,814 [WARNING] function_executor - Không thể parse JSON từ LLM output: 'dict' object has no attribute 'find'
2025-10-12 18:29:06,767 [INFO] numexpr.utils - NumExpr defaulting to 12 threads.
2025-10-12 18:29:38,852 [INFO] numexpr.utils - NumExpr defaulting to 12 threads.
2025-10-12 18:29:56,031 [INFO] httpx - HTTP Request: GET http://localhost:6333 "HTTP/1.1 200 OK"
2025-10-12 18:29:56,039 [INFO] httpx - HTTP Request: GET http://localhost:6333/collections/text_chunks "HTTP/1.1 200 OK"
2025-10-12 18:29:56,050 [INFO] sentence_transformers.SentenceTransformer - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
2025-10-12 18:30:02,939 [INFO] src.chatbot.llm_client - 🔮 LLMClient khởi tạo với model: models/gemini-2.0-flash-001
2025-10-12 18:30:02,939 [INFO] function_router - 🔧 Đã đăng ký tool: create_txt
2025-10-12 18:30:02,939 [INFO] rag_api - ✅ RAGPipeline đã khởi tạo thành công.
2025-10-12 18:30:05,771 [INFO] rag_api - 📥 Câu hỏi: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
2025-10-12 18:30:10,939 [INFO] httpx - HTTP Request: POST http://localhost:6333/collections/text_chunks/points/search "HTTP/1.1 200 OK"
2025-10-12 18:30:27,710 [INFO] src.chatbot.llm_client - 🧠 Gửi prompt tới Gemini...
2025-10-12 18:30:32,763 [INFO] src.chatbot.llm_client - 💬 Gemini output: Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong...
2025-10-12 18:30:37,214 [INFO] src.chatbot.llm_client - 🤖 Function call JSON: {'action': 'create_txt', 'params': {'text': 'Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên hành chính và 900 nhân viên được tài trợ.'}}
2025-10-12 18:30:54,917 [INFO] function_router - ⚙️ Gọi tool 'create_txt' với params={'text': 'Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên hành chính và 900 nhân viên được tài trợ.'}
2025-10-12 18:30:54,920 [INFO] function_router - ✅ Tool 'create_txt' thực thi thành công
2025-10-12 18:31:01,729 [INFO] root - 📤 Gửi prompt tới LLM: ### Vai trò hệ thống:
Bạn là trợ lý AI chuyên nghiệp, hiểu tiếng Việt, có khả năng trả lời tự nhiên, chính xác và ngắn gọn. Bạn có thể vừa trả lời, vừa thực hiện hành động theo yêu cầu người dùng. Nếu người dùng chỉ đặt câu hỏi, hãy trả lời tự nhiên. Nếu người dùng yêu cầu hành động, hãy trả về **đúng JSON hợp lệ**.
### Dữ liệu ngữ cảnh:
Dưới đây là các thông tin có liên quan:
(Đoạn 1 - score=0.818)
Đại học Hanover
Đại học Hanover, chính thức là Gottfried Wilhelm Leibniz Universität Hannover hoặc Luh, là một trường đại học nằm ở Hanover, Đức. Trường được thành lập vào năm 1831 và là tổ chức đào tạo đại học lớn thứ hai ở Niedersachsen. Đại học Leibniz Hannover là một thành viên của TU9, một hiệp hội của chín Viện Công nghệ hàng đầu tại Đức.
Lịch sử.
Trường đại học này được thành lập vào năm 1831 là một trường cao đẳng thương mại. Trường đã bắt đầu nghiên cứu toán học, kiến trúc, kỹ thuật, lịch sử tự nhiên, vật lý, hóa học, vẽ, công nghệ, nghiên cứu và kế toán. Năm 1879 trường đã được nâng cấp thành Trường Cao đẳng Công nghệ Hoàng gia, năm 1898 nó đã được trao quyền đào tạo tiến sĩ.
Lĩnh vực của trường đại học này, từ đầu của nó, tập trung vào khoa học và công nghệ. Trong thế kỷ 20, các ngành nghệ thuật và nhân văn đã được bổ sung, và trường đã được sáp nhập thêm Trường Cao đẳng Sư phạm trước đó là một trường độc lập.
Khoa.
Trường có 9 khoa với hơn 150 cấp độ độ đầu tiên toàn thời gian và các khóa học trình độ bán thời gian, khiến cho trường này là trường đại học lớn thứ hai của giáo dục đại học tổ chức ở Lower Saxony. Đội ngũ cán bộ trường đại học này bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong các chức năng hành chính, và có thêm 900 người được tài trợ của bên thứ ba.
### Công cụ có thể sử dụng:
Hiện bạn có thể sử dụng công cụ sau:
1. create_txt(text: str) — Tạo file .txt chứa nội dung văn bản.
Nếu người dùng yêu cầu hành động (như tạo, lưu, ghi vào file),
hãy trả về đúng JSON theo mẫu sau:
{"action": "create_txt", "params": {"text": "<nội dung cần lưu>"}}
Nếu người dùng chỉ hỏi thông tin, hãy trả lời văn bản tự nhiên, không JSON.
### Câu hỏi của người dùng:
Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
### Hướng dẫn cho AI:
- Nếu người dùng chỉ hỏi → trả lời tự nhiên, ngắn gọn, chính xác.
- Nếu người dùng yêu cầu lưu/tạo file → hãy dùng create_txt.
- Khi trả về JSON, không thêm giải thích, chỉ trả JSON hợp lệ duy nhất.
- Nếu không đủ dữ liệu, hãy nói rõ ràng rằng bạn chưa có thông tin chính xác.
- Không bịa thêm, không suy diễn.
### Trả lời:
...
2025-10-12 18:31:04,946 [INFO] rag_api - ✅ Đã trả lời: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nh...
2025-10-12 18:36:06,726 [INFO] numexpr.utils - NumExpr defaulting to 12 threads.
2025-10-12 18:36:09,883 [INFO] httpx - HTTP Request: GET http://localhost:6333 "HTTP/1.1 200 OK"
2025-10-12 18:36:09,916 [INFO] httpx - HTTP Request: GET http://localhost:6333/collections/text_chunks "HTTP/1.1 200 OK"
2025-10-12 18:36:09,923 [INFO] sentence_transformers.SentenceTransformer - Load pretrained SentenceTransformer: Alibaba-NLP/gte-multilingual-base
2025-10-12 18:36:16,771 [INFO] src.chatbot.llm_client - 🔮 LLMClient khởi tạo với model: models/gemini-2.0-flash-001
2025-10-12 18:36:16,771 [INFO] function_router - 🔧 Đã đăng ký tool: create_txt
2025-10-12 18:36:16,771 [INFO] rag_api - ✅ RAGPipeline đã khởi tạo thành công.
2025-10-12 18:36:25,890 [INFO] rag_api - 📥 Câu hỏi: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
2025-10-12 18:36:26,245 [INFO] httpx - HTTP Request: POST http://localhost:6333/collections/text_chunks/points/search "HTTP/1.1 200 OK"
2025-10-12 18:36:26,250 [INFO] src.chatbot.llm_client - 🧠 Gửi prompt tới Gemini...
2025-10-12 18:36:27,947 [INFO] src.chatbot.llm_client - 💬 Gemini output: Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên h...
2025-10-12 18:36:27,948 [INFO] src.chatbot.llm_client - 🤖 Function call JSON: {'action': 'create_txt', 'params': {'text': 'Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên hành chính và 900 người được tài trợ từ bên thứ ba.'}}
2025-10-12 18:36:27,949 [INFO] function_router - ⚙️ Gọi tool 'create_txt' với params={'text': 'Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên hành chính và 900 người được tài trợ từ bên thứ ba.'}
2025-10-12 18:36:27,952 [INFO] function_router - ✅ Tool 'create_txt' thực thi thành công
2025-10-12 18:36:27,952 [INFO] root - 📤 Gửi prompt tới LLM: ### Vai trò hệ thống:
Bạn là trợ lý AI chuyên nghiệp, hiểu tiếng Việt, có khả năng trả lời tự nhiên, chính xác và ngắn gọn. Bạn có thể vừa trả lời, vừa thực hiện hành động theo yêu cầu người dùng. Nếu người dùng chỉ đặt câu hỏi, hãy trả lời tự nhiên. Nếu người dùng yêu cầu hành động, hãy trả về **đúng JSON hợp lệ**.
### Dữ liệu ngữ cảnh:
Dưới đây là các thông tin có liên quan:
(Đoạn 1 - score=0.818)
Đại học Hanover
Đại học Hanover, chính thức là Gottfried Wilhelm Leibniz Universität Hannover hoặc Luh, là một trường đại học nằm ở Hanover, Đức. Trường được thành lập vào năm 1831 và là tổ chức đào tạo đại học lớn thứ hai ở Niedersachsen. Đại học Leibniz Hannover là một thành viên của TU9, một hiệp hội của chín Viện Công nghệ hàng đầu tại Đức.
Lịch sử.
Trường đại học này được thành lập vào năm 1831 là một trường cao đẳng thương mại. Trường đã bắt đầu nghiên cứu toán học, kiến trúc, kỹ thuật, lịch sử tự nhiên, vật lý, hóa học, vẽ, công nghệ, nghiên cứu và kế toán. Năm 1879 trường đã được nâng cấp thành Trường Cao đẳng Công nghệ Hoàng gia, năm 1898 nó đã được trao quyền đào tạo tiến sĩ.
Lĩnh vực của trường đại học này, từ đầu của nó, tập trung vào khoa học và công nghệ. Trong thế kỷ 20, các ngành nghệ thuật và nhân văn đã được bổ sung, và trường đã được sáp nhập thêm Trường Cao đẳng Sư phạm trước đó là một trường độc lập.
Khoa.
Trường có 9 khoa với hơn 150 cấp độ độ đầu tiên toàn thời gian và các khóa học trình độ bán thời gian, khiến cho trường này là trường đại học lớn thứ hai của giáo dục đại học tổ chức ở Lower Saxony. Đội ngũ cán bộ trường đại học này bao gồm 1.120 nhân viên, bao gồm 340 giáo sư, 1.560 nhân viên trong các chức năng hành chính, và có thêm 900 người được tài trợ của bên thứ ba.
### Công cụ có thể sử dụng:
Hiện bạn có thể sử dụng công cụ sau:
1. create_txt(text: str) — Tạo file .txt chứa nội dung văn bản.
Nếu người dùng yêu cầu hành động (như tạo, lưu, ghi vào file),
hãy trả về đúng JSON theo mẫu sau:
{"action": "create_txt", "params": {"text": "<nội dung cần lưu>"}}
Nếu người dùng chỉ hỏi thông tin, hãy trả lời văn bản tự nhiên, không JSON.
### Câu hỏi của người dùng:
Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nhân viên? Lưu thông tin vào file txt
### Hướng dẫn cho AI:
- Nếu người dùng chỉ hỏi → trả lời tự nhiên, ngắn gọn, chính xác.
- Nếu người dùng yêu cầu lưu/tạo file → hãy dùng create_txt.
- Khi trả về JSON, không thêm giải thích, chỉ trả JSON hợp lệ duy nhất.
- Nếu không đủ dữ liệu, hãy nói rõ ràng rằng bạn chưa có thông tin chính xác.
- Không bịa thêm, không suy diễn.
### Trả lời:
...
2025-10-12 18:36:27,952 [INFO] rag_api - ✅ Đã trả lời: Đại học Hanover có bao nhiêu khoa? Có bao nhiêu nh...

1053
logs/api_2025-10-13.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
Đại học Hanover có 9 khoa. Đội ngũ cán bộ của trường bao gồm 1.120 nhân viên, trong đó có 340 giáo sư, 1.560 nhân viên hành chính và 900 người được tài trợ từ bên thứ ba.

View File

@ -0,0 +1 @@
Thời Đại Thiếu Niên Đoàn có 7 thành viên: Đinh Trình Hâm, Mã Gia Kỳ, Trương Chân Nguyên, Tống Á Hiên, Hạ Tuấn Lâm, Nghiêm Hạo Tường, Lưu Diệu Văn.

View File

@ -86,11 +86,14 @@ async def chat_endpoint(req: ChatRequest):
try:
result = rag_pipeline.run(req.query, top_k=req.top_k)
logging.info(f"📤 Gửi prompt tới LLM: {result['prompt']}...")
response = {
"query": req.query,
"answer": result["answer"],
"context_count": len(result.get("context_used", [])),
"context_used": result.get("context_used", []),
"sources": result.get("sources", []), # ✅ mới
"used_web": result.get("used_web", False), # ✅ mới
}
logger.info(f"✅ Đã trả lời: {req.query[:50]}...")

View File

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from abc import ABC, abstractmethod
class BaseTool(ABC):
"""
Interface (chuẩn chung) cho mọi công cụ (tool).
Mọi tool con đều phải kế thừa implement hàm execute().
"""
# Tên duy nhất của tool (LLM và router sẽ dùng để gọi)
name: str = "base_tool"
# Mô tả ngắn, để LLM biết công cụ này làm gì (sẽ hiển thị trong prompt)
description: str = "No description."
@abstractmethod
def execute(self, **kwargs) -> str:
"""
Phương thức bắt buộc.
- Nhận tham số (kwargs) từ hệ thống hoặc LLM
- Trả về kết quả sau khi thực thi công cụ
"""
...

View File

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
"""
FunctionExecutor giải output LLM & kích hoạt tool nếu cần.
"""
import json
import logging
from typing import Any, Dict, Optional
from .router import FunctionRouter
from src.chatbot.function_tools.google_tool import GoogleSearchTool
logger = logging.getLogger("function_executor")
def _strip_code_fences(s: str) -> str:
if not isinstance(s, str):
return s
s = s.strip()
if s.startswith("```"):
s = s.strip("`")
# nếu có ```json ... ``` thì bỏ dòng đầu/cuối
if s.startswith("json"):
s = s[4:]
return s.strip()
class FunctionExecutor:
def __init__(self):
self.router = FunctionRouter()
self.google_tool = GoogleSearchTool()
def parse_action(self, text: Any) -> Optional[Dict[str, Any]]:
"""Parse JSON action từ LLM output (hỗ trợ string/dict, có code-fence)."""
if not text:
return None
if isinstance(text, dict):
return text if "action" in text else None
if isinstance(text, str):
try:
s = _strip_code_fences(text)
start = s.find("{")
end = s.rfind("}") + 1
if start == -1 or end == 0:
return None
data = json.loads(s[start:end])
return data if isinstance(data, dict) and "action" in data else None
except Exception as e:
logger.warning(f"Không thể parse JSON từ LLM output: {e}")
return None
return None
def execute_if_needed(self, llm_output: Any, original_query: str = "", allow_web_search: bool = True) -> Dict:
"""
Trả về dict chuẩn cho pipeline:
{
"text": "<văn bản gốc của LLM (nếu có)>",
"action_result": "<kết quả tool action nếu có hoặc None>",
"web": {"summary_text":..., "items":[...]} | None
}
"""
result: Dict[str, Any] = {"text": "", "action_result": None, "web": None}
# Base text
result["text"] = llm_output if isinstance(llm_output, str) else json.dumps(llm_output, ensure_ascii=False)
# 1⃣ Nếu LLM yêu cầu action → parse JSON
data = self.parse_action(llm_output)
if data:
action = data.get("action")
params = data.get("params", {})
try:
logger.info(f"🔧 LLM yêu cầu thực thi tool: {action}({params})")
exec_out = self.router.execute_tool(action, **params)
# 🧠 Nếu tool là search_google → gán vào result["web"]
if action == "search_google" and isinstance(exec_out, dict):
result["web"] = exec_out
result["action_result"] = f"✅ Đã tìm kiếm Google với truy vấn: {params.get('query', '')}"
else:
result["action_result"] = exec_out
return result
except Exception as e:
msg = f"⚠️ Tool '{action}' lỗi: {e}"
logger.exception(msg)
result["action_result"] = msg
return result
# 2⃣ Nếu không có action → fallback Google (nếu bật cờ và có query)
# if allow_web_search and original_query:
# logger.info("⚠️ Không có action → fallback Google Search")
# web = self.google_tool.execute(original_query, max_results=3, fetch_pages=True)
# result["web"] = web
# return result
# 3⃣ Nếu không có gì cả
return result

View File

@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""
GoogleSearchTool (Custom Search API + Smart Filter + Page fetch)
----------------------------------------------------------------
- Tìm bằng Google CSE
- Lọc domain uy tín + khử trùng lặp theo domain
- Tải nội dung trang (HTML) rút trích đoạn <p>
- Chuẩn hóa output: trả về cả summary_text items (để two-pass sources)
"""
import logging
from typing import List, Dict
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from src.chatbot.function_tools.base_tool import BaseTool
from src.core.config import GOOGLE_API_KEY, GOOGLE_CX, TRUSTED_DOMAINS
logger = logging.getLogger("google_search_tool")
class GoogleSearchTool(BaseTool):
name = "search_google"
description = "Tìm thông tin mới nhất trên Internet bằng Google Custom Search API."
def __init__(self):
if not GOOGLE_API_KEY or not GOOGLE_CX:
raise ValueError("❌ Thiếu GOOGLE_API_KEY hoặc GOOGLE_CX trong config.")
self.api_key = GOOGLE_API_KEY
self.cx = GOOGLE_CX
self.trusted_domains = set(TRUSTED_DOMAINS)
# ----------------- helpers -----------------
def _domain(self, url: str) -> str:
return urlparse(url).netloc.lower()
def _is_trusted(self, url: str) -> bool:
dom = self._domain(url)
return any(trust in dom for trust in self.trusted_domains)
def _summarize(self, text: str, limit: int = 280) -> str:
if not text:
return ""
text = " ".join(text.split())
if len(text) <= limit:
return text
cut = text.rfind(".", 0, limit)
if cut < int(limit * 0.6):
cut = limit
return text[:cut].rstrip() + ""
def _fetch_page_text(self, url: str, limit_chars: int = 1800) -> str:
try:
headers = {"User-Agent": "Mozilla/5.0 (RAGBot)"}
r = requests.get(url, headers=headers, timeout=10)
if r.status_code != 200 or not r.text:
return ""
soup = BeautifulSoup(r.text, "html.parser")
paragraphs = [p.get_text(" ", strip=True) for p in soup.find_all("p")]
text = " ".join(paragraphs)
text = " ".join(text.split())
if len(text) > limit_chars:
text = text[:limit_chars] + ""
return text
except Exception as e:
logger.warning(f"Fetch page failed {url}: {e}")
return ""
# ----------------- main -----------------
def execute(self, query: str, max_results: int = 5, fetch_pages: bool = True) -> Dict:
"""
Return:
{
"summary_text": "<chuỗi gộp đã format cho hiển thị nhanh>",
"items": [
{"title":..., "snippet":..., "link":..., "domain":..., "page_text":...},
...
]
}
"""
try:
url = "https://www.googleapis.com/customsearch/v1"
params = {
"key": self.api_key,
"cx": self.cx,
"q": query,
"num": max_results * 2, # xin nhiều hơn chút để lọc
"hl": "vi",
}
resp = requests.get(url, params=params, timeout=10)
if resp.status_code != 200:
return {
"summary_text": f"⚠️ Google API lỗi {resp.status_code}: {resp.text}",
"items": []
}
data = resp.json()
raw_items = data.get("items", [])
# Lọc domain uy tín + khử trùng lặp theo domain
picked: List[Dict] = []
seen_domains = set()
for it in raw_items:
link = it.get("link") or ""
if not link:
continue
dom = self._domain(link)
if not self._is_trusted(link):
continue
if dom in seen_domains:
continue
seen_domains.add(dom)
item = {
"title": (it.get("title") or "").strip(),
"snippet": self._summarize(it.get("snippet") or ""),
"link": link,
"domain": dom,
"page_text": ""
}
if fetch_pages:
item["page_text"] = self._fetch_page_text(link)
picked.append(item)
if len(picked) >= max_results:
break
if not picked:
return {
"summary_text": f"❌ Không tìm thấy nguồn uy tín cho truy vấn: '{query}'",
"items": []
}
# Gộp summary_text để hiển thị/nhét prompt dễ
blocks = []
for i, it in enumerate(picked, 1):
page_part = f"\n📄 {self._summarize(it['page_text'], 700)}" if it["page_text"] else ""
blocks.append(
f"{i}. {it['title']}\n{it['snippet']}{page_part}\n🔗 {it['link']}"
)
summary_text = f"🔍 Kết quả Google (đã lọc nguồn) cho: '{query}'\n\n" + "\n\n".join(blocks)
return {"summary_text": summary_text, "items": picked}
except Exception as e:
return {"summary_text": f"⚠️ Lỗi khi tìm kiếm Google: {e}", "items": []}

View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""
FunctionRouter Bộ điều phối công cụ (function tools)
------------------------------------------------------
- Quản điều hướng các công cụ LLM thể gọi.
- Khi LLM trả JSON {"action": "<tên_tool>", "params": {...}},
router sẽ xác định đúng tool gọi thực thi tương ứng.
"""
import logging
from typing import Dict
from .base_tool import BaseTool
from .txt_tool import TxtTool
from .google_tool import GoogleSearchTool # ✅ Thêm import Google search
logger = logging.getLogger("function_router")
class FunctionRouter:
"""
Router trung tâm, chịu trách nhiệm:
- Đăng tool vào registry (theo tên)
- Điều phối thực thi tool
"""
def __init__(self):
# Danh mục các tool khả dụng (tên -> instance)
self.tools: Dict[str, BaseTool] = {}
# 🔧 Đăng ký các tool mặc định
self.register_tool(TxtTool())
self.register_tool(GoogleSearchTool())
logger.info(f"✅ FunctionRouter khởi tạo, có {len(self.tools)} công cụ sẵn sàng.")
# ------------------------------------------------------------
# Đăng ký tool mới
# ------------------------------------------------------------
def register_tool(self, tool: BaseTool):
"""Đăng ký 1 tool mới vào router"""
if not isinstance(tool, BaseTool):
raise TypeError("Tool phải kế thừa BaseTool.")
if not hasattr(tool, "name"):
raise ValueError("Tool phải có thuộc tính name.")
if not callable(getattr(tool, "execute", None)):
raise ValueError("Tool phải có hàm execute().")
# Ghi đè nếu trùng tên
if tool.name in self.tools:
logger.warning(f"⚠️ Tool '{tool.name}' đã tồn tại, ghi đè instance mới.")
self.tools[tool.name] = tool
logger.info(f"🔧 Đã đăng ký tool: {tool.name}{tool.description}")
# ------------------------------------------------------------
# Lấy danh sách tool (cho debug / hiển thị)
# ------------------------------------------------------------
def list_tools(self) -> Dict[str, str]:
"""Trả về danh sách tên và mô tả của tất cả công cụ"""
return {name: t.description for name, t in self.tools.items()}
# ------------------------------------------------------------
# Thực thi tool dựa trên action
# ------------------------------------------------------------
def execute_tool(self, action: str, **params) -> str:
"""
Gọi tool theo tên hành động (action).
dụ:
router.execute_tool("create_txt", text="Xin chào!")
router.execute_tool("search_google", query="tin tức hôm nay")
Trả về: Chuỗi kết quả hoặc message lỗi.
"""
tool = self.tools.get(action)
if not tool:
msg = f"❌ Không tìm thấy tool '{action}' trong router."
logger.error(msg)
return msg
try:
logger.info(f"⚙️ Thực thi tool '{action}' với params={params}")
result = tool.execute(**params)
logger.info(f"✅ Tool '{action}' thực thi thành công.")
return result
except Exception as e:
msg = f"❌ Lỗi khi thực thi tool '{action}': {e}"
logger.exception(msg)
return msg

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
"""
TxtTool công cụ tạo file văn bản .txt
"""
import os
import re
from datetime import datetime
from .base_tool import BaseTool
def _slugify(s: str) -> str:
"""Chuẩn hóa tên file an toàn (không dấu, không ký tự lạ)."""
s = (s or "").lower().strip()
s = re.sub(r"[^a-z0-9._-]+", "-", s)
s = re.sub(r"-{2,}", "-", s).strip("-")
return s or "output"
class TxtTool(BaseTool):
"""Tool tạo file TXT từ nội dung văn bản."""
# Định danh của tool (LLM sẽ dùng để gọi)
name = "create_txt"
# Mô tả — dùng cho LLM biết công cụ này làm gì
description = "Tạo file TXT. Tham số: {text: str, filename?: str}"
def execute(self, text: str, filename: str | None = None) -> str:
"""
Tạo file txt chứa nội dung được truyền vào.
Trả về thông báo thành công ( đường dẫn file).
"""
# 1⃣ Đảm bảo thư mục outputs tồn tại
os.makedirs("outputs", exist_ok=True)
# 2⃣ Nếu không chỉ định tên, tạo theo timestamp
if not filename:
filename = f"file_{datetime.now():%Y%m%d_%H%M%S}.txt"
# 3⃣ Chuẩn hóa tên file (loại bỏ ký tự lạ)
filename = _slugify(filename)
if not filename.endswith(".txt"):
filename += ".txt"
path = os.path.join("outputs", filename)
# 4⃣ Ghi nội dung vào file
try:
with open(path, "w", encoding="utf-8") as f:
f.write((text or "")[:20000]) # giới hạn 20k ký tự cho an toàn
except Exception as e:
return f"❌ Lỗi khi ghi file: {e}"
# 5⃣ Trả kết quả
return f"✅ File TXT đã được tạo: {path}"

View File

@ -1,24 +1,78 @@
# -*- coding: utf-8 -*-
"""
LLMClient
---------
Tầng giao tiếp với hình ngôn ngữ Gemini (Google Generative AI).
Hỗ trợ cả phản hồi văn bản tự nhiên function calling (JSON).
"""
import os
import json
import re
import google.generativeai as genai
import logging
from typing import Any, Optional
from src.core.config import GEMINI_API_KEY, GEMINI_MODEL
logger = logging.getLogger(__name__)
class LLMClient:
def __init__(self, api_key: str = GEMINI_API_KEY, model: str = GEMINI_MODEL):
if not api_key:
raise ValueError("Thiếu GEMINI_API_KEY trong config hoặc .env")
raise ValueError("Thiếu GEMINI_API_KEY trong config hoặc .env")
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel(model)
logger.info(f"🔮 LLMClient khởi tạo với model: {model}")
def generate(self, prompt: str) -> str:
# ----------------------------------------------------------
def _try_extract_json(self, text: str) -> Optional[dict]:
"""
Gửi prompt vào Gemini trả về nội dung text.
Cố gắng trích JSON từ đầu ra LLM.
- Loại bỏ text thừa, chỉ giữ đoạn {...} đầu tiên.
- Nếu JSON chứa 'action', coi function call hợp lệ.
"""
try:
response = self.model.generate_content(prompt)
return response.text.strip() if response.text else ""
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
return None
json_str = match.group(0).strip()
parsed = json.loads(json_str)
if isinstance(parsed, dict) and "action" in parsed:
return parsed
return None
except Exception as e:
print(f"[GeminiLLMClient] Lỗi khi gọi Gemini API: {e}")
return ""
logger.warning(f"⚠️ Không thể parse JSON: {e}")
return None
# ----------------------------------------------------------
def generate(self, prompt: str) -> Any:
"""
Gửi prompt vào Gemini trả về:
- dict (nếu function call)
- str (nếu câu trả lời tự nhiên)
"""
try:
logger.info("🧠 Gửi prompt tới Gemini...")
response = self.model.generate_content(prompt)
text = response.text.strip() if response.text else ""
logger.info(f"💬 Gemini output: {text[:120]}...")
# ✅ Cố gắng parse JSON (function call)
parsed = self._try_extract_json(text)
if parsed:
logger.info(f"🤖 Function call JSON: {parsed}")
return parsed
# 🔤 Nếu không phải JSON → trả lời tự nhiên
return text
except Exception as e:
logger.exception(f"❌ Lỗi khi gọi Gemini API: {e}")
return f"Lỗi LLM: {str(e)}"

View File

@ -18,43 +18,33 @@ class PromptBuilder:
context_label: str = "Dưới đây là các thông tin có liên quan:"
):
self.system_prompt = system_prompt or (
"Bạn là trợ lý AI chuyên nghiệp, hiểu tiếng Việt, "
"có khả năng trả lời tự nhiên, chính xác và ngắn gọn. "
"Chỉ sử dụng thông tin trong phần ngữ cảnh để trả lời. "
"Nếu không đủ dữ liệu, hãy nói 'Tôi chưa có thông tin để trả lời chính xác' "
"và không phỏng đoán hoặc suy luận thêm."
"Bạn là trợ lý AI thông minh, hiểu tiếng Việt, có khả năng trả lời tự nhiên, "
"chính xác và ngắn gọn. Bạn được phép sử dụng các công cụ có sẵn để tìm kiếm "
"hoặc thực hiện hành động nếu cần."
)
self.max_context_len = max_context_len
self.context_label = context_label
# -----------------------------------------------------
def _smart_truncate(self, text: str, limit: int) -> str:
"""Cắt ngắn văn bản theo giới hạn ký tự, ưu tiên dừng ở dấu câu."""
text = text.strip()
if len(text) <= limit:
return text
cut = text.rfind(".", 0, limit)
if cut < int(limit * 0.6): # không tìm thấy dấu chấm hợp lý
if cut < int(limit * 0.6):
cut = limit
return text[:cut].rstrip() + ""
# -----------------------------------------------------
# -----------------------------------------------------
def build_context_block(self, retrieved_docs: List[Dict]) -> str:
"""
Ghép các đoạn văn bản được truy xuất từ Qdrant thành block context.
- Luôn đảm bảo ít nhất một đoạn context.
- Cắt ngắn tự động để không vượt quá giới hạn max_context_len.
"""
"""Ghép các đoạn văn bản được truy xuất từ Qdrant thành block context."""
if not retrieved_docs:
return "(Không có dữ liệu ngữ cảnh)"
sorted_docs = sorted(retrieved_docs, key=lambda x: x.get("score", 0), reverse=True)
context_blocks = []
total_len = 0
budget = self.max_context_len - len(self.context_label) - 50 # trừ phần tiêu đề
context_blocks, total_len = [], 0
budget = self.max_context_len - len(self.context_label) - 50
for i, doc in enumerate(sorted_docs, start=1):
text = (doc.get("text") or "").strip()
@ -76,7 +66,6 @@ class PromptBuilder:
if total_len >= budget:
break
# Nếu vì lý do nào đó chưa có đoạn nào, lấy đoạn đầu tiên
if not context_blocks:
first = sorted_docs[0]
header = f"(Đoạn 1 - score={first.get('score', 0):.3f})\n"
@ -85,30 +74,59 @@ class PromptBuilder:
return f"{self.context_label}\n\n" + "\n\n".join(context_blocks)
# -----------------------------------------------------
# Xây prompt hoàn chỉnh
# -----------------------------------------------------
def build_prompt(self, user_query: str, retrieved_docs: List[Dict]) -> str:
"""
Tạo prompt hoàn chỉnh cho LLM.
"""
"""Tạo prompt hoàn chỉnh cho LLM, ép trả JSON nếu thiếu dữ liệu."""
context_block = self.build_context_block(retrieved_docs)
prompt = f"""### Vai trò hệ thống:
tools_description = """
Bạn thể sử dụng các công cụ sau:
1) search_google(query: str, max_results: int = 5)
Dùng khi ngữ cảnh không đủ hoặc không thông tin để trả lời chính xác.
2) create_txt(text: str)
Dùng khi người dùng yêu cầu lưu hoặc tạo file văn bản.
Khi cần dùng công cụ, bạn PHẢI trả về **JSON hợp lệ duy nhất** theo mẫu:
{"action": "<tên_công_cụ>", "params": {...}}
dụ:
{"action": "search_google", "params": {"query": "Stray Kids có bao nhiêu thành viên?"}}
{"action": "create_txt", "params": {"text": "Nội dung cần lưu"}}
Không bao giờ thêm chữ, tự hay lời giải thích nào ngoài JSON.
"""
rules = """
Quy tắc ra quyết định:
1 Nếu dữ liệu trong ngữ cảnh (context) chứa đủ chi tiết để trả lời chính xác Trả lời văn bản tự nhiên, KHÔNG JSON.
2 Nếu ngữ cảnh thiếu, hồ, hoặc không dữ liệu liên quan TRẢ VỀ JSON:
{"action": "search_google", "params": {"query": "<câu hỏi người dùng>"}}
3 Nếu người dùng yêu cầu lưu/tạo file TRẢ VỀ JSON:
{"action": "create_txt", "params": {"text": "<nội dung cần lưu>"}}
4 Nếu bạn không chắc chắn, cũng hãy gọi search_google thay suy đoán.
"""
prompt = f"""### Vai trò hệ thống
{self.system_prompt}
### Dữ liệu ngữ cảnh:
### Ngữ cảnh (retrieved from database)
{context_block}
### Câu hỏi của người dùng:
### Công cụ có thể sử dụng
{tools_description}
### Câu hỏi người dùng
{user_query}
### Hướng dẫn cho AI:
- Trả lời ngắn gọn, ràng, đúng trọng tâm.
- Dựa hoàn toàn vào thông tin trong ngữ cảnh.
- Nếu thông tin không trong ngữ cảnh, hãy nói ràng rằng bạn chưa dữ liệu để trả lời chính xác.
- Không bịa thêm, không suy diễn.
### Hướng dẫn cho AI
{rules}
### Trả lời:
### Định dạng đầu ra
- Nếu đủ thông tin trả lời tự nhiên, ngắn gọn, chính xác.
- Nếu thiếu thông tin trả về JSON như hướng dẫn trên, KHÔNG văn bản khác.
### Trả lời
"""
return prompt

View File

@ -1,39 +1,90 @@
# -*- coding: utf-8 -*-
"""
RAGPipeline
-----------
Kết nối các module:
- Retriever (Qdrant)
- PromptBuilder
- LLMClient
Đầu vào: user_query (string)
Đầu ra: câu trả lời (string)
RAGPipeline RAG + Function Calling + Google fallback + Two-pass refine
"""
from typing import Dict, Any, List
from typing import List, Dict, Any
from src.chatbot.llm_client import LLMClient
from src.chatbot.retriever import Retriever
from src.chatbot.prompt_builder import PromptBuilder
from src.chatbot.function_tools.function_executor import FunctionExecutor
from src.core.config import ALLOW_WEB_SEARCH, SECOND_PASS
class RAGPipeline:
def __init__(self):
self.retriever = Retriever()
self.prompt_builder = PromptBuilder()
self.llm = LLMClient()
self.function_executor = FunctionExecutor()
def run(self, user_query: str, top_k: int = 5) -> Dict[str, Any]:
"""Trả về cả câu trả lời và context dùng"""
def _build_refine_prompt(self, user_query: str, web_summary: str, items: List[Dict]) -> str:
# Tạo block nguồn gọn để LLM trích dẫn
links = [it.get("link", "") for it in items if it.get("link")]
sources_text = "\n".join([f"- {url}" for url in links])
return f"""### Dữ liệu web (đã thu thập):
{web_summary}
### Nguồn:
{sources_text}
### Câu hỏi gốc:
{user_query}
### Yêu cầu:
- Tóm tắt chính xác súc tích (36 câu), ngắn gọn, dễ hiểu.
- Không bịa đặt. Nếu thông tin không chắc chắn, nêu mức độ chắc chắn.
- Trích dẫn 13 nguồn cuối, dạng: "Nguồn: <link1>, <link2>"
"""
def run(self, user_query: str, top_k: int = 5, allow_web_search: bool = ALLOW_WEB_SEARCH, second_pass: bool = SECOND_PASS) -> Dict[str, Any]:
# 1) RAG retrieve
docs = self.retriever.search(user_query, top_k=top_k)
# 2) Build prompt từ context
prompt = self.prompt_builder.build_prompt(user_query, docs)
answer = self.llm.generate(prompt)
# 3) Gọi LLM (pass 1)
llm_answer = self.llm.generate(prompt)
# 4) Thực thi tool nếu có / fallback Google
exec_result = self.function_executor.execute_if_needed(
llm_output=llm_answer,
original_query=user_query,
allow_web_search=allow_web_search
)
# Chuẩn bị output mặc định
final_answer = exec_result.get("text") or ""
sources = []
# Nếu có web result → two-pass refine (nếu bật)
web = exec_result.get("web")
if web and isinstance(web, dict) and web.get("items"):
sources = [it.get("link") for it in web["items"] if it.get("link")]
if second_pass:
refine_prompt = self._build_refine_prompt(user_query, web.get("summary_text", ""), web["items"])
final_answer = self.llm.generate(refine_prompt)
# Nếu có action_result (VD: create_txt), nối thêm thông báo ngắn
action_result = exec_result.get("action_result")
if action_result:
final_answer = f"{final_answer}\n\n🛠️ Tool: {action_result}"
# 5) Trả đầy đủ cho API
return {
"answer": answer,
"answer": final_answer.strip(),
"context_used": docs,
"prompt": prompt,
"sources": sources, # ✅ link nguồn (nếu có)
"used_web": bool(web), # ✅ có dùng web hay không
}
if __name__ == "__main__":
pipeline = RAGPipeline()
query = "Bạn biết Mahola là ai không?"
response = pipeline.run(query)
print("Câu hỏi:", query)
print("Trả lời:", response)
q = "Acid propionic là gì? Hôm nay có cập nhật gì mới không?"
out = pipeline.run(q)
print("Answer:\n", out["answer"])
print("Sources:", out["sources"])

View File

@ -48,12 +48,22 @@ load_dotenv()
DATA_RAW = Path(os.getenv("DATA_RAW", "./data/data_raw10k")).resolve()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "models/gemini-2.0-flash-001")
SERPER_API_KEY = os.getenv("SERPER_API_KEY", "14ea1e5de7b68084d3e41a4efe204108a8fa76c6fab062b00fa1912a97d3c28b")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "AIzaSyCOtXgt7M6qKlj5srF8hF_7iyvZbBrVnhA")
GOOGLE_CX = os.getenv("GOOGLE_CX", "9379e94eed0f145b4")
# ==== Embedding model (SentenceTransformers) ====
EMBED_MODEL = os.getenv("EMBED_MODEL", "Alibaba-NLP/gte-multilingual-base")
EMBED_DIM = int(os.getenv("EMBED_DIM", "768"))
EMBED_DEVICE = os.getenv("EMBED_DEVICE", "cuda") # "cuda" | "cpu" | "auto"
EMBED_BATCH_SIZE = int(os.getenv("EMBED_BATCH_SIZE", "64"))
_TRUSTED = os.getenv(
"TRUSTED_DOMAINS",
"wikipedia.org,chinhphu.vn,vnexpress.net,bbc.com,nhandan.vn,tuoitre.vn,thanhnien.vn,zingnews.vn,vov.vn,vietnamnet.vn"
)
TRUSTED_DOMAINS = [d.strip().lower() for d in _TRUSTED.split(",") if d.strip()]
ALLOW_WEB_SEARCH = os.getenv("ALLOW_WEB_SEARCH", "true").lower() == "true"
SECOND_PASS = os.getenv("SECOND_PASS", "true").lower() == "true"
# ==== Semantic chunking ====
# Ngưỡng cosine similarity để tách chủ đề (thấp hơn ngưỡng => tách chunk)

Binary file not shown.

4
utils/test_searchgg.py Normal file
View File

@ -0,0 +1,4 @@
from src.chatbot.function_tools.google_tool import GoogleSearchTool
tool = GoogleSearchTool()
print(tool.execute("Messi là ai?", max_results=2))