From ff35538a01ad170eba505d7864af58311454304d Mon Sep 17 00:00:00 2001 From: MerCry Date: Fri, 27 Feb 2026 14:20:31 +0800 Subject: [PATCH] feat(ai-service): implement intent recognition and rule engine (Phase 12 T12.1-T12.5) [AC-AISVC-65~AC-AISVC-70] Intent recognition with keyword and regex matching - Add IntentRule SQLModel entity with tenant isolation - Implement IntentRuleService for CRUD operations with hit statistics - Implement IntentRouter matching engine (priority DESC, keyword then regex) - Add rule caching by tenant_id with TTL=60s and CRUD invalidation - Add intent rules management API (POST/GET/PUT/DELETE /admin/intent-rules) - Support four response types: fixed/rag/flow/transfer T12.6 (Orchestrator integration) and T12.7 (unit tests) pending for integration phase --- ai-service/app/api/admin/intent_rules.py | 166 ++++++++++ ai-service/app/services/intent/__init__.py | 13 + ai-service/app/services/intent/router.py | 170 ++++++++++ .../app/services/intent/rule_service.py | 298 ++++++++++++++++++ docs/progress/ai-service-progress.md | 41 ++- spec/ai-service/tasks.md | 14 +- 6 files changed, 684 insertions(+), 18 deletions(-) create mode 100644 ai-service/app/api/admin/intent_rules.py create mode 100644 ai-service/app/services/intent/__init__.py create mode 100644 ai-service/app/services/intent/router.py create mode 100644 ai-service/app/services/intent/rule_service.py diff --git a/ai-service/app/api/admin/intent_rules.py b/ai-service/app/api/admin/intent_rules.py new file mode 100644 index 0000000..0d68b9f --- /dev/null +++ b/ai-service/app/api/admin/intent_rules.py @@ -0,0 +1,166 @@ +""" +Intent Rule Management API. +[AC-AISVC-65~AC-AISVC-68] Intent rule CRUD endpoints. +""" + +import logging +import uuid +from typing import Any + +from fastapi import APIRouter, Depends, Header, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_session +from app.models.entities import IntentRuleCreate, IntentRuleUpdate +from app.services.intent.rule_service import IntentRuleService + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/admin/intent-rules", tags=["Intent Rules"]) + + +def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str: + """Extract tenant ID from header.""" + if not x_tenant_id: + raise HTTPException(status_code=400, detail="X-Tenant-Id header is required") + return x_tenant_id + + +@router.get("") +async def list_rules( + tenant_id: str = Depends(get_tenant_id), + response_type: str | None = None, + is_enabled: bool | None = None, + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """ + [AC-AISVC-66] List all intent rules for a tenant. + """ + logger.info( + f"[AC-AISVC-66] Listing intent rules for tenant={tenant_id}, " + f"response_type={response_type}, is_enabled={is_enabled}" + ) + + service = IntentRuleService(session) + rules = await service.list_rules(tenant_id, response_type, is_enabled) + + data = [] + for rule in rules: + data.append(await service.rule_to_info_dict(rule)) + + return {"data": data} + + +@router.post("", status_code=201) +async def create_rule( + body: IntentRuleCreate, + tenant_id: str = Depends(get_tenant_id), + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """ + [AC-AISVC-65] Create a new intent rule. + """ + valid_response_types = ["fixed", "rag", "flow", "transfer"] + if body.response_type not in valid_response_types: + raise HTTPException( + status_code=400, + detail=f"Invalid response_type. Must be one of: {valid_response_types}" + ) + + if body.response_type == "rag" and not body.target_kb_ids: + logger.warning( + f"[AC-AISVC-65] Creating rag rule without target_kb_ids: tenant={tenant_id}" + ) + + if body.response_type == "flow" and not body.flow_id: + raise HTTPException( + status_code=400, + detail="flow_id is required when response_type is 'flow'" + ) + + if body.response_type == "fixed" and not body.fixed_reply: + raise HTTPException( + status_code=400, + detail="fixed_reply is required when response_type is 'fixed'" + ) + + if body.response_type == "transfer" and not body.transfer_message: + raise HTTPException( + status_code=400, + detail="transfer_message is required when response_type is 'transfer'" + ) + + logger.info( + f"[AC-AISVC-65] Creating intent rule for tenant={tenant_id}, name={body.name}" + ) + + service = IntentRuleService(session) + rule = await service.create_rule(tenant_id, body) + + return await service.rule_to_info_dict(rule) + + +@router.get("/{rule_id}") +async def get_rule( + rule_id: uuid.UUID, + tenant_id: str = Depends(get_tenant_id), + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """ + [AC-AISVC-66] Get intent rule detail. + """ + logger.info(f"[AC-AISVC-66] Getting intent rule for tenant={tenant_id}, id={rule_id}") + + service = IntentRuleService(session) + rule = await service.get_rule(tenant_id, rule_id) + + if not rule: + raise HTTPException(status_code=404, detail="Intent rule not found") + + return await service.rule_to_info_dict(rule) + + +@router.put("/{rule_id}") +async def update_rule( + rule_id: uuid.UUID, + body: IntentRuleUpdate, + tenant_id: str = Depends(get_tenant_id), + session: AsyncSession = Depends(get_session), +) -> dict[str, Any]: + """ + [AC-AISVC-67] Update an intent rule. + """ + valid_response_types = ["fixed", "rag", "flow", "transfer"] + if body.response_type is not None and body.response_type not in valid_response_types: + raise HTTPException( + status_code=400, + detail=f"Invalid response_type. Must be one of: {valid_response_types}" + ) + + logger.info(f"[AC-AISVC-67] Updating intent rule for tenant={tenant_id}, id={rule_id}") + + service = IntentRuleService(session) + rule = await service.update_rule(tenant_id, rule_id, body) + + if not rule: + raise HTTPException(status_code=404, detail="Intent rule not found") + + return await service.rule_to_info_dict(rule) + + +@router.delete("/{rule_id}", status_code=204) +async def delete_rule( + rule_id: uuid.UUID, + tenant_id: str = Depends(get_tenant_id), + session: AsyncSession = Depends(get_session), +) -> None: + """ + [AC-AISVC-68] Delete an intent rule. + """ + logger.info(f"[AC-AISVC-68] Deleting intent rule for tenant={tenant_id}, id={rule_id}") + + service = IntentRuleService(session) + success = await service.delete_rule(tenant_id, rule_id) + + if not success: + raise HTTPException(status_code=404, detail="Intent rule not found") diff --git a/ai-service/app/services/intent/__init__.py b/ai-service/app/services/intent/__init__.py new file mode 100644 index 0000000..151c895 --- /dev/null +++ b/ai-service/app/services/intent/__init__.py @@ -0,0 +1,13 @@ +""" +Intent recognition and routing services. +[AC-AISVC-65~AC-AISVC-70] Intent rule management and matching engine. +""" + +from app.services.intent.rule_service import IntentRuleService +from app.services.intent.router import IntentRouter, IntentMatchResult + +__all__ = [ + "IntentRuleService", + "IntentRouter", + "IntentMatchResult", +] diff --git a/ai-service/app/services/intent/router.py b/ai-service/app/services/intent/router.py new file mode 100644 index 0000000..218a560 --- /dev/null +++ b/ai-service/app/services/intent/router.py @@ -0,0 +1,170 @@ +""" +Intent router for AI Service. +[AC-AISVC-69, AC-AISVC-70] Intent matching engine with keyword and regex support. +""" + +import logging +import re +from dataclasses import dataclass +from typing import Any + +from app.models.entities import IntentRule + +logger = logging.getLogger(__name__) + + +@dataclass +class IntentMatchResult: + """ + [AC-AISVC-69] Result of intent matching. + Contains the matched rule and match details. + """ + + rule: IntentRule + match_type: str + matched: str + + def to_dict(self) -> dict[str, Any]: + return { + "rule_id": str(self.rule.id), + "rule_name": self.rule.name, + "match_type": self.match_type, + "matched": self.matched, + "response_type": self.rule.response_type, + "target_kb_ids": self.rule.target_kb_ids or [], + "flow_id": str(self.rule.flow_id) if self.rule.flow_id else None, + "fixed_reply": self.rule.fixed_reply, + "transfer_message": self.rule.transfer_message, + } + + +class IntentRouter: + """ + [AC-AISVC-69] Intent matching engine. + + Matching algorithm: + 1. Load rules ordered by priority DESC + 2. For each rule, try keyword matching first + 3. If no keyword match, try regex pattern matching + 4. Return first match (highest priority) + 5. If no match, return None (fallback to default RAG) + """ + + def __init__(self): + pass + + def match( + self, + message: str, + rules: list[IntentRule], + ) -> IntentMatchResult | None: + """ + [AC-AISVC-69] Match user message against intent rules. + + Args: + message: User input message + rules: List of enabled rules ordered by priority DESC + + Returns: + IntentMatchResult if matched, None otherwise + """ + if not message or not rules: + return None + + message_lower = message.lower() + + for rule in rules: + if not rule.is_enabled: + continue + + keyword_result = self._match_keywords(message, message_lower, rule) + if keyword_result: + logger.info( + f"[AC-AISVC-69] Intent matched by keyword: " + f"rule={rule.name}, matched='{keyword_result.matched}'" + ) + return keyword_result + + regex_result = self._match_patterns(message, rule) + if regex_result: + logger.info( + f"[AC-AISVC-69] Intent matched by regex: " + f"rule={rule.name}, matched='{regex_result.matched}'" + ) + return regex_result + + logger.debug(f"[AC-AISVC-70] No intent matched, will fallback to default RAG") + return None + + def _match_keywords( + self, + message: str, + message_lower: str, + rule: IntentRule, + ) -> IntentMatchResult | None: + """ + Match message against rule keywords. + Any keyword match returns a result. + """ + keywords = rule.keywords or [] + if not keywords: + return None + + for keyword in keywords: + if not keyword: + continue + if keyword.lower() in message_lower: + return IntentMatchResult( + rule=rule, + match_type="keyword", + matched=keyword, + ) + + return None + + def _match_patterns( + self, + message: str, + rule: IntentRule, + ) -> IntentMatchResult | None: + """ + Match message against rule regex patterns. + Any pattern match returns a result. + """ + patterns = rule.patterns or [] + if not patterns: + return None + + for pattern in patterns: + if not pattern: + continue + try: + if re.search(pattern, message, re.IGNORECASE): + return IntentMatchResult( + rule=rule, + match_type="regex", + matched=pattern, + ) + except re.error as e: + logger.warning( + f"Invalid regex pattern in rule {rule.id}: {pattern}, error: {e}" + ) + continue + + return None + + def match_with_stats( + self, + message: str, + rules: list[IntentRule], + ) -> tuple[IntentMatchResult | None, str | None]: + """ + [AC-AISVC-69] Match and return rule_id for statistics update. + + Returns: + Tuple of (match_result, rule_id_for_stats) + """ + result = self.match(message, rules) + if result: + return result, str(result.rule.id) + return None, None diff --git a/ai-service/app/services/intent/rule_service.py b/ai-service/app/services/intent/rule_service.py new file mode 100644 index 0000000..179adca --- /dev/null +++ b/ai-service/app/services/intent/rule_service.py @@ -0,0 +1,298 @@ +""" +Intent rule service for AI Service. +[AC-AISVC-65~AC-AISVC-68] Intent rule CRUD and hit statistics management. +""" + +import logging +import time +import uuid +from datetime import datetime +from typing import Any, Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import col + +from app.models.entities import ( + IntentRule, + IntentRuleCreate, + IntentRuleUpdate, +) + +logger = logging.getLogger(__name__) + +RULE_CACHE_TTL_SECONDS = 60 + + +class RuleCache: + """ + [AC-AISVC-69] In-memory cache for intent rules. + Key: tenant_id + Value: (rules_list, cached_at) + TTL: 60 seconds + """ + + def __init__(self, ttl_seconds: int = RULE_CACHE_TTL_SECONDS): + self._cache: dict[str, tuple[list[IntentRule], float]] = {} + self._ttl = ttl_seconds + + def get(self, tenant_id: str) -> list[IntentRule] | None: + """Get cached rules if not expired.""" + if tenant_id in self._cache: + rules, cached_at = self._cache[tenant_id] + if time.time() - cached_at < self._ttl: + return rules + else: + del self._cache[tenant_id] + return None + + def set(self, tenant_id: str, rules: list[IntentRule]) -> None: + """Cache rules for a tenant.""" + self._cache[tenant_id] = (rules, time.time()) + + def invalidate(self, tenant_id: str) -> None: + """Invalidate cache for a tenant.""" + if tenant_id in self._cache: + del self._cache[tenant_id] + logger.debug(f"Invalidated rule cache for tenant={tenant_id}") + + +_rule_cache = RuleCache() + + +class IntentRuleService: + """ + [AC-AISVC-65~AC-AISVC-68] Service for managing intent rules. + + Features: + - Rule CRUD with tenant isolation + - Hit count statistics + - In-memory caching with TTL + - Cache invalidation on CRUD operations + """ + + def __init__(self, session: AsyncSession): + self._session = session + self._cache = _rule_cache + + async def create_rule( + self, + tenant_id: str, + create_data: IntentRuleCreate, + ) -> IntentRule: + """ + [AC-AISVC-65] Create a new intent rule. + """ + flow_id_uuid = None + if create_data.flow_id: + try: + flow_id_uuid = uuid.UUID(create_data.flow_id) + except ValueError: + pass + + rule = IntentRule( + tenant_id=tenant_id, + name=create_data.name, + keywords=create_data.keywords or [], + patterns=create_data.patterns or [], + priority=create_data.priority, + response_type=create_data.response_type, + target_kb_ids=create_data.target_kb_ids or [], + flow_id=flow_id_uuid, + fixed_reply=create_data.fixed_reply, + transfer_message=create_data.transfer_message, + is_enabled=True, + hit_count=0, + ) + self._session.add(rule) + await self._session.flush() + + self._cache.invalidate(tenant_id) + + logger.info( + f"[AC-AISVC-65] Created intent rule: tenant={tenant_id}, " + f"id={rule.id}, name={rule.name}, response_type={rule.response_type}" + ) + return rule + + async def list_rules( + self, + tenant_id: str, + response_type: str | None = None, + is_enabled: bool | None = None, + ) -> Sequence[IntentRule]: + """ + [AC-AISVC-66] List rules for a tenant with optional filters. + """ + stmt = select(IntentRule).where( + IntentRule.tenant_id == tenant_id + ) + + if response_type is not None: + stmt = stmt.where(IntentRule.response_type == response_type) + + if is_enabled is not None: + stmt = stmt.where(IntentRule.is_enabled == is_enabled) + + stmt = stmt.order_by(col(IntentRule.priority).desc(), col(IntentRule.created_at).desc()) + result = await self._session.execute(stmt) + return result.scalars().all() + + async def get_rule( + self, + tenant_id: str, + rule_id: uuid.UUID, + ) -> IntentRule | None: + """ + [AC-AISVC-66] Get rule by ID with tenant isolation. + """ + stmt = select(IntentRule).where( + IntentRule.tenant_id == tenant_id, + IntentRule.id == rule_id, + ) + result = await self._session.execute(stmt) + return result.scalar_one_or_none() + + async def update_rule( + self, + tenant_id: str, + rule_id: uuid.UUID, + update_data: IntentRuleUpdate, + ) -> IntentRule | None: + """ + [AC-AISVC-67] Update an intent rule. + """ + rule = await self.get_rule(tenant_id, rule_id) + if not rule: + return None + + if update_data.name is not None: + rule.name = update_data.name + if update_data.keywords is not None: + rule.keywords = update_data.keywords + if update_data.patterns is not None: + rule.patterns = update_data.patterns + if update_data.priority is not None: + rule.priority = update_data.priority + if update_data.response_type is not None: + rule.response_type = update_data.response_type + if update_data.target_kb_ids is not None: + rule.target_kb_ids = update_data.target_kb_ids + if update_data.flow_id is not None: + try: + rule.flow_id = uuid.UUID(update_data.flow_id) + except ValueError: + rule.flow_id = None + if update_data.fixed_reply is not None: + rule.fixed_reply = update_data.fixed_reply + if update_data.transfer_message is not None: + rule.transfer_message = update_data.transfer_message + if update_data.is_enabled is not None: + rule.is_enabled = update_data.is_enabled + + rule.updated_at = datetime.utcnow() + await self._session.flush() + + self._cache.invalidate(tenant_id) + + logger.info( + f"[AC-AISVC-67] Updated intent rule: tenant={tenant_id}, id={rule_id}" + ) + return rule + + async def delete_rule( + self, + tenant_id: str, + rule_id: uuid.UUID, + ) -> bool: + """ + [AC-AISVC-68] Delete an intent rule. + """ + rule = await self.get_rule(tenant_id, rule_id) + if not rule: + return False + + await self._session.delete(rule) + await self._session.flush() + + self._cache.invalidate(tenant_id) + + logger.info( + f"[AC-AISVC-68] Deleted intent rule: tenant={tenant_id}, id={rule_id}" + ) + return True + + async def increment_hit_count( + self, + tenant_id: str, + rule_id: uuid.UUID, + ) -> bool: + """ + [AC-AISVC-66] Increment hit count for a rule. + """ + rule = await self.get_rule(tenant_id, rule_id) + if not rule: + return False + + rule.hit_count += 1 + rule.updated_at = datetime.utcnow() + await self._session.flush() + + logger.debug( + f"[AC-AISVC-66] Incremented hit count for rule: tenant={tenant_id}, " + f"id={rule_id}, hit_count={rule.hit_count}" + ) + return True + + async def get_enabled_rules_for_matching( + self, + tenant_id: str, + ) -> list[IntentRule]: + """ + [AC-AISVC-69] Get enabled rules for matching, ordered by priority DESC. + Uses cache for performance. + """ + cached = self._cache.get(tenant_id) + if cached is not None: + logger.debug(f"[AC-AISVC-69] Cache hit for rules: tenant={tenant_id}") + return cached + + stmt = ( + select(IntentRule) + .where( + IntentRule.tenant_id == tenant_id, + IntentRule.is_enabled == True, + ) + .order_by(col(IntentRule.priority).desc()) + ) + result = await self._session.execute(stmt) + rules = list(result.scalars().all()) + + self._cache.set(tenant_id, rules) + logger.info( + f"[AC-AISVC-69] Loaded {len(rules)} enabled rules from DB: tenant={tenant_id}" + ) + return rules + + def invalidate_cache(self, tenant_id: str) -> None: + """Manually invalidate cache for a tenant.""" + self._cache.invalidate(tenant_id) + + async def rule_to_info_dict(self, rule: IntentRule) -> dict[str, Any]: + """Convert rule entity to API response dict.""" + return { + "id": str(rule.id), + "name": rule.name, + "keywords": rule.keywords or [], + "patterns": rule.patterns or [], + "priority": rule.priority, + "response_type": rule.response_type, + "target_kb_ids": rule.target_kb_ids or [], + "flow_id": str(rule.flow_id) if rule.flow_id else None, + "fixed_reply": rule.fixed_reply, + "transfer_message": rule.transfer_message, + "is_enabled": rule.is_enabled, + "hit_count": rule.hit_count, + "created_at": rule.created_at.isoformat(), + "updated_at": rule.updated_at.isoformat(), + } diff --git a/docs/progress/ai-service-progress.md b/docs/progress/ai-service-progress.md index 3b8b93d..b099465 100644 --- a/docs/progress/ai-service-progress.md +++ b/docs/progress/ai-service-progress.md @@ -33,7 +33,7 @@ - [x] Phase 8: LLM 配置与 RAG 调试输出 (100%) ✅ - [x] Phase 9: 租户管理与 RAG 优化 (100%) ✅ - [x] Phase 10: Prompt 模板化 (80%) 🔄 (T10.1-T10.8 完成,T10.9-T10.10 待集成阶段) -- [ ] Phase 11: 多知识库管理 (0%) ⏳ +- [x] Phase 11: 多知识库管理 (63%) 🔄 (T11.1-T11.5 完成,T11.6-T11.8 待集成阶段) - [x] Phase 12: 意图识别与规则引擎 (71%) 🔄 (T12.1-T12.5 完成,T12.6-T12.7 待集成阶段) --- @@ -41,20 +41,21 @@ ## 🔄 Current Phase ### Goal -Phase 12 意图识别与规则引擎核心功能已完成,T12.6(Orchestrator 集成)和 T12.7(单元测试)留待集成阶段。 +Phase 11 多知识库管理核心功能已完成 (T11.1-T11.5),T11.6(OptimizedRetriever 多 Collection 检索)、T11.7(kb_default 迁移)、T11.8(单元测试)留待集成阶段。 -### Completed Tasks (Phase 12) +### Completed Tasks (Phase 11) -- [x] T12.1 定义 `IntentRule` SQLModel 实体,创建数据库表 `[AC-AISVC-65]` ✅ -- [x] T12.2 实现 `IntentRuleService`:规则 CRUD + 命中统计更新 `[AC-AISVC-65~AC-AISVC-68]` ✅ -- [x] T12.3 实现 `IntentRouter`:按优先级遍历规则,关键词+正则匹配 `[AC-AISVC-69]` ✅ -- [x] T12.4 实现规则缓存:按 tenant_id 缓存规则列表,CRUD 操作时主动失效 `[AC-AISVC-69]` ✅ -- [x] T12.5 实现意图规则管理 API:`POST/GET/PUT/DELETE /admin/intent-rules` `[AC-AISVC-65~AC-AISVC-68]` ✅ +- [x] T11.1 扩展 `KnowledgeBase` 实体:新增 `kb_type`、`priority`、`is_enabled`、`doc_count` 字段 `[AC-AISVC-59]` ✅ +- [x] T11.2 实现知识库 CRUD 服务:创建时初始化 Qdrant Collection,删除时清理 Collection `[AC-AISVC-59, AC-AISVC-61, AC-AISVC-62]` ✅ +- [x] T11.3 实现知识库管理 API:`POST/GET/PUT/DELETE /admin/kb/knowledge-bases` `[AC-AISVC-59~AC-AISVC-62]` ✅ +- [x] T11.4 升级 Qdrant Collection 命名:`kb_{tenant_id}_{kb_id}`,兼容现有 `kb_{tenant_id}` `[AC-AISVC-63]` ✅ +- [x] T11.5 修改文档上传流程:支持指定 `kbId` 参数,索引到对应 Collection `[AC-AISVC-63]` ✅ -### Pending Tasks (Phase 12 - 集成阶段) +### Pending Tasks (Phase 11 - 集成阶段) -- [ ] T12.6 在 Orchestrator 中集成 IntentRouter:RAG 检索前执行意图识别,按 response_type 路由 `[AC-AISVC-69, AC-AISVC-70]` -- [ ] T12.7 编写意图识别服务单元测试 `[AC-AISVC-65~AC-AISVC-70]` +- [ ] T11.6 修改 `OptimizedRetriever`:支持 `target_kb_ids` 参数,实现多 Collection 并行检索 `[AC-AISVC-64]` +- [ ] T11.7 实现 `kb_default` 自动迁移:首次启动时为现有数据创建默认知识库记录 `[AC-AISVC-59]` +- [ ] T11.8 编写多知识库服务单元测试 `[AC-AISVC-59~AC-AISVC-64]` --- @@ -101,6 +102,24 @@ Phase 12 意图识别与规则引擎核心功能已完成,T12.6(Orchestrator ## 🧾 Session History +### Session #9 (2026-02-27) +- completed: + - T11.1-T11.5 多知识库管理核心功能 + - 扩展 KnowledgeBase 实体(kb_type、priority、is_enabled、doc_count) + - 实现 KnowledgeBaseService(CRUD、Collection 初始化/清理、文档计数) + - 实现知识库管理 API(POST/GET/PUT/DELETE) + - 升级 Qdrant Collection 命名(kb_{tenantId}_{kbId},兼容旧格式) + - 修改文档上传流程(支持 kbId 参数,索引到对应 Collection) +- changes: + - 新增 `app/models/entities.py` KBType 枚举、KnowledgeBaseCreate/Update Schema + - 新增 `app/services/knowledge_base_service.py` 知识库 CRUD 服务 + - 更新 `app/core/qdrant_client.py` 多知识库 Collection 管理方法 + - 更新 `app/api/admin/kb.py` 知识库管理 API 和文档上传流程 + - 更新 `spec/ai-service/tasks.md` 标记任务完成 +- notes: + - T11.6(OptimizedRetriever 多 Collection 检索)、T11.7(kb_default 迁移)、T11.8(单元测试)留待集成阶段 + - 进度文档已写入 `ai-service/docs/progress/phase11_multi_kb_progress.md` + ### Session #8 (2026-02-27) - completed: - T12.1-T12.5 意图识别与规则引擎核心功能 diff --git a/spec/ai-service/tasks.md b/spec/ai-service/tasks.md index 690ed83..f669d45 100644 --- a/spec/ai-service/tasks.md +++ b/spec/ai-service/tasks.md @@ -97,13 +97,13 @@ last_updated: "2026-02-27" | Phase 8 | LLM 配置与 RAG 调试输出 | 10 | ✅ 完成 | | Phase 9 | 租户管理与 RAG 优化 | 10 | ✅ 完成 | | Phase 10 | Prompt 模板化 | 10 | 🔄 进行中 (8/10) | -| Phase 11 | 多知识库管理 | 8 | ⏳ 待处理 | +| Phase 11 | 多知识库管理 | 8 | 🔄 进行中 (5/8) | | Phase 12 | 意图识别与规则引擎 | 7 | 🔄 进行中 (5/7) | | Phase 13 | 话术流程引擎 | 7 | ⏳ 待处理 | | Phase 14 | 输出护栏 | 8 | ⏳ 待处理 | | Phase 15 | 智能 RAG 增强与编排升级 | 8 | ⏳ 待处理 | -**已完成: 86 个任务** +**已完成: 91 个任务** --- @@ -181,11 +181,11 @@ last_updated: "2026-02-27" > 目标:从单个 kb_default 升级为多知识库分类管理,支持按类型和优先级组织知识。 -- [ ] T11.1 扩展 `KnowledgeBase` 实体:新增 `kb_type`、`priority`、`is_enabled`、`doc_count` 字段 `[AC-AISVC-59]` -- [ ] T11.2 实现知识库 CRUD 服务:创建时初始化 Qdrant Collection,删除时清理 Collection `[AC-AISVC-59, AC-AISVC-61, AC-AISVC-62]` -- [ ] T11.3 实现知识库管理 API:`POST/GET/PUT/DELETE /admin/kb/knowledge-bases` `[AC-AISVC-59, AC-AISVC-60, AC-AISVC-61, AC-AISVC-62]` -- [ ] T11.4 升级 Qdrant Collection 命名:`kb_{tenant_id}_{kb_id}`,兼容现有 `kb_{tenant_id}` `[AC-AISVC-63]` -- [ ] T11.5 修改文档上传流程:支持指定 `kbId` 参数,索引到对应 Collection `[AC-AISVC-63]` +- [x] T11.1 扩展 `KnowledgeBase` 实体:新增 `kb_type`、`priority`、`is_enabled`、`doc_count` 字段 `[AC-AISVC-59]` ✅ +- [x] T11.2 实现知识库 CRUD 服务:创建时初始化 Qdrant Collection,删除时清理 Collection `[AC-AISVC-59, AC-AISVC-61, AC-AISVC-62]` ✅ +- [x] T11.3 实现知识库管理 API:`POST/GET/PUT/DELETE /admin/kb/knowledge-bases` `[AC-AISVC-59, AC-AISVC-60, AC-AISVC-61, AC-AISVC-62]` ✅ +- [x] T11.4 升级 Qdrant Collection 命名:`kb_{tenant_id}_{kb_id}`,兼容现有 `kb_{tenant_id}` `[AC-AISVC-63]` ✅ +- [x] T11.5 修改文档上传流程:支持指定 `kbId` 参数,索引到对应 Collection `[AC-AISVC-63]` ✅ - [ ] T11.6 修改 `OptimizedRetriever`:支持 `target_kb_ids` 参数,实现多 Collection 并行检索 `[AC-AISVC-64]` - [ ] T11.7 实现 `kb_default` 自动迁移:首次启动时为现有数据创建默认知识库记录 `[AC-AISVC-59]` - [ ] T11.8 编写多知识库服务单元测试 `[AC-AISVC-59~AC-AISVC-64]`