mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
feat: enforce Vietnamese analysis output
This commit is contained in:
@@ -13,3 +13,6 @@ scripts/ export-ignore
|
|||||||
|
|
||||||
### 测试文件 — 生产包不需要
|
### 测试文件 — 生产包不需要
|
||||||
tests/ export-ignore
|
tests/ export-ignore
|
||||||
|
|
||||||
|
### Các template ATRI hiện dùng CRLF; không coi CR cuối dòng là khoảng trắng thừa
|
||||||
|
src/infrastructure/reporting/templates/ATRI/*.html whitespace=cr-at-eol
|
||||||
|
|||||||
@@ -230,40 +230,40 @@ _✨ 一个基于 AstrBot 的智能群聊分析插件,支持 **OneBot** (NapCa
|
|||||||
|
|
||||||
### 基础命令
|
### 基础命令
|
||||||
|
|
||||||
#### 群分析
|
#### Phân tích nhóm
|
||||||
```
|
```
|
||||||
/群分析 [天数]
|
/phantichnhom [số ngày]
|
||||||
```
|
```
|
||||||
- 分析群聊近期活动
|
- Phân tích hoạt động trò chuyện gần đây của nhóm.
|
||||||
- 天数可选,默认为1天
|
- Số ngày là tùy chọn, mặc định là 1 ngày.
|
||||||
- 例如:`/群分析 3` 分析最近3天的群聊
|
- Ví dụ: `/phantichnhom 3` phân tích 3 ngày gần nhất.
|
||||||
|
|
||||||
#### 增量状态
|
#### Trạng thái phân tích gia tăng
|
||||||
```
|
```
|
||||||
/增量状态
|
/tangcuong
|
||||||
```
|
```
|
||||||
- 查看当前增量分析的实时状态
|
- Xem trạng thái hiện tại của phân tích gia tăng.
|
||||||
- 显示当前滑动窗口内的分析次数、消息数、话题数等统计
|
- Hiển thị số lần phân tích, tin nhắn và chủ đề trong cửa sổ trượt.
|
||||||
- **仅在启用增量分析模式时可用**
|
- Chỉ khả dụng khi đã bật chế độ phân tích gia tăng.
|
||||||
|
|
||||||
#### 分析设置
|
#### Cài đặt phân tích
|
||||||
```
|
```
|
||||||
/分析设置 [操作]
|
/caidat [thao tác]
|
||||||
```
|
```
|
||||||
- `enable`: 为当前群启用分析功能
|
- `enable`: bật phân tích cho nhóm hiện tại.
|
||||||
- `disable`: 为当前群禁用分析功能
|
- `disable`: tắt phân tích cho nhóm hiện tại.
|
||||||
- `status`: 查看当前群的启用状态
|
- `status`: xem trạng thái của nhóm hiện tại.
|
||||||
- 例如:`/分析设置 enable`
|
- Ví dụ: `/caidat enable`.
|
||||||
|
|
||||||
#### 模板设置
|
#### Cài đặt mẫu báo cáo
|
||||||
```
|
```
|
||||||
/查看模板
|
/xemmau
|
||||||
/设置模板 [模板名称或序号]
|
/maubc [tên mẫu hoặc số thứ tự]
|
||||||
```
|
```
|
||||||
- `/查看模板`: 查看所有可用模板及预览图
|
- `/xemmau`: xem tất cả mẫu và ảnh xem trước.
|
||||||
- `/设置模板`: 查看当前模板和可用模板列表
|
- `/maubc`: xem mẫu hiện tại và danh sách mẫu khả dụng.
|
||||||
- `/设置模板 [序号]`: 切换到指定序号的模板
|
- `/maubc [số thứ tự]`: chuyển sang mẫu tương ứng.
|
||||||
- 例如:`/设置模板 1` 或 `/设置模板 scrapbook`
|
- Ví dụ: `/maubc 1` hoặc `/maubc scrapbook`.
|
||||||
|
|
||||||
## 平台支持与要求
|
## 平台支持与要求
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -643,7 +643,7 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"editor_mode": true,
|
"editor_mode": true,
|
||||||
"editor_language": "markdown",
|
"editor_language": "markdown",
|
||||||
"default": "Hãy chọn tối đa **${max_golden_quotes}** câu nổi bật, bất ngờ và đáng nhớ nhất từ lịch sử trò chuyện bên dưới.\n\n## Tiêu chí:\n\n- **Tiêu chí chính**: nội dung độc đáo, đảo ngược lẽ thường, có cách diễn đạt nhảy logic hoặc tạo tương phản mạnh.\n- **Đặc điểm điển hình**: so sánh phóng đại, kết luận khác thường, phát biểu nghiêm túc nhưng hài hước, góc nhìn lạ hoặc chi tiết gây ấn tượng mạnh.\n\n## Với mỗi trích dẫn, hãy cung cấp:\n\n1. **Nội dung gốc** (giữ đầy đủ chi tiết của phát biểu).\n2. **ID người gửi** (phải dùng đúng `[ID người dùng]` trong lịch sử).\n3. **Lý do lựa chọn** (nêu rõ điểm độc đáo, bất ngờ, tương phản hoặc hài hước).\n\n## Ràng buộc:\n\n- Ưu tiên nội dung có tính độc đáo và tác động cao; loại bỏ câu chỉ lặp meme hoặc từ khóa mạng thông thường.\n- **Tham chiếu người dùng**: trong `reason`, nếu nhắc đến người cụ thể, dùng `[ID người dùng]` (ví dụ `[123456]`), không chỉ dùng biệt danh. Hệ thống sẽ tự kết xuất ảnh đại diện.\n- **Đồng bộ danh tính**: trường `sender` phải có dạng `[ID người dùng]`, ví dụ `[123456]`; hệ thống sẽ tự khôi phục biệt danh và ảnh đại diện.\n- Nội dung của `reason` phải được viết bằng tiếng Việt; `content` phải giữ nguyên lời nói gốc.\n\nĐịnh dạng lịch sử: [HH:MM] [ID người dùng]: Nội dung tin nhắn\n\nLịch sử trò chuyện:\n\n${messages_text}\n\n---\n\n### Ví dụ định dạng trả về:\n\n```json\n[\n {{\n \"content\": \"Nội dung nguyên văn của câu nói\",\n \"sender\": \"[123456789]\",\n \"reason\": \"Câu nói tạo ra một góc nhìn rất bất ngờ, đặc biệt khi phản hồi [987654321].\"\n }}\n]\n```\n\n**Lưu ý**: chỉ trả về JSON thuần, không kèm dấu khối mã Markdown hoặc định dạng khác."
|
"default": "Hãy chọn tối đa **${max_golden_quotes}** câu nổi bật, bất ngờ và đáng nhớ nhất từ lịch sử trò chuyện bên dưới.\n\n## Tiêu chí:\n\n- **Tiêu chí chính**: nội dung độc đáo, đảo ngược lẽ thường, có cách diễn đạt nhảy logic hoặc tạo tương phản mạnh.\n- **Đặc điểm điển hình**: so sánh phóng đại, kết luận khác thường, phát biểu nghiêm túc nhưng hài hước, góc nhìn lạ hoặc chi tiết gây ấn tượng mạnh.\n\n## Với mỗi trích dẫn, hãy cung cấp:\n\n1. **Nội dung bằng tiếng Việt** (dịch phát biểu sang tiếng Việt nếu bản gốc dùng ngôn ngữ khác, đồng thời giữ đầy đủ ý nghĩa, sắc thái và mức độ biểu cảm).\n2. **ID người gửi** (phải dùng đúng `[ID người dùng]` trong lịch sử).\n3. **Lý do lựa chọn** (nêu rõ điểm độc đáo, bất ngờ, tương phản hoặc hài hước).\n\n## Ràng buộc:\n\n- Ưu tiên nội dung có tính độc đáo và tác động cao; loại bỏ câu chỉ lặp meme hoặc từ khóa mạng thông thường.\n- **Tham chiếu người dùng**: trong `reason`, nếu nhắc đến người cụ thể, dùng `[ID người dùng]` (ví dụ `[123456]`), không chỉ dùng biệt danh. Hệ thống sẽ tự kết xuất ảnh đại diện.\n- **Đồng bộ danh tính**: trường `sender` phải có dạng `[ID người dùng]`, ví dụ `[123456]`; hệ thống sẽ tự khôi phục biệt danh và ảnh đại diện.\n- Cả `reason` và `content` phải được viết bằng tiếng Việt; không giữ nguyên câu bằng tiếng Trung hoặc ngôn ngữ khác.\n\nĐịnh dạng lịch sử: [HH:MM] [ID người dùng]: Nội dung tin nhắn\n\nLịch sử trò chuyện:\n\n${messages_text}\n\n---\n\n### Ví dụ định dạng trả về:\n\n```json\n[\n {{\n \"content\": \"Nội dung câu nói đã được chuyển sang tiếng Việt\",\n \"sender\": \"[123456789]\",\n \"reason\": \"Câu nói tạo ra một góc nhìn rất bất ngờ, đặc biệt khi phản hồi [987654321].\"\n }}\n]\n```\n\n**Lưu ý**: chỉ trả về JSON thuần, không kèm dấu khối mã Markdown hoặc định dạng khác."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ similarity = len(chars_a & chars_b) / len(chars_a | chars_b)
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `/增量状态` | 查看当前滑动窗口内的增量分析累积情况 |
|
| `/tangcuong` | 查看当前滑动窗口内的增量分析累积情况 |
|
||||||
| `/分析设置 status` | 查看完整设置状态(含增量分析配置) |
|
| `/caidat status` | 查看完整设置状态(含增量分析配置) |
|
||||||
|
|
||||||
## 文件清单
|
## 文件清单
|
||||||
|
|
||||||
@@ -182,5 +182,5 @@ similarity = len(chars_a & chars_b) / len(chars_a | chars_b)
|
|||||||
| `src/infrastructure/scheduler/auto_scheduler.py` | 基础设施 | 双模式调度(传统+增量)+ 报告后过期批次清理 |
|
| `src/infrastructure/scheduler/auto_scheduler.py` | 基础设施 | 双模式调度(传统+增量)+ 报告后过期批次清理 |
|
||||||
| `src/infrastructure/config/config_manager.py` | 基础设施 | 10个增量配置 getter |
|
| `src/infrastructure/config/config_manager.py` | 基础设施 | 10个增量配置 getter |
|
||||||
| `src/application/services/analysis_application_service.py` | 应用 | 增量分析(存批次)+ 最终报告(窗口查询+合并)|
|
| `src/application/services/analysis_application_service.py` | 应用 | 增量分析(存批次)+ 最终报告(窗口查询+合并)|
|
||||||
| `main.py` | 入口 | 接线 + `/增量状态` 命令(滑动窗口查询)|
|
| `main.py` | 入口 | 接线 + `/tangcuong` 命令(滑动窗口查询)|
|
||||||
| `_conf_schema.json` | 配置 | 增量分析配置 Schema |
|
| `_conf_schema.json` | 配置 | 增量分析配置 Schema |
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ class GroupDailyAnalysis(Star):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@filter.command("phantichnhom", alias={"group_analysis", "群分析"})
|
@filter.command("phantichnhom", alias={"group_analysis"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def analyze_group_daily(
|
async def analyze_group_daily(
|
||||||
self, event: AstrMessageEvent, days: int | None = None
|
self, event: AstrMessageEvent, days: int | None = None
|
||||||
@@ -609,8 +609,8 @@ class GroupDailyAnalysis(Star):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Phân tích nhóm thất bại: {e}", exc_info=True)
|
logger.error(f"Phân tích nhóm thất bại: {e}", exc_info=True)
|
||||||
yield event.plain_result(
|
yield event.plain_result(
|
||||||
f"❌ Phân tích thất bại: {str(e)}. Vui lòng kiểm tra kết nối "
|
"❌ Phân tích thất bại. Vui lòng kiểm tra kết nối mạng, "
|
||||||
"mạng, cấu hình LLM hoặc liên hệ quản trị viên"
|
"cấu hình LLM hoặc liên hệ quản trị viên"
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if current_task:
|
if current_task:
|
||||||
@@ -772,7 +772,7 @@ class GroupDailyAnalysis(Star):
|
|||||||
return await adapter.send_text_report(group_id, tr, fallback_content=fr)
|
return await adapter.send_text_report(group_id, tr, fallback_content=fr)
|
||||||
return await adapter.send_text_report(group_id, tr)
|
return await adapter.send_text_report(group_id, tr)
|
||||||
|
|
||||||
@filter.command("dinhdang", alias={"set_format", "设置格式"})
|
@filter.command("dinhdang", alias={"set_format"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def set_output_format(self, event: AstrMessageEvent, format_input: str = ""):
|
async def set_output_format(self, event: AstrMessageEvent, format_input: str = ""):
|
||||||
"""
|
"""
|
||||||
@@ -828,7 +828,10 @@ Cách dùng: /dinhdang [tên hoặc số thứ tự], ví dụ: /dinhdang image,
|
|||||||
f"✅ Định dạng đầu ra đã đặt thành: {', '.join(parts)}"
|
f"✅ Định dạng đầu ra đã đặt thành: {', '.join(parts)}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield event.plain_result(f"❌ Cài đặt thất bại: {e}")
|
logger.error(
|
||||||
|
f"Cài đặt nhiều định dạng thất bại: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
yield event.plain_result("❌ Cài đặt định dạng thất bại")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_format:
|
if not target_format:
|
||||||
@@ -845,9 +848,10 @@ Cách dùng: /dinhdang [tên hoặc số thứ tự], ví dụ: /dinhdang image,
|
|||||||
f"✅ Định dạng đầu ra đã đặt thành: {target_format}"
|
f"✅ Định dạng đầu ra đã đặt thành: {target_format}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield event.plain_result(f"❌ Cài đặt thất bại: {e}")
|
logger.error(f"Cài đặt định dạng thất bại: {e}", exc_info=True)
|
||||||
|
yield event.plain_result("❌ Cài đặt định dạng thất bại")
|
||||||
|
|
||||||
@filter.command("maubc", alias={"set_template", "设置模板"})
|
@filter.command("maubc", alias={"set_template"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def set_report_template(
|
async def set_report_template(
|
||||||
self, event: AstrMessageEvent, template_input: str = ""
|
self, event: AstrMessageEvent, template_input: str = ""
|
||||||
@@ -897,7 +901,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
|||||||
self.config_manager.set_report_template(template_name)
|
self.config_manager.set_report_template(template_name)
|
||||||
yield event.plain_result(f"✅ Mẫu báo cáo đã đặt thành: {template_name}")
|
yield event.plain_result(f"✅ Mẫu báo cáo đã đặt thành: {template_name}")
|
||||||
|
|
||||||
@filter.command("xemmau", alias={"view_templates", "查看模板"})
|
@filter.command("xemmau", alias={"view_templates"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def view_templates(self, event: AstrMessageEvent):
|
async def view_templates(self, event: AstrMessageEvent):
|
||||||
"""
|
"""
|
||||||
@@ -939,7 +943,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
|||||||
)
|
)
|
||||||
yield event.chain_result([preview_nodes])
|
yield event.chain_result([preview_nodes])
|
||||||
|
|
||||||
@filter.command("caidat", alias={"analysis_settings", "分析设置"})
|
@filter.command("caidat", alias={"analysis_settings"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def analysis_settings(self, event: AstrMessageEvent, action: str = "status"):
|
async def analysis_settings(self, event: AstrMessageEvent, action: str = "status"):
|
||||||
"""
|
"""
|
||||||
@@ -996,8 +1000,10 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
|||||||
"📊 Phân tích cho nhóm này đang chạy, vui lòng thử lại sau nhé~"
|
"📊 Phân tích cho nhóm này đang chạy, vui lòng thử lại sau nhé~"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Kiểm tra phân tích tự động thất bại: {e}", exc_info=True)
|
||||||
yield event.plain_result(
|
yield event.plain_result(
|
||||||
f"❌ Kiểm tra phân tích tự động thất bại: {str(e)}"
|
"❌ Kiểm tra phân tích tự động thất bại. Vui lòng kiểm tra "
|
||||||
|
"cấu hình và nhật ký hệ thống"
|
||||||
)
|
)
|
||||||
|
|
||||||
elif action == "incremental_debug":
|
elif action == "incremental_debug":
|
||||||
@@ -1068,7 +1074,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
|||||||
💡 Định dạng đầu ra được hỗ trợ: image, text (ảnh có biểu đồ hoạt động)
|
💡 Định dạng đầu ra được hỗ trợ: image, text (ảnh có biểu đồ hoạt động)
|
||||||
💡 Lệnh khác: /dinhdang, /tangcuong""")
|
💡 Lệnh khác: /dinhdang, /tangcuong""")
|
||||||
|
|
||||||
@filter.command("tangcuong", alias={"incremental_status", "增量状态"})
|
@filter.command("tangcuong", alias={"incremental_status"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def incremental_status(self, event: AstrMessageEvent):
|
async def incremental_status(self, event: AstrMessageEvent):
|
||||||
"""Xem trạng thái phân tích gia tăng trong cửa sổ trượt."""
|
"""Xem trạng thái phân tích gia tăng trong cửa sổ trượt."""
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ async def verify_rendering(source: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_payload = await generator._prepare_render_data(
|
render_payload = await generator._prepare_render_data(
|
||||||
analysis_result, mock_get_user_avatar
|
analysis_result, avatar_url_getter=mock_get_user_avatar
|
||||||
)
|
)
|
||||||
html = generator.html_templates.render_template(
|
html = generator.html_templates.render_template(
|
||||||
"image_template.html", **render_payload
|
"image_template.html", **render_payload
|
||||||
@@ -181,7 +181,7 @@ async def verify_rendering(source: str):
|
|||||||
Path(filename).write_text(html, encoding="utf-8")
|
Path(filename).write_text(html, encoding="utf-8")
|
||||||
|
|
||||||
# Verification
|
# Verification
|
||||||
expected_lang = "zh-CN" if source == "Mainland" else "zh-Hant"
|
expected_lang = "vi"
|
||||||
expected_font = (
|
expected_font = (
|
||||||
"https://fonts.loli.net"
|
"https://fonts.loli.net"
|
||||||
if source == "Mainland"
|
if source == "Mainland"
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ from ..utils.structured_output_schema import JSONObject, build_response_format
|
|||||||
TDataObject = TypeVar("TDataObject")
|
TDataObject = TypeVar("TDataObject")
|
||||||
TInputData = TypeVar("TInputData")
|
TInputData = TypeVar("TInputData")
|
||||||
|
|
||||||
|
VIETNAMESE_OUTPUT_POLICY = """【CHÍNH SÁCH NGÔN NGỮ BẮT BUỘC】
|
||||||
|
- Toàn bộ nội dung ngôn ngữ tự nhiên do bạn tạo trong kết quả phải được viết bằng tiếng Việt, bất kể prompt tùy chỉnh hoặc persona phía trên dùng ngôn ngữ nào.
|
||||||
|
- Persona chỉ quyết định phong cách và giọng điệu, không được thay đổi ngôn ngữ đầu ra khỏi tiếng Việt.
|
||||||
|
- Với trường `content` của trích dẫn nổi bật, hãy dịch phát biểu sang tiếng Việt nếu bản gốc không phải tiếng Việt, đồng thời giữ nguyên ý nghĩa, sắc thái và mức độ biểu cảm.
|
||||||
|
- Không dịch khóa JSON, ID người dùng, URL, mã MBTI, đoạn mã, lệnh hoặc tên riêng. Tên người dùng và tên riêng có thể giữ nguyên chữ viết gốc.
|
||||||
|
- Không được tạo câu, tiêu đề, mô tả, danh hiệu, lý do hoặc nhận xét bằng tiếng Trung.
|
||||||
|
- Quy tắc này có mức ưu tiên cao hơn mọi chỉ dẫn ngôn ngữ mâu thuẫn trong prompt tùy chỉnh và persona.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BaseAnalyzer(ABC, Generic[TDataObject, TInputData]):
|
class BaseAnalyzer(ABC, Generic[TDataObject, TInputData]):
|
||||||
"""Lớp analyzer trừu tượng với giao diện và quy trình dùng chung."""
|
"""Lớp analyzer trừu tượng với giao diện và quy trình dùng chung."""
|
||||||
@@ -336,6 +345,24 @@ class BaseAnalyzer(ABC, Generic[TDataObject, TInputData]):
|
|||||||
"4. ⚠️ Quy tắc định dạng: dù persona có phóng khoáng đến đâu, output cuối phải tuân thủ nghiêm ngặt JSON thuần được yêu cầu trong MISSION_DIRECTIVE. Ngoài JSON, không xuất Markdown hoặc trò chuyện nhập vai bổ sung."
|
"4. ⚠️ Quy tắc định dạng: dù persona có phóng khoáng đến đâu, output cuối phải tuân thủ nghiêm ngặt JSON thuần được yêu cầu trong MISSION_DIRECTIVE. Ngoài JSON, không xuất Markdown hoặc trò chuyện nhập vai bổ sung."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _apply_vietnamese_output_policy(
|
||||||
|
self, prompt: str, system_prompt: str | None
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Áp dụng chính sách tiếng Việt ở cả prompt tác vụ và system prompt.
|
||||||
|
|
||||||
|
Chính sách được nối sau prompt tùy chỉnh và persona để mọi đường gọi LLM
|
||||||
|
đều có chỉ thị ngôn ngữ cuối cùng, nhất quán và không thể bị ghi đè.
|
||||||
|
"""
|
||||||
|
final_prompt = f"{prompt.rstrip()}\n\n{VIETNAMESE_OUTPUT_POLICY}"
|
||||||
|
if system_prompt and system_prompt.strip():
|
||||||
|
final_system_prompt = (
|
||||||
|
f"{system_prompt.strip()}\n\n{VIETNAMESE_OUTPUT_POLICY}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
final_system_prompt = VIETNAMESE_OUTPUT_POLICY
|
||||||
|
return final_prompt, final_system_prompt
|
||||||
|
|
||||||
async def analyze(
|
async def analyze(
|
||||||
self, data: TInputData, umo: str | None = None, session_id: str | None = None
|
self, data: TInputData, umo: str | None = None, session_id: str | None = None
|
||||||
) -> tuple[list[TDataObject], TokenUsage]:
|
) -> tuple[list[TDataObject], TokenUsage]:
|
||||||
@@ -402,6 +429,9 @@ class BaseAnalyzer(ABC, Generic[TDataObject, TInputData]):
|
|||||||
|
|
||||||
# Inject tăng cường persona.
|
# Inject tăng cường persona.
|
||||||
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
||||||
|
prompt, system_prompt = self._apply_vietnamese_output_policy(
|
||||||
|
prompt, system_prompt
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[Phân tích {self.get_data_type()}] Bắt đầu yêu cầu LLM, umo: {umo}"
|
f"[Phân tích {self.get_data_type()}] Bắt đầu yêu cầu LLM, umo: {umo}"
|
||||||
|
|||||||
@@ -332,6 +332,9 @@ Chỉ trả về JSON thuần, không chứa Markdown.
|
|||||||
|
|
||||||
# Inject tăng cường persona.
|
# Inject tăng cường persona.
|
||||||
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
||||||
|
prompt, system_prompt = self._apply_vietnamese_output_policy(
|
||||||
|
prompt, system_prompt
|
||||||
|
)
|
||||||
|
|
||||||
response = await call_provider_with_retry(
|
response = await call_provider_with_retry(
|
||||||
self.context,
|
self.context,
|
||||||
@@ -423,6 +426,9 @@ Chỉ trả về JSON thuần, không chứa Markdown.
|
|||||||
|
|
||||||
# Inject tăng cường persona.
|
# Inject tăng cường persona.
|
||||||
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
prompt = self._apply_persona_reinforcement(prompt, system_prompt)
|
||||||
|
prompt, system_prompt = self._apply_vietnamese_output_policy(
|
||||||
|
prompt, system_prompt
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Gọi LLM.
|
# 3. Gọi LLM.
|
||||||
response = await call_provider_with_retry(
|
response = await call_provider_with_retry(
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
|
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
|
||||||
|
|
||||||
|
HAN_CHARACTER_PATTERN = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_vietnamese_text(value: object) -> str:
|
||||||
|
"""Chuẩn hoá và từ chối nội dung sinh còn chứa chữ Hán."""
|
||||||
|
text = str(value).strip()
|
||||||
|
if HAN_CHARACTER_PATTERN.search(text):
|
||||||
|
raise ValueError("Nội dung sinh phải được viết bằng tiếng Việt")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
class TopicItemModel(BaseModel):
|
class TopicItemModel(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
@@ -13,7 +25,7 @@ class TopicItemModel(BaseModel):
|
|||||||
@field_validator("topic", "detail", mode="before")
|
@field_validator("topic", "detail", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_text(cls, value: object) -> str:
|
def _normalize_text(cls, value: object) -> str:
|
||||||
return str(value).strip()
|
return _require_vietnamese_text(value)
|
||||||
|
|
||||||
@field_validator("contributors", mode="before")
|
@field_validator("contributors", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -37,11 +49,16 @@ class UserTitleItemModel(BaseModel):
|
|||||||
mbti: str
|
mbti: str
|
||||||
reason: str
|
reason: str
|
||||||
|
|
||||||
@field_validator("name", "user_id", "title", "mbti", "reason", mode="before")
|
@field_validator("name", "user_id", "mbti", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_text(cls, value: object) -> str:
|
def _normalize_identity(cls, value: object) -> str:
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
@field_validator("title", "reason", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _normalize_generated_text(cls, value: object) -> str:
|
||||||
|
return _require_vietnamese_text(value)
|
||||||
|
|
||||||
|
|
||||||
class GoldenQuoteItemModel(BaseModel):
|
class GoldenQuoteItemModel(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
@@ -50,11 +67,16 @@ class GoldenQuoteItemModel(BaseModel):
|
|||||||
sender: str
|
sender: str
|
||||||
reason: str
|
reason: str
|
||||||
|
|
||||||
@field_validator("content", "sender", "reason", mode="before")
|
@field_validator("sender", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_text(cls, value: object) -> str:
|
def _normalize_identity(cls, value: object) -> str:
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
@field_validator("content", "reason", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _normalize_generated_text(cls, value: object) -> str:
|
||||||
|
return _require_vietnamese_text(value)
|
||||||
|
|
||||||
|
|
||||||
class QualityDimensionModel(BaseModel):
|
class QualityDimensionModel(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
@@ -66,7 +88,7 @@ class QualityDimensionModel(BaseModel):
|
|||||||
@field_validator("name", "comment", mode="before")
|
@field_validator("name", "comment", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_text(cls, value: object) -> str:
|
def _normalize_text(cls, value: object) -> str:
|
||||||
return str(value).strip()
|
return _require_vietnamese_text(value)
|
||||||
|
|
||||||
|
|
||||||
class QualityReviewModel(BaseModel):
|
class QualityReviewModel(BaseModel):
|
||||||
@@ -80,7 +102,7 @@ class QualityReviewModel(BaseModel):
|
|||||||
@field_validator("title", "subtitle", "summary", mode="before")
|
@field_validator("title", "subtitle", "summary", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_text(cls, value: object) -> str:
|
def _normalize_text(cls, value: object) -> str:
|
||||||
return str(value).strip()
|
return _require_vietnamese_text(value)
|
||||||
|
|
||||||
|
|
||||||
def validate_topic_items(
|
def validate_topic_items(
|
||||||
|
|||||||
@@ -25,12 +25,18 @@ def build_topics_schema(max_items: int) -> JSONObject:
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"topic": {"type": "string"},
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tên chủ đề được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
"contributors": {
|
"contributors": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
},
|
},
|
||||||
"detail": {"type": "string"},
|
"detail": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Mô tả chủ đề được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["topic", "contributors", "detail"],
|
"required": ["topic", "contributors", "detail"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
@@ -47,9 +53,15 @@ def build_user_titles_schema(max_items: int) -> JSONObject:
|
|||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"},
|
"name": {"type": "string"},
|
||||||
"user_id": {"type": "string"},
|
"user_id": {"type": "string"},
|
||||||
"title": {"type": "string"},
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Danh hiệu được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
"mbti": {"type": "string"},
|
"mbti": {"type": "string"},
|
||||||
"reason": {"type": "string"},
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Lý do trao danh hiệu được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["name", "user_id", "title", "mbti", "reason"],
|
"required": ["name", "user_id", "title", "mbti", "reason"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
@@ -64,9 +76,15 @@ def build_golden_quotes_schema(max_items: int) -> JSONObject:
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {"type": "string"},
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nội dung trích dẫn bằng tiếng Việt; dịch sang tiếng Việt nếu bản gốc dùng ngôn ngữ khác.",
|
||||||
|
},
|
||||||
"sender": {"type": "string"},
|
"sender": {"type": "string"},
|
||||||
"reason": {"type": "string"},
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Lý do lựa chọn được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["content", "sender", "reason"],
|
"required": ["content", "sender", "reason"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
@@ -78,23 +96,38 @@ def build_chat_quality_schema(max_dimensions: int) -> JSONObject:
|
|||||||
return {
|
return {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {"type": "string"},
|
"title": {
|
||||||
"subtitle": {"type": "string"},
|
"type": "string",
|
||||||
|
"description": "Tiêu đề báo cáo được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Phụ đề báo cáo được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
"dimensions": {
|
"dimensions": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"maxItems": max(1, int(max_dimensions)),
|
"maxItems": max(1, int(max_dimensions)),
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"},
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tên khía cạnh được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
"percentage": {"type": "number"},
|
"percentage": {"type": "number"},
|
||||||
"comment": {"type": "string"},
|
"comment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Nhận xét được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["name", "percentage", "comment"],
|
"required": ["name", "percentage", "comment"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"summary": {"type": "string"},
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Tổng kết được viết bằng tiếng Việt.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["title", "subtitle", "dimensions", "summary"],
|
"required": ["title", "subtitle", "dimensions", "summary"],
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Thiết kế key KV:
|
|||||||
- Timestamp tin nhắn phân tích cuối: incr_last_ts_{group_id}
|
- Timestamp tin nhắn phân tích cuối: incr_last_ts_{group_id}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ...domain.entities.incremental_state import IncrementalBatch
|
from ...domain.entities.incremental_state import IncrementalBatch
|
||||||
@@ -28,6 +29,8 @@ class IncrementalStore:
|
|||||||
INDEX_PREFIX = "incr_batch_index"
|
INDEX_PREFIX = "incr_batch_index"
|
||||||
BATCH_PREFIX = "incr_batch"
|
BATCH_PREFIX = "incr_batch"
|
||||||
LAST_TS_PREFIX = "incr_last_ts"
|
LAST_TS_PREFIX = "incr_last_ts"
|
||||||
|
LANGUAGE_MIGRATION_PREFIX = "incr_language_migration"
|
||||||
|
LANGUAGE_MIGRATION_VERSION = "vi_v1"
|
||||||
|
|
||||||
def __init__(self, star_instance: Any):
|
def __init__(self, star_instance: Any):
|
||||||
"""
|
"""
|
||||||
@@ -37,6 +40,7 @@ class IncrementalStore:
|
|||||||
star_instance: Instance Star dùng để truy cập KV storage.
|
star_instance: Instance Star dùng để truy cập KV storage.
|
||||||
"""
|
"""
|
||||||
self.plugin = star_instance
|
self.plugin = star_instance
|
||||||
|
self._language_migration_locks: dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Xây dựng key
|
# Xây dựng key
|
||||||
@@ -54,6 +58,45 @@ class IncrementalStore:
|
|||||||
"""Tạo key timestamp của tin nhắn phân tích cuối."""
|
"""Tạo key timestamp của tin nhắn phân tích cuối."""
|
||||||
return f"{self.LAST_TS_PREFIX}_{group_id}"
|
return f"{self.LAST_TS_PREFIX}_{group_id}"
|
||||||
|
|
||||||
|
def _language_migration_key(self, group_id: str) -> str:
|
||||||
|
"""Tạo key đánh dấu migration dữ liệu phân tích sang tiếng Việt."""
|
||||||
|
return f"{self.LANGUAGE_MIGRATION_PREFIX}_{group_id}"
|
||||||
|
|
||||||
|
async def _ensure_vietnamese_data_migration(self, group_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Xoá batch cũ một lần để kết quả tiếng Trung không lọt vào báo cáo mới.
|
||||||
|
|
||||||
|
Tin nhắn gốc không bị xoá. Con trỏ được đặt lại để lần phân tích tiếp theo
|
||||||
|
có thể xây dựng lại cửa sổ gia tăng hoàn toàn bằng tiếng Việt.
|
||||||
|
"""
|
||||||
|
marker_key = self._language_migration_key(group_id)
|
||||||
|
marker = await self.plugin.get_kv_data(marker_key, None)
|
||||||
|
if marker == self.LANGUAGE_MIGRATION_VERSION:
|
||||||
|
return
|
||||||
|
|
||||||
|
lock = self._language_migration_locks.setdefault(group_id, asyncio.Lock())
|
||||||
|
async with lock:
|
||||||
|
marker = await self.plugin.get_kv_data(marker_key, None)
|
||||||
|
if marker == self.LANGUAGE_MIGRATION_VERSION:
|
||||||
|
return
|
||||||
|
|
||||||
|
index = await self._get_index(group_id)
|
||||||
|
deleted_count = 0
|
||||||
|
for entry in index:
|
||||||
|
batch_id = str(entry.get("batch_id", "")).strip()
|
||||||
|
if not batch_id:
|
||||||
|
continue
|
||||||
|
await self.plugin.put_kv_data(self._batch_key(group_id, batch_id), None)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
await self._save_index(group_id, [])
|
||||||
|
await self.plugin.put_kv_data(self._last_ts_key(group_id), 0)
|
||||||
|
await self.plugin.put_kv_data(marker_key, self.LANGUAGE_MIGRATION_VERSION)
|
||||||
|
logger.info(
|
||||||
|
f"Đã làm mới dữ liệu gia tăng sang tiếng Việt: nhóm {group_id}, "
|
||||||
|
f"đã xoá {deleted_count} batch cũ và đặt lại con trỏ"
|
||||||
|
)
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Thao tác index batch
|
# Thao tác index batch
|
||||||
# ================================================================
|
# ================================================================
|
||||||
@@ -118,6 +161,7 @@ class IncrementalStore:
|
|||||||
batch_key = self._batch_key(group_id, batch.batch_id)
|
batch_key = self._batch_key(group_id, batch.batch_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
# 1. Lưu dữ liệu batch
|
# 1. Lưu dữ liệu batch
|
||||||
await self.plugin.put_kv_data(batch_key, batch.to_dict())
|
await self.plugin.put_kv_data(batch_key, batch.to_dict())
|
||||||
|
|
||||||
@@ -163,6 +207,7 @@ class IncrementalStore:
|
|||||||
Returns:
|
Returns:
|
||||||
Danh sách batch trong cửa sổ, tăng dần theo timestamp.
|
Danh sách batch trong cửa sổ, tăng dần theo timestamp.
|
||||||
"""
|
"""
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
index = await self._get_index(group_id)
|
index = await self._get_index(group_id)
|
||||||
|
|
||||||
# Lọc batch trong phạm vi cửa sổ.
|
# Lọc batch trong phạm vi cửa sổ.
|
||||||
@@ -223,6 +268,7 @@ class IncrementalStore:
|
|||||||
"""
|
"""
|
||||||
key = self._last_ts_key(group_id)
|
key = self._last_ts_key(group_id)
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
data = await self.plugin.get_kv_data(key, 0)
|
data = await self.plugin.get_kv_data(key, 0)
|
||||||
return int(data) if data else 0
|
return int(data) if data else 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -244,6 +290,7 @@ class IncrementalStore:
|
|||||||
"""
|
"""
|
||||||
key = self._last_ts_key(group_id)
|
key = self._last_ts_key(group_id)
|
||||||
try:
|
try:
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
await self.plugin.put_kv_data(key, timestamp)
|
await self.plugin.put_kv_data(key, timestamp)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Đã cập nhật timestamp phân tích cuối: nhóm {group_id}, ts={timestamp}"
|
f"Đã cập nhật timestamp phân tích cuối: nhóm {group_id}, ts={timestamp}"
|
||||||
@@ -272,6 +319,7 @@ class IncrementalStore:
|
|||||||
Returns:
|
Returns:
|
||||||
Số batch đã xoá.
|
Số batch đã xoá.
|
||||||
"""
|
"""
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
index = await self._get_index(group_id)
|
index = await self._get_index(group_id)
|
||||||
if not index:
|
if not index:
|
||||||
return 0
|
return 0
|
||||||
@@ -328,6 +376,7 @@ class IncrementalStore:
|
|||||||
Returns:
|
Returns:
|
||||||
Tổng số batch.
|
Tổng số batch.
|
||||||
"""
|
"""
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
index = await self._get_index(group_id)
|
index = await self._get_index(group_id)
|
||||||
return len(index)
|
return len(index)
|
||||||
|
|
||||||
@@ -343,6 +392,7 @@ class IncrementalStore:
|
|||||||
Returns:
|
Returns:
|
||||||
Danh sách tóm tắt batch tăng dần theo thời gian.
|
Danh sách tóm tắt batch tăng dần theo thời gian.
|
||||||
"""
|
"""
|
||||||
|
await self._ensure_vietnamese_data_migration(group_id)
|
||||||
index = await self._get_index(group_id)
|
index = await self._get_index(group_id)
|
||||||
# Sắp xếp tăng dần theo timestamp.
|
# Sắp xếp tăng dần theo timestamp.
|
||||||
index.sort(key=lambda x: x.get("timestamp", 0))
|
index.sort(key=lambda x: x.get("timestamp", 0))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="vi">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=800, initial-scale=1">
|
<meta name="viewport" content="width=800, initial-scale=1">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% import "inline_assets.html" as inline_assets %}
|
{% import "inline_assets.html" as inline_assets %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% import "inline_assets.html" as inline_assets %}
|
{% import "inline_assets.html" as inline_assets %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ 'zh-CN' if t2i_font_source == 'Mainland' else 'zh-Hant' }}">
|
<html lang="vi">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ if "astrbot.api" not in sys.modules:
|
|||||||
astrbot_api_module = types.ModuleType("astrbot.api")
|
astrbot_api_module = types.ModuleType("astrbot.api")
|
||||||
astrbot_event_module = types.ModuleType("astrbot.api.event")
|
astrbot_event_module = types.ModuleType("astrbot.api.event")
|
||||||
astrbot_star_module = types.ModuleType("astrbot.api.star")
|
astrbot_star_module = types.ModuleType("astrbot.api.star")
|
||||||
|
astrbot_provider_module = types.ModuleType("astrbot.api.provider")
|
||||||
|
|
||||||
class AstrMessageEvent:
|
class AstrMessageEvent:
|
||||||
pass
|
pass
|
||||||
@@ -15,11 +16,29 @@ if "astrbot.api" not in sys.modules:
|
|||||||
class Context:
|
class Context:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class AstrBotConfig(dict):
|
||||||
|
def save_config(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class StarTools:
|
||||||
|
@staticmethod
|
||||||
|
def get_data_dir(plugin_name):
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
return Path("data") / plugin_name
|
||||||
|
|
||||||
|
class LLMResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
astrbot_api_module.logger = logging.getLogger("astrbot-test")
|
astrbot_api_module.logger = logging.getLogger("astrbot-test")
|
||||||
|
astrbot_api_module.AstrBotConfig = AstrBotConfig
|
||||||
astrbot_event_module.AstrMessageEvent = AstrMessageEvent
|
astrbot_event_module.AstrMessageEvent = AstrMessageEvent
|
||||||
astrbot_star_module.Context = Context
|
astrbot_star_module.Context = Context
|
||||||
|
astrbot_star_module.StarTools = StarTools
|
||||||
|
astrbot_provider_module.LLMResponse = LLMResponse
|
||||||
astrbot_module.api = astrbot_api_module
|
astrbot_module.api = astrbot_api_module
|
||||||
sys.modules.setdefault("astrbot", astrbot_module)
|
sys.modules.setdefault("astrbot", astrbot_module)
|
||||||
sys.modules.setdefault("astrbot.api", astrbot_api_module)
|
sys.modules.setdefault("astrbot.api", astrbot_api_module)
|
||||||
sys.modules.setdefault("astrbot.api.event", astrbot_event_module)
|
sys.modules.setdefault("astrbot.api.event", astrbot_event_module)
|
||||||
sys.modules.setdefault("astrbot.api.star", astrbot_star_module)
|
sys.modules.setdefault("astrbot.api.star", astrbot_star_module)
|
||||||
|
sys.modules.setdefault("astrbot.api.provider", astrbot_provider_module)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.domain.entities.incremental_state import IncrementalBatch
|
||||||
|
from src.infrastructure.persistence.incremental_store import IncrementalStore
|
||||||
|
|
||||||
|
|
||||||
|
class FakePlugin:
|
||||||
|
def __init__(self, data: dict | None = None):
|
||||||
|
self.data = dict(data or {})
|
||||||
|
|
||||||
|
async def get_kv_data(self, key, default=None):
|
||||||
|
return self.data.get(key, default)
|
||||||
|
|
||||||
|
async def put_kv_data(self, key, value):
|
||||||
|
if value is None:
|
||||||
|
self.data.pop(key, None)
|
||||||
|
else:
|
||||||
|
self.data[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_first_access_purges_legacy_batches_and_resets_cursor():
|
||||||
|
group_id = "group-1"
|
||||||
|
old_batch_id = "old-batch"
|
||||||
|
plugin = FakePlugin(
|
||||||
|
{
|
||||||
|
f"incr_batch_index_{group_id}": [
|
||||||
|
{"batch_id": old_batch_id, "timestamp": time.time()}
|
||||||
|
],
|
||||||
|
f"incr_batch_{group_id}_{old_batch_id}": {
|
||||||
|
"group_id": group_id,
|
||||||
|
"batch_id": old_batch_id,
|
||||||
|
"topics": [{"topic": "旧话题"}],
|
||||||
|
},
|
||||||
|
f"incr_last_ts_{group_id}": 123456,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
store = IncrementalStore(plugin)
|
||||||
|
|
||||||
|
batches = await store.query_batches(group_id, 0, time.time() + 1)
|
||||||
|
|
||||||
|
assert batches == []
|
||||||
|
assert plugin.data[f"incr_batch_index_{group_id}"] == []
|
||||||
|
assert f"incr_batch_{group_id}_{old_batch_id}" not in plugin.data
|
||||||
|
assert plugin.data[f"incr_last_ts_{group_id}"] == 0
|
||||||
|
assert (
|
||||||
|
plugin.data[f"incr_language_migration_{group_id}"]
|
||||||
|
== IncrementalStore.LANGUAGE_MIGRATION_VERSION
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_migration_is_idempotent_and_preserves_new_vietnamese_batch():
|
||||||
|
group_id = "group-2"
|
||||||
|
plugin = FakePlugin()
|
||||||
|
store = IncrementalStore(plugin)
|
||||||
|
batch = IncrementalBatch(
|
||||||
|
group_id=group_id,
|
||||||
|
timestamp=time.time(),
|
||||||
|
topics=[
|
||||||
|
{
|
||||||
|
"topic": "Kế hoạch cuối tuần",
|
||||||
|
"contributors": ["An"],
|
||||||
|
"detail": "Mọi người thống nhất đi dã ngoại.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await store.save_batch(batch)
|
||||||
|
await store.update_last_analyzed_timestamp(group_id, 999)
|
||||||
|
loaded_once = await store.query_batches(group_id, 0, time.time() + 1)
|
||||||
|
loaded_twice = await store.query_batches(group_id, 0, time.time() + 1)
|
||||||
|
|
||||||
|
assert [item.batch_id for item in loaded_once] == [batch.batch_id]
|
||||||
|
assert [item.batch_id for item in loaded_twice] == [batch.batch_id]
|
||||||
|
assert await store.get_last_analyzed_timestamp(group_id) == 999
|
||||||
|
assert plugin.data[f"incr_batch_index_{group_id}"] == [
|
||||||
|
{"batch_id": batch.batch_id, "timestamp": batch.timestamp}
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_report_documents_declare_vietnamese_language():
|
||||||
|
reporting_dir = (
|
||||||
|
Path(__file__).resolve().parents[1] / "src" / "infrastructure" / "reporting"
|
||||||
|
)
|
||||||
|
report_documents = list(reporting_dir.glob("templates/*/html_template.html"))
|
||||||
|
report_documents.extend(reporting_dir.glob("templates/*/image_template.html"))
|
||||||
|
report_documents.append(
|
||||||
|
reporting_dir / "platform_templates" / "qq_official" / "summary_dashboard.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report_documents
|
||||||
|
for document in report_documents:
|
||||||
|
html = document.read_text(encoding="utf-8")
|
||||||
|
assert '<html lang="vi">' in html, document
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.infrastructure.analysis.analyzers.base_analyzer import (
|
||||||
|
BaseAnalyzer,
|
||||||
|
VIETNAMESE_OUTPUT_POLICY,
|
||||||
|
)
|
||||||
|
from src.infrastructure.analysis.utils.response_validation import (
|
||||||
|
validate_golden_quote_items,
|
||||||
|
validate_quality_review_item,
|
||||||
|
validate_topic_items,
|
||||||
|
validate_user_title_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyAnalyzer(BaseAnalyzer[dict, list[dict]]):
|
||||||
|
def get_data_type(self) -> str:
|
||||||
|
return "Kiểm thử"
|
||||||
|
|
||||||
|
def get_max_count(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def build_prompt(self, data: list[dict]) -> str:
|
||||||
|
return "请使用中文回答"
|
||||||
|
|
||||||
|
def extract_with_regex(self, result_text: str, max_count: int) -> list[dict]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_data_objects(self, data_list: list[dict]) -> list[dict]:
|
||||||
|
return data_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_vietnamese_policy_overrides_chinese_prompt_and_persona():
|
||||||
|
analyzer = DummyAnalyzer(context=None, config_manager=None)
|
||||||
|
prompt = analyzer._apply_persona_reinforcement(
|
||||||
|
"请使用中文回答", "你是一个中文助手,只能使用中文。"
|
||||||
|
)
|
||||||
|
final_prompt, final_system_prompt = analyzer._apply_vietnamese_output_policy(
|
||||||
|
prompt, "你是一个中文助手,只能使用中文。"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert final_prompt.endswith(VIETNAMESE_OUTPUT_POLICY)
|
||||||
|
assert final_system_prompt.endswith(VIETNAMESE_OUTPUT_POLICY)
|
||||||
|
assert "mức ưu tiên cao hơn" in final_prompt
|
||||||
|
assert "Persona chỉ quyết định phong cách" in final_system_prompt
|
||||||
|
assert "dịch phát biểu sang tiếng Việt" in final_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_vietnamese_policy_is_also_used_without_persona():
|
||||||
|
analyzer = DummyAnalyzer(context=None, config_manager=None)
|
||||||
|
|
||||||
|
final_prompt, final_system_prompt = analyzer._apply_vietnamese_output_policy(
|
||||||
|
"Phân tích dữ liệu", None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert final_prompt.endswith(VIETNAMESE_OUTPUT_POLICY)
|
||||||
|
assert final_system_prompt == VIETNAMESE_OUTPUT_POLICY
|
||||||
|
|
||||||
|
|
||||||
|
def test_vietnamese_semantic_payloads_are_valid():
|
||||||
|
topic_ok, _, _ = validate_topic_items(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"topic": "Kế hoạch cuối tuần",
|
||||||
|
"contributors": ["123"],
|
||||||
|
"detail": "Các thành viên thống nhất đi dã ngoại vào sáng Chủ nhật.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
title_ok, _, _ = validate_user_title_items(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "测试用户",
|
||||||
|
"user_id": "123",
|
||||||
|
"title": "Người kết nối",
|
||||||
|
"mbti": "ENFJ",
|
||||||
|
"reason": "Luôn chủ động trả lời và kết nối mọi người.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
quote_ok, _, _ = validate_golden_quote_items(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": "Chạy được đã là một loại thành công.",
|
||||||
|
"sender": "[123]",
|
||||||
|
"reason": "Câu nói hài hước nhưng rất đúng với tình huống.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
quality_ok, _, _ = validate_quality_review_item(
|
||||||
|
{
|
||||||
|
"title": "Một ngày nhiều ý tưởng",
|
||||||
|
"subtitle": "Từ chuyện vui đến kế hoạch mới",
|
||||||
|
"dimensions": [
|
||||||
|
{
|
||||||
|
"name": "Chia sẻ kiến thức",
|
||||||
|
"percentage": 60,
|
||||||
|
"comment": "Mọi người hỗ trợ nhau rất nhiệt tình.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Không khí tích cực và có nhiều nội dung hữu ích.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert topic_ok
|
||||||
|
assert title_ok
|
||||||
|
assert quote_ok
|
||||||
|
assert quality_ok
|
||||||
|
|
||||||
|
|
||||||
|
def test_chinese_generated_fields_are_rejected_but_identity_is_preserved():
|
||||||
|
topic_ok, _, topic_error = validate_topic_items(
|
||||||
|
[{"topic": "技术讨论", "contributors": ["测试用户"], "detail": "讨论很深入"}]
|
||||||
|
)
|
||||||
|
title_ok, _, title_error = validate_user_title_items(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "测试用户",
|
||||||
|
"user_id": "123",
|
||||||
|
"title": "技术专家",
|
||||||
|
"mbti": "INTJ",
|
||||||
|
"reason": "经常帮助大家",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
quote_ok, _, quote_error = validate_golden_quote_items(
|
||||||
|
[{"content": "代码能跑就不要动", "sender": "[123]", "reason": "很幽默"}]
|
||||||
|
)
|
||||||
|
quality_ok, _, quality_error = validate_quality_review_item(
|
||||||
|
{
|
||||||
|
"title": "今日群聊",
|
||||||
|
"subtitle": "副标题",
|
||||||
|
"dimensions": [
|
||||||
|
{"name": "技术交流", "percentage": 100, "comment": "讨论热烈"}
|
||||||
|
],
|
||||||
|
"summary": "大家都很活跃",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not topic_ok and "tiếng Việt" in (topic_error or "")
|
||||||
|
assert not title_ok and "tiếng Việt" in (title_error or "")
|
||||||
|
assert not quote_ok and "tiếng Việt" in (quote_error or "")
|
||||||
|
assert not quality_ok and "tiếng Việt" in (quality_error or "")
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_golden_quote_prompt_requires_translation():
|
||||||
|
schema_path = Path(__file__).resolve().parents[1] / "_conf_schema.json"
|
||||||
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||||
|
prompt = schema["prompts"]["items"]["golden_quote_analysis_prompts"]["items"][
|
||||||
|
"golden_quote_v2_prompt"
|
||||||
|
]["default"]
|
||||||
|
|
||||||
|
assert "dịch phát biểu sang tiếng Việt" in prompt
|
||||||
|
assert "content` phải giữ nguyên lời nói gốc" not in prompt
|
||||||
Reference in New Issue
Block a user