feat: 实现 Prompt 模板化功能 (Phase 10 T10.1-T10.8) [AC-AISVC-51~AC-AISVC-58]
- 新增 PromptTemplate 和 PromptTemplateVersion SQLModel 实体 - 实现 PromptTemplateService:模板 CRUD、版本管理、发布/回滚、缓存 - 实现 VariableResolver:内置变量注入和自定义变量替换 - 实现 Prompt 模板管理 API(CRUD + 发布/回滚) - T10.9(修改 Orchestrator)和 T10.10(单元测试)留待集成阶段
This commit is contained in:
parent
0f1fa7de5c
commit
eb93636227
|
|
@ -6,10 +6,12 @@ Admin API routes for AI Service management.
|
|||
from app.api.admin.api_key import router as api_key_router
|
||||
from app.api.admin.dashboard import router as dashboard_router
|
||||
from app.api.admin.embedding import router as embedding_router
|
||||
from app.api.admin.intent_rules import router as intent_rules_router
|
||||
from app.api.admin.kb import router as kb_router
|
||||
from app.api.admin.llm import router as llm_router
|
||||
from app.api.admin.prompt_templates import router as prompt_templates_router
|
||||
from app.api.admin.rag import router as rag_router
|
||||
from app.api.admin.sessions import router as sessions_router
|
||||
from app.api.admin.tenants import router as tenants_router
|
||||
|
||||
__all__ = ["api_key_router", "dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"]
|
||||
__all__ = ["api_key_router", "dashboard_router", "embedding_router", "intent_rules_router", "kb_router", "llm_router", "prompt_templates_router", "rag_router", "sessions_router", "tenants_router"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
"""
|
||||
Prompt Template Management API.
|
||||
[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58, AC-AISVC-54, AC-AISVC-55] Prompt template CRUD and publish/rollback 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 PromptTemplateCreate, PromptTemplateUpdate
|
||||
from app.services.prompt.template_service import PromptTemplateService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/admin/prompt-templates", tags=["Prompt Management"])
|
||||
|
||||
|
||||
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_templates(
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
scene: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-57] List all prompt templates for a tenant.
|
||||
"""
|
||||
logger.info(f"[AC-AISVC-57] Listing prompt templates for tenant={tenant_id}, scene={scene}")
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
templates = await service.list_templates(tenant_id, scene)
|
||||
|
||||
data = []
|
||||
for t in templates:
|
||||
published_version = await service.get_published_version_info(tenant_id, t.id)
|
||||
data.append({
|
||||
"id": str(t.id),
|
||||
"name": t.name,
|
||||
"scene": t.scene,
|
||||
"description": t.description,
|
||||
"is_default": t.is_default,
|
||||
"published_version": published_version,
|
||||
"created_at": t.created_at.isoformat(),
|
||||
"updated_at": t.updated_at.isoformat(),
|
||||
})
|
||||
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_template(
|
||||
body: PromptTemplateCreate,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-52] Create a new prompt template.
|
||||
"""
|
||||
logger.info(f"[AC-AISVC-52] Creating prompt template for tenant={tenant_id}, name={body.name}")
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
template = await service.create_template(tenant_id, body)
|
||||
|
||||
return {
|
||||
"id": str(template.id),
|
||||
"name": template.name,
|
||||
"scene": template.scene,
|
||||
"description": template.description,
|
||||
"is_default": template.is_default,
|
||||
"created_at": template.created_at.isoformat(),
|
||||
"updated_at": template.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{tpl_id}")
|
||||
async def get_template_detail(
|
||||
tpl_id: uuid.UUID,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-58] Get prompt template detail with version history.
|
||||
"""
|
||||
logger.info(f"[AC-AISVC-58] Getting template detail for tenant={tenant_id}, id={tpl_id}")
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
detail = await service.get_template_detail(tenant_id, tpl_id)
|
||||
|
||||
if not detail:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
return detail
|
||||
|
||||
|
||||
@router.put("/{tpl_id}")
|
||||
async def update_template(
|
||||
tpl_id: uuid.UUID,
|
||||
body: PromptTemplateUpdate,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-53] Update prompt template (creates a new version).
|
||||
"""
|
||||
logger.info(f"[AC-AISVC-53] Updating template for tenant={tenant_id}, id={tpl_id}")
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
template = await service.update_template(tenant_id, tpl_id, body)
|
||||
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
published_version = await service.get_published_version_info(tenant_id, template.id)
|
||||
|
||||
return {
|
||||
"id": str(template.id),
|
||||
"name": template.name,
|
||||
"scene": template.scene,
|
||||
"description": template.description,
|
||||
"is_default": template.is_default,
|
||||
"published_version": published_version,
|
||||
"created_at": template.created_at.isoformat(),
|
||||
"updated_at": template.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{tpl_id}/publish")
|
||||
async def publish_template(
|
||||
tpl_id: uuid.UUID,
|
||||
body: dict[str, int],
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-54] Publish a specific version of the template.
|
||||
"""
|
||||
version = body.get("version")
|
||||
if version is None:
|
||||
raise HTTPException(status_code=400, detail="version is required")
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-54] Publishing template version for tenant={tenant_id}, "
|
||||
f"id={tpl_id}, version={version}"
|
||||
)
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
success = await service.publish_version(tenant_id, tpl_id, version)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Template or version not found")
|
||||
|
||||
return {"success": True, "message": f"Version {version} published successfully"}
|
||||
|
||||
|
||||
@router.post("/{tpl_id}/rollback")
|
||||
async def rollback_template(
|
||||
tpl_id: uuid.UUID,
|
||||
body: dict[str, int],
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
[AC-AISVC-55] Rollback to a specific historical version.
|
||||
"""
|
||||
version = body.get("version")
|
||||
if version is None:
|
||||
raise HTTPException(status_code=400, detail="version is required")
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-55] Rolling back template for tenant={tenant_id}, "
|
||||
f"id={tpl_id}, version={version}"
|
||||
)
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
success = await service.rollback_version(tenant_id, tpl_id, version)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Template or version not found")
|
||||
|
||||
return {"success": True, "message": f"Rolled back to version {version} successfully"}
|
||||
|
||||
|
||||
@router.delete("/{tpl_id}", status_code=204)
|
||||
async def delete_template(
|
||||
tpl_id: uuid.UUID,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a prompt template and all its versions.
|
||||
"""
|
||||
logger.info(f"Deleting template for tenant={tenant_id}, id={tpl_id}")
|
||||
|
||||
service = PromptTemplateService(session)
|
||||
success = await service.delete_template(tenant_id, tpl_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
|
@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api import chat_router, health_router
|
||||
from app.api.admin import api_key_router, dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router
|
||||
from app.api.admin import api_key_router, dashboard_router, embedding_router, intent_rules_router, kb_router, llm_router, prompt_templates_router, rag_router, sessions_router, tenants_router
|
||||
from app.api.admin.kb_optimized import router as kb_optimized_router
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import close_db, init_db
|
||||
|
|
@ -130,9 +130,11 @@ app.include_router(chat_router)
|
|||
app.include_router(api_key_router)
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(embedding_router)
|
||||
app.include_router(intent_rules_router)
|
||||
app.include_router(kb_router)
|
||||
app.include_router(kb_optimized_router)
|
||||
app.include_router(llm_router)
|
||||
app.include_router(prompt_templates_router)
|
||||
app.include_router(rag_router)
|
||||
app.include_router(sessions_router)
|
||||
app.include_router(tenants_router)
|
||||
|
|
|
|||
|
|
@ -118,20 +118,34 @@ class Tenant(SQLModel, table=True):
|
|||
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||
|
||||
|
||||
class KBType(str, Enum):
|
||||
PRODUCT = "product"
|
||||
FAQ = "faq"
|
||||
SCRIPT = "script"
|
||||
POLICY = "policy"
|
||||
GENERAL = "general"
|
||||
|
||||
|
||||
class KnowledgeBase(SQLModel, table=True):
|
||||
"""
|
||||
[AC-ASA-01] Knowledge base entity with tenant isolation.
|
||||
[AC-ASA-01, AC-AISVC-59] Knowledge base entity with tenant isolation.
|
||||
[v0.6.0] Extended with kb_type, priority, is_enabled, doc_count for multi-KB management.
|
||||
"""
|
||||
|
||||
__tablename__ = "knowledge_bases"
|
||||
__table_args__ = (
|
||||
Index("ix_knowledge_bases_tenant_id", "tenant_id"),
|
||||
Index("ix_knowledge_bases_tenant_kb_type", "tenant_id", "kb_type"),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
|
||||
name: str = Field(..., description="Knowledge base name")
|
||||
kb_type: str = Field(default=KBType.GENERAL.value, description="Knowledge base type: product/faq/script/policy/general")
|
||||
description: str | None = Field(default=None, description="Knowledge base description")
|
||||
priority: int = Field(default=0, ge=0, description="Priority weight, higher value means higher priority")
|
||||
is_enabled: bool = Field(default=True, description="Whether the knowledge base is enabled")
|
||||
doc_count: int = Field(default=0, ge=0, description="Document count (cached statistic)")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||
|
||||
|
|
@ -184,9 +198,20 @@ class IndexJob(SQLModel, table=True):
|
|||
class KnowledgeBaseCreate(SQLModel):
|
||||
"""Schema for creating a new knowledge base."""
|
||||
|
||||
tenant_id: str
|
||||
name: str
|
||||
kb_type: str = KBType.GENERAL.value
|
||||
description: str | None = None
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class KnowledgeBaseUpdate(SQLModel):
|
||||
"""Schema for updating a knowledge base."""
|
||||
|
||||
name: str | None = None
|
||||
kb_type: str | None = None
|
||||
description: str | None = None
|
||||
priority: int | None = None
|
||||
is_enabled: bool | None = None
|
||||
|
||||
|
||||
class DocumentCreate(SQLModel):
|
||||
|
|
@ -222,3 +247,181 @@ class ApiKeyCreate(SQLModel):
|
|||
key: str
|
||||
name: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class TemplateVersionStatus(str, Enum):
|
||||
DRAFT = "draft"
|
||||
PUBLISHED = "published"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class PromptTemplate(SQLModel, table=True):
|
||||
"""
|
||||
[AC-AISVC-51, AC-AISVC-52] Prompt template entity with tenant isolation.
|
||||
Main table for storing template metadata.
|
||||
"""
|
||||
|
||||
__tablename__ = "prompt_templates"
|
||||
__table_args__ = (
|
||||
Index("ix_prompt_templates_tenant_scene", "tenant_id", "scene"),
|
||||
Index("ix_prompt_templates_tenant_id", "tenant_id"),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
|
||||
name: str = Field(..., description="Template name, e.g., 'Default Customer Service Persona'")
|
||||
scene: str = Field(..., description="Scene tag: chat/rag_qa/greeting/farewell")
|
||||
description: str | None = Field(default=None, description="Template description")
|
||||
is_default: bool = Field(default=False, description="Whether this is the default template for the scene")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||
|
||||
|
||||
class PromptTemplateVersion(SQLModel, table=True):
|
||||
"""
|
||||
[AC-AISVC-53] Prompt template version entity.
|
||||
Stores versioned content with status management.
|
||||
"""
|
||||
|
||||
__tablename__ = "prompt_template_versions"
|
||||
__table_args__ = (
|
||||
Index("ix_template_versions_template_status", "template_id", "status"),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
template_id: uuid.UUID = Field(..., description="Foreign key to prompt_templates.id", foreign_key="prompt_templates.id", index=True)
|
||||
version: int = Field(..., description="Version number (auto-incremented per template)")
|
||||
status: str = Field(default=TemplateVersionStatus.DRAFT.value, description="Version status: draft/published/archived")
|
||||
system_instruction: str = Field(..., description="System instruction content with {{variable}} placeholders")
|
||||
variables: list[dict[str, Any]] | None = Field(
|
||||
default=None,
|
||||
sa_column=Column("variables", JSON, nullable=True),
|
||||
description="Variable definitions, e.g., [{'name': 'persona_name', 'default': '小N', 'description': '人设名称'}]"
|
||||
)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||
|
||||
|
||||
class PromptTemplateCreate(SQLModel):
|
||||
"""Schema for creating a new prompt template."""
|
||||
|
||||
name: str
|
||||
scene: str
|
||||
description: str | None = None
|
||||
system_instruction: str
|
||||
variables: list[dict[str, Any]] | None = None
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
class PromptTemplateUpdate(SQLModel):
|
||||
"""Schema for updating a prompt template."""
|
||||
|
||||
name: str | None = None
|
||||
scene: str | None = None
|
||||
description: str | None = None
|
||||
system_instruction: str | None = None
|
||||
variables: list[dict[str, Any]] | None = None
|
||||
is_default: bool | None = None
|
||||
|
||||
|
||||
class ResponseType(str, Enum):
|
||||
"""[AC-AISVC-65] Response type for intent rules."""
|
||||
FIXED = "fixed"
|
||||
RAG = "rag"
|
||||
FLOW = "flow"
|
||||
TRANSFER = "transfer"
|
||||
|
||||
|
||||
class IntentRule(SQLModel, table=True):
|
||||
"""
|
||||
[AC-AISVC-65] Intent rule entity with tenant isolation.
|
||||
Supports keyword and regex matching for intent recognition.
|
||||
"""
|
||||
|
||||
__tablename__ = "intent_rules"
|
||||
__table_args__ = (
|
||||
Index("ix_intent_rules_tenant_enabled_priority", "tenant_id", "is_enabled"),
|
||||
Index("ix_intent_rules_tenant_id", "tenant_id"),
|
||||
)
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
|
||||
name: str = Field(..., description="Intent name, e.g., 'Return Intent'")
|
||||
keywords: list[str] | None = Field(
|
||||
default=None,
|
||||
sa_column=Column("keywords", JSON, nullable=True),
|
||||
description="Keyword list for matching, e.g., ['return', 'refund']"
|
||||
)
|
||||
patterns: list[str] | None = Field(
|
||||
default=None,
|
||||
sa_column=Column("patterns", JSON, nullable=True),
|
||||
description="Regex pattern list for matching, e.g., ['return.*goods', 'how to return']"
|
||||
)
|
||||
priority: int = Field(default=0, description="Priority (higher value = higher priority)")
|
||||
response_type: str = Field(..., description="Response type: fixed/rag/flow/transfer")
|
||||
target_kb_ids: list[str] | None = Field(
|
||||
default=None,
|
||||
sa_column=Column("target_kb_ids", JSON, nullable=True),
|
||||
description="Target knowledge base IDs for rag type"
|
||||
)
|
||||
flow_id: uuid.UUID | None = Field(default=None, description="Flow ID for flow type")
|
||||
fixed_reply: str | None = Field(default=None, description="Fixed reply content for fixed type")
|
||||
transfer_message: str | None = Field(default=None, description="Transfer message for transfer type")
|
||||
is_enabled: bool = Field(default=True, description="Whether the rule is enabled")
|
||||
hit_count: int = Field(default=0, description="Hit count for statistics")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||
|
||||
|
||||
class IntentRuleCreate(SQLModel):
|
||||
"""[AC-AISVC-65] Schema for creating a new intent rule."""
|
||||
|
||||
name: str
|
||||
keywords: list[str] | None = None
|
||||
patterns: list[str] | None = None
|
||||
priority: int = 0
|
||||
response_type: str
|
||||
target_kb_ids: list[str] | None = None
|
||||
flow_id: str | None = None
|
||||
fixed_reply: str | None = None
|
||||
transfer_message: str | None = None
|
||||
|
||||
|
||||
class IntentRuleUpdate(SQLModel):
|
||||
"""[AC-AISVC-67] Schema for updating an intent rule."""
|
||||
|
||||
name: str | None = None
|
||||
keywords: list[str] | None = None
|
||||
patterns: list[str] | None = None
|
||||
priority: int | None = None
|
||||
response_type: str | None = None
|
||||
target_kb_ids: list[str] | None = None
|
||||
flow_id: str | None = None
|
||||
fixed_reply: str | None = None
|
||||
transfer_message: str | None = None
|
||||
is_enabled: bool | None = None
|
||||
|
||||
|
||||
class IntentMatchResult:
|
||||
"""
|
||||
[AC-AISVC-69] Result of intent matching.
|
||||
Contains the matched rule and match details.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rule: IntentRule,
|
||||
match_type: str,
|
||||
matched: str,
|
||||
):
|
||||
self.rule = rule
|
||||
self.match_type = match_type
|
||||
self.matched = matched
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
Prompt template services for AI Service.
|
||||
[AC-AISVC-51~AC-AISVC-58] Database-driven prompt template system.
|
||||
"""
|
||||
|
||||
from app.services.prompt.template_service import PromptTemplateService
|
||||
from app.services.prompt.variable_resolver import VariableResolver
|
||||
|
||||
__all__ = ["PromptTemplateService", "VariableResolver"]
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
"""
|
||||
Prompt template service for AI Service.
|
||||
[AC-AISVC-51~AC-AISVC-58] Template CRUD, version management, publish/rollback, and caching.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Sequence
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import col
|
||||
|
||||
from app.models.entities import (
|
||||
PromptTemplate,
|
||||
PromptTemplateVersion,
|
||||
PromptTemplateCreate,
|
||||
PromptTemplateUpdate,
|
||||
TemplateVersionStatus,
|
||||
)
|
||||
from app.services.prompt.variable_resolver import VariableResolver
|
||||
from app.core.prompts import SYSTEM_PROMPT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CACHE_TTL_SECONDS = 300
|
||||
|
||||
|
||||
class TemplateCache:
|
||||
"""
|
||||
[AC-AISVC-51] In-memory cache for published templates.
|
||||
Key: (tenant_id, scene)
|
||||
Value: (template_version, cached_at)
|
||||
TTL: 300 seconds
|
||||
"""
|
||||
|
||||
def __init__(self, ttl_seconds: int = CACHE_TTL_SECONDS):
|
||||
self._cache: dict[tuple[str, str], tuple[PromptTemplateVersion, float]] = {}
|
||||
self._ttl = ttl_seconds
|
||||
|
||||
def get(self, tenant_id: str, scene: str) -> PromptTemplateVersion | None:
|
||||
"""Get cached template version if not expired."""
|
||||
key = (tenant_id, scene)
|
||||
if key in self._cache:
|
||||
version, cached_at = self._cache[key]
|
||||
if time.time() - cached_at < self._ttl:
|
||||
return version
|
||||
else:
|
||||
del self._cache[key]
|
||||
return None
|
||||
|
||||
def set(self, tenant_id: str, scene: str, version: PromptTemplateVersion) -> None:
|
||||
"""Cache a template version."""
|
||||
key = (tenant_id, scene)
|
||||
self._cache[key] = (version, time.time())
|
||||
|
||||
def invalidate(self, tenant_id: str, scene: str | None = None) -> None:
|
||||
"""Invalidate cache for a tenant (optionally for a specific scene)."""
|
||||
if scene:
|
||||
key = (tenant_id, scene)
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
else:
|
||||
keys_to_delete = [k for k in self._cache if k[0] == tenant_id]
|
||||
for key in keys_to_delete:
|
||||
del self._cache[key]
|
||||
|
||||
|
||||
_template_cache = TemplateCache()
|
||||
|
||||
|
||||
class PromptTemplateService:
|
||||
"""
|
||||
[AC-AISVC-52~AC-AISVC-58] Service for managing prompt templates.
|
||||
|
||||
Features:
|
||||
- Template CRUD with tenant isolation
|
||||
- Version management (auto-create new version on update)
|
||||
- Publish/rollback functionality
|
||||
- In-memory caching with TTL
|
||||
- Fallback to hardcoded SYSTEM_PROMPT
|
||||
"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self._session = session
|
||||
self._cache = _template_cache
|
||||
|
||||
async def create_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
create_data: PromptTemplateCreate,
|
||||
) -> PromptTemplate:
|
||||
"""
|
||||
[AC-AISVC-52] Create a new prompt template with initial version.
|
||||
"""
|
||||
template = PromptTemplate(
|
||||
tenant_id=tenant_id,
|
||||
name=create_data.name,
|
||||
scene=create_data.scene,
|
||||
description=create_data.description,
|
||||
is_default=create_data.is_default,
|
||||
)
|
||||
self._session.add(template)
|
||||
await self._session.flush()
|
||||
|
||||
initial_version = PromptTemplateVersion(
|
||||
template_id=template.id,
|
||||
version=1,
|
||||
status=TemplateVersionStatus.DRAFT.value,
|
||||
system_instruction=create_data.system_instruction,
|
||||
variables=create_data.variables,
|
||||
)
|
||||
self._session.add(initial_version)
|
||||
await self._session.flush()
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-52] Created prompt template: tenant={tenant_id}, "
|
||||
f"id={template.id}, name={template.name}"
|
||||
)
|
||||
return template
|
||||
|
||||
async def list_templates(
|
||||
self,
|
||||
tenant_id: str,
|
||||
scene: str | None = None,
|
||||
) -> Sequence[PromptTemplate]:
|
||||
"""
|
||||
[AC-AISVC-57] List templates for a tenant, optionally filtered by scene.
|
||||
"""
|
||||
stmt = select(PromptTemplate).where(
|
||||
PromptTemplate.tenant_id == tenant_id
|
||||
)
|
||||
|
||||
if scene:
|
||||
stmt = stmt.where(PromptTemplate.scene == scene)
|
||||
|
||||
stmt = stmt.order_by(col(PromptTemplate.created_at).desc())
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
) -> PromptTemplate | None:
|
||||
"""
|
||||
[AC-AISVC-58] Get template by ID with tenant isolation.
|
||||
"""
|
||||
stmt = select(PromptTemplate).where(
|
||||
PromptTemplate.tenant_id == tenant_id,
|
||||
PromptTemplate.id == template_id,
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_template_detail(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
[AC-AISVC-58] Get template detail with all versions.
|
||||
"""
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return None
|
||||
|
||||
versions = await self._get_versions(template_id)
|
||||
|
||||
current_version = None
|
||||
for v in versions:
|
||||
if v.status == TemplateVersionStatus.PUBLISHED.value:
|
||||
current_version = v
|
||||
break
|
||||
|
||||
return {
|
||||
"id": str(template.id),
|
||||
"name": template.name,
|
||||
"scene": template.scene,
|
||||
"description": template.description,
|
||||
"is_default": template.is_default,
|
||||
"current_version": {
|
||||
"version": current_version.version,
|
||||
"status": current_version.status,
|
||||
"system_instruction": current_version.system_instruction,
|
||||
"variables": current_version.variables or [],
|
||||
} if current_version else None,
|
||||
"versions": [
|
||||
{
|
||||
"version": v.version,
|
||||
"status": v.status,
|
||||
"created_at": v.created_at.isoformat(),
|
||||
}
|
||||
for v in versions
|
||||
],
|
||||
"created_at": template.created_at.isoformat(),
|
||||
"updated_at": template.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
async def update_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
update_data: PromptTemplateUpdate,
|
||||
) -> PromptTemplate | None:
|
||||
"""
|
||||
[AC-AISVC-53] Update template and create a new version.
|
||||
"""
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return None
|
||||
|
||||
if update_data.name is not None:
|
||||
template.name = update_data.name
|
||||
if update_data.scene is not None:
|
||||
template.scene = update_data.scene
|
||||
if update_data.description is not None:
|
||||
template.description = update_data.description
|
||||
if update_data.is_default is not None:
|
||||
template.is_default = update_data.is_default
|
||||
template.updated_at = datetime.utcnow()
|
||||
|
||||
if update_data.system_instruction is not None:
|
||||
latest_version = await self._get_latest_version(template_id)
|
||||
new_version_num = (latest_version.version + 1) if latest_version else 1
|
||||
|
||||
new_version = PromptTemplateVersion(
|
||||
template_id=template_id,
|
||||
version=new_version_num,
|
||||
status=TemplateVersionStatus.DRAFT.value,
|
||||
system_instruction=update_data.system_instruction,
|
||||
variables=update_data.variables,
|
||||
)
|
||||
self._session.add(new_version)
|
||||
|
||||
await self._session.flush()
|
||||
|
||||
self._cache.invalidate(tenant_id, template.scene)
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-53] Updated prompt template: tenant={tenant_id}, id={template_id}"
|
||||
)
|
||||
return template
|
||||
|
||||
async def publish_version(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
version: int,
|
||||
) -> bool:
|
||||
"""
|
||||
[AC-AISVC-54] Publish a specific version of the template.
|
||||
Old published version will be archived.
|
||||
"""
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return False
|
||||
|
||||
versions = await self._get_versions(template_id)
|
||||
|
||||
for v in versions:
|
||||
if v.status == TemplateVersionStatus.PUBLISHED.value:
|
||||
v.status = TemplateVersionStatus.ARCHIVED.value
|
||||
|
||||
target_version = None
|
||||
for v in versions:
|
||||
if v.version == version:
|
||||
target_version = v
|
||||
break
|
||||
|
||||
if not target_version:
|
||||
return False
|
||||
|
||||
target_version.status = TemplateVersionStatus.PUBLISHED.value
|
||||
await self._session.flush()
|
||||
|
||||
self._cache.invalidate(tenant_id, template.scene)
|
||||
self._cache.set(tenant_id, template.scene, target_version)
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-54] Published template version: tenant={tenant_id}, "
|
||||
f"template_id={template_id}, version={version}"
|
||||
)
|
||||
return True
|
||||
|
||||
async def rollback_version(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
version: int,
|
||||
) -> bool:
|
||||
"""
|
||||
[AC-AISVC-55] Rollback to a specific historical version.
|
||||
"""
|
||||
return await self.publish_version(tenant_id, template_id, version)
|
||||
|
||||
async def get_published_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
scene: str,
|
||||
resolver: VariableResolver | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
[AC-AISVC-51, AC-AISVC-56] Get the published template for a scene.
|
||||
|
||||
Resolution order:
|
||||
1. Check in-memory cache
|
||||
2. Query database for published version
|
||||
3. Fallback to hardcoded SYSTEM_PROMPT
|
||||
"""
|
||||
cached = self._cache.get(tenant_id, scene)
|
||||
if cached:
|
||||
logger.debug(f"[AC-AISVC-51] Cache hit for template: tenant={tenant_id}, scene={scene}")
|
||||
if resolver:
|
||||
return resolver.resolve(cached.system_instruction, cached.variables)
|
||||
return cached.system_instruction
|
||||
|
||||
stmt = (
|
||||
select(PromptTemplateVersion)
|
||||
.join(PromptTemplate, PromptTemplateVersion.template_id == PromptTemplate.id)
|
||||
.where(
|
||||
PromptTemplate.tenant_id == tenant_id,
|
||||
PromptTemplate.scene == scene,
|
||||
PromptTemplateVersion.status == TemplateVersionStatus.PUBLISHED.value,
|
||||
)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
published_version = result.scalar_one_or_none()
|
||||
|
||||
if published_version:
|
||||
self._cache.set(tenant_id, scene, published_version)
|
||||
logger.info(
|
||||
f"[AC-AISVC-51] Loaded published template from DB: "
|
||||
f"tenant={tenant_id}, scene={scene}"
|
||||
)
|
||||
if resolver:
|
||||
return resolver.resolve(published_version.system_instruction, published_version.variables)
|
||||
return published_version.system_instruction
|
||||
|
||||
logger.info(
|
||||
f"[AC-AISVC-51] No published template found, using fallback: "
|
||||
f"tenant={tenant_id}, scene={scene}"
|
||||
)
|
||||
return SYSTEM_PROMPT
|
||||
|
||||
async def get_published_version_info(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
) -> int | None:
|
||||
"""Get the published version number for a template."""
|
||||
stmt = (
|
||||
select(PromptTemplateVersion)
|
||||
.where(
|
||||
PromptTemplateVersion.template_id == template_id,
|
||||
PromptTemplateVersion.status == TemplateVersionStatus.PUBLISHED.value,
|
||||
)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
version = result.scalar_one_or_none()
|
||||
return version.version if version else None
|
||||
|
||||
async def _get_versions(
|
||||
self,
|
||||
template_id: uuid.UUID,
|
||||
) -> Sequence[PromptTemplateVersion]:
|
||||
"""Get all versions for a template, ordered by version desc."""
|
||||
stmt = (
|
||||
select(PromptTemplateVersion)
|
||||
.where(PromptTemplateVersion.template_id == template_id)
|
||||
.order_by(col(PromptTemplateVersion.version).desc())
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def _get_latest_version(
|
||||
self,
|
||||
template_id: uuid.UUID,
|
||||
) -> PromptTemplateVersion | None:
|
||||
"""Get the latest version for a template."""
|
||||
stmt = (
|
||||
select(PromptTemplateVersion)
|
||||
.where(PromptTemplateVersion.template_id == template_id)
|
||||
.order_by(col(PromptTemplateVersion.version).desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def delete_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: uuid.UUID,
|
||||
) -> bool:
|
||||
"""Delete a template and all its versions."""
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return False
|
||||
|
||||
versions = await self._get_versions(template_id)
|
||||
for v in versions:
|
||||
await self._session.delete(v)
|
||||
|
||||
await self._session.delete(template)
|
||||
await self._session.flush()
|
||||
|
||||
self._cache.invalidate(tenant_id, template.scene)
|
||||
|
||||
logger.info(
|
||||
f"Deleted prompt template: tenant={tenant_id}, id={template_id}"
|
||||
)
|
||||
return True
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
"""
|
||||
Variable resolver for prompt templates.
|
||||
[AC-AISVC-56] Built-in and custom variable replacement engine.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VARIABLE_PATTERN = re.compile(r"\{\{(\w+)\}\}")
|
||||
|
||||
BUILTIN_VARIABLES = {
|
||||
"persona_name": "小N",
|
||||
"current_time": lambda: datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"channel_type": "default",
|
||||
"tenant_name": "平台",
|
||||
"session_id": "",
|
||||
}
|
||||
|
||||
|
||||
class VariableResolver:
|
||||
"""
|
||||
[AC-AISVC-56] Variable replacement engine for prompt templates.
|
||||
|
||||
Supports:
|
||||
- Built-in variables: persona_name, current_time, channel_type, tenant_name, session_id
|
||||
- Custom variables: defined in template with defaults
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_type: str = "default",
|
||||
tenant_name: str = "平台",
|
||||
session_id: str = "",
|
||||
):
|
||||
self._context = {
|
||||
"channel_type": channel_type,
|
||||
"tenant_name": tenant_name,
|
||||
"session_id": session_id,
|
||||
}
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
template: str,
|
||||
variables: list[dict[str, Any]] | None = None,
|
||||
extra_context: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Resolve all {{variable}} placeholders in the template.
|
||||
|
||||
Args:
|
||||
template: Template string with {{variable}} placeholders
|
||||
variables: Custom variable definitions from template
|
||||
extra_context: Additional context for resolution
|
||||
|
||||
Returns:
|
||||
Template with all variables replaced
|
||||
"""
|
||||
context = self._build_context(variables, extra_context)
|
||||
|
||||
def replace_var(match: re.Match) -> str:
|
||||
var_name = match.group(1)
|
||||
if var_name in context:
|
||||
value = context[var_name]
|
||||
if callable(value):
|
||||
return str(value())
|
||||
return str(value)
|
||||
logger.warning(f"Unknown variable in template: {var_name}")
|
||||
return match.group(0)
|
||||
|
||||
resolved = VARIABLE_PATTERN.sub(replace_var, template)
|
||||
return resolved
|
||||
|
||||
def _build_context(
|
||||
self,
|
||||
variables: list[dict[str, Any]] | None,
|
||||
extra_context: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the complete context for variable resolution."""
|
||||
context = {}
|
||||
|
||||
for key, value in BUILTIN_VARIABLES.items():
|
||||
if key in self._context:
|
||||
context[key] = self._context[key]
|
||||
else:
|
||||
context[key] = value
|
||||
|
||||
if variables:
|
||||
for var in variables:
|
||||
name = var.get("name")
|
||||
default = var.get("default", "")
|
||||
if name:
|
||||
context[name] = default
|
||||
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
|
||||
return context
|
||||
|
||||
def extract_variables(self, template: str) -> list[str]:
|
||||
"""
|
||||
Extract all variable names from a template.
|
||||
|
||||
Args:
|
||||
template: Template string
|
||||
|
||||
Returns:
|
||||
List of variable names found in the template
|
||||
"""
|
||||
return VARIABLE_PATTERN.findall(template)
|
||||
|
||||
def validate_variables(
|
||||
self,
|
||||
template: str,
|
||||
defined_variables: list[dict[str, Any]] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Validate that all variables in template are defined.
|
||||
|
||||
Args:
|
||||
template: Template string
|
||||
defined_variables: Variables defined in template metadata
|
||||
|
||||
Returns:
|
||||
Dict with 'valid' boolean and 'missing' list
|
||||
"""
|
||||
used_vars = set(self.extract_variables(template))
|
||||
builtin_vars = set(BUILTIN_VARIABLES.keys())
|
||||
|
||||
defined_names = set()
|
||||
if defined_variables:
|
||||
defined_names = {v.get("name") for v in defined_variables if v.get("name")}
|
||||
|
||||
available_vars = builtin_vars | defined_names
|
||||
missing = used_vars - available_vars
|
||||
|
||||
return {
|
||||
"valid": len(missing) == 0,
|
||||
"missing": list(missing),
|
||||
"used_variables": list(used_vars),
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- module: `ai-service`
|
||||
- feature: `AISVC` (Python AI 中台)
|
||||
- status: ✅ 已完成
|
||||
- status: 🔄 进行中 (Phase 12)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -32,26 +32,29 @@
|
|||
- [x] Phase 7: 嵌入模型可插拔与文档解析 (100%) ✅
|
||||
- [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 12: 意图识别与规则引擎 (71%) 🔄 (T12.1-T12.5 完成,T12.6-T12.7 待集成阶段)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Current Phase
|
||||
|
||||
### Goal
|
||||
Phase 9 已完成!项目进入稳定迭代阶段。
|
||||
Phase 12 意图识别与规则引擎核心功能已完成,T12.6(Orchestrator 集成)和 T12.7(单元测试)留待集成阶段。
|
||||
|
||||
### Completed Tasks (Phase 9)
|
||||
### Completed Tasks (Phase 12)
|
||||
|
||||
- [x] T9.1 实现 `Tenant` 实体:定义租户数据模型 `[AC-AISVC-10]` ✅
|
||||
- [x] T9.2 实现租户 ID 格式校验:`name@ash@year` 格式验证 `[AC-AISVC-10, AC-AISVC-12]` ✅
|
||||
- [x] T9.3 实现租户自动创建:请求时自动创建不存在的租户 `[AC-AISVC-10]` ✅
|
||||
- [x] T9.4 实现 `GET /admin/tenants` API:返回租户列表 `[AC-AISVC-10]` ✅
|
||||
- [x] T9.5 前端租户选择器:实现租户切换功能 `[AC-ASA-01]` ✅
|
||||
- [x] T9.6 文档多编码支持:支持 UTF-8、GBK、GB2312 等编码解码 `[AC-AISVC-21]` ✅
|
||||
- [x] T9.7 按行分块功能:实现 `chunk_text_by_lines` 函数 `[AC-AISVC-22]` ✅
|
||||
- [x] T9.8 实现 `NomicEmbeddingProvider`:支持多维度向量 `[AC-AISVC-29]` ✅
|
||||
- [x] T9.9 实现多向量存储:支持 full/256/512 三种维度 `[AC-AISVC-16]` ✅
|
||||
- [x] T9.10 实现 `KnowledgeIndexer`:优化的知识库索引服务 `[AC-AISVC-22]` ✅
|
||||
- [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]` ✅
|
||||
|
||||
### Pending Tasks (Phase 12 - 集成阶段)
|
||||
|
||||
- [ ] T12.6 在 Orchestrator 中集成 IntentRouter:RAG 检索前执行意图识别,按 response_type 路由 `[AC-AISVC-69, AC-AISVC-70]`
|
||||
- [ ] T12.7 编写意图识别服务单元测试 `[AC-AISVC-65~AC-AISVC-70]`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -62,41 +65,79 @@ Phase 9 已完成!项目进入稳定迭代阶段。
|
|||
- `ai-service/`
|
||||
- `app/`
|
||||
- `api/` - FastAPI 路由层
|
||||
- `admin/tenants.py` - 租户管理 API ✅
|
||||
- `core/` - 配置、异常、中间件、SSE
|
||||
- `middleware.py` - 租户 ID 格式校验与自动创建 ✅
|
||||
- `admin/intent_rules.py` - 意图规则管理 API ✅
|
||||
- `admin/prompt_templates.py` - Prompt 模板管理 API ✅
|
||||
- `models/` - Pydantic 模型和 SQLModel 实体
|
||||
- `entities.py` - Tenant 实体 ✅
|
||||
- `entities.py` - IntentRule, PromptTemplate, PromptTemplateVersion 实体 ✅
|
||||
- `services/`
|
||||
- `embedding/nomic_provider.py` - Nomic 嵌入提供者 ✅
|
||||
- `retrieval/` - 检索层
|
||||
- `indexer.py` - 知识库索引服务 ✅
|
||||
- `metadata.py` - 元数据模型 ✅
|
||||
- `optimized_retriever.py` - 优化检索器 ✅
|
||||
- `tests/` - 单元测试
|
||||
- `intent/` - 意图识别服务 ✅
|
||||
- `__init__.py` - 模块导出
|
||||
- `rule_service.py` - 规则 CRUD、命中统计、缓存
|
||||
- `router.py` - IntentRouter 匹配引擎
|
||||
- `prompt/` - Prompt 模板服务 ✅
|
||||
- `__init__.py` - 模块导出
|
||||
- `template_service.py` - 模板 CRUD、版本管理、发布/回滚、缓存
|
||||
- `variable_resolver.py` - 变量替换引擎
|
||||
|
||||
### Key Decisions (Why / Impact)
|
||||
|
||||
- decision: 租户 ID 格式采用 `name@ash@year` 格式
|
||||
reason: 便于解析和展示租户信息
|
||||
impact: 中间件自动校验格式并解析
|
||||
- decision: 意图规则数据库驱动
|
||||
reason: 支持动态配置意图识别规则,无需重启服务
|
||||
impact: 规则存储在 PostgreSQL,支持按租户隔离
|
||||
|
||||
- decision: 租户自动创建策略
|
||||
reason: 简化租户管理流程,无需预先创建
|
||||
impact: 首次请求时自动创建租户记录
|
||||
- decision: 关键词 + 正则双匹配机制
|
||||
reason: 关键词匹配快速高效,正则匹配支持复杂模式
|
||||
impact: 先关键词匹配再正则匹配,优先级高的规则先匹配
|
||||
|
||||
- decision: 多维度向量存储(full/256/512)
|
||||
reason: 支持不同检索场景的性能优化
|
||||
impact: Qdrant 使用 named vector 存储
|
||||
- decision: 内存缓存 + TTL 策略
|
||||
reason: 减少数据库查询,提升匹配性能
|
||||
impact: 缓存 TTL=60s,CRUD 操作时主动失效
|
||||
|
||||
- decision: 文档多编码支持
|
||||
reason: 兼容中文文档的各种编码格式
|
||||
impact: 按优先级尝试多种编码解码
|
||||
- decision: 四种响应类型
|
||||
reason: 支持不同的处理链路
|
||||
impact: fixed 直接返回、rag 定向检索、flow 进入流程、transfer 转人工
|
||||
|
||||
---
|
||||
|
||||
## 🧾 Session History
|
||||
|
||||
### Session #8 (2026-02-27)
|
||||
- completed:
|
||||
- T12.1-T12.5 意图识别与规则引擎核心功能
|
||||
- 实现 IntentRule 实体(支持关键词、正则、优先级、四种响应类型)
|
||||
- 实现 IntentRuleService(CRUD、命中统计、缓存)
|
||||
- 实现 IntentRouter 匹配引擎(按优先级 DESC 遍历,先关键词再正则)
|
||||
- 实现规则缓存(按 tenant_id 缓存,TTL=60s,CRUD 主动失效)
|
||||
- 实现意图规则管理 API(POST/GET/PUT/DELETE)
|
||||
- changes:
|
||||
- 新增 `app/models/entities.py` IntentRule, IntentRuleCreate, IntentRuleUpdate, IntentMatchResult 实体
|
||||
- 新增 `app/services/intent/__init__.py` 模块导出
|
||||
- 新增 `app/services/intent/rule_service.py` 规则服务
|
||||
- 新增 `app/services/intent/router.py` 匹配引擎
|
||||
- 新增 `app/api/admin/intent_rules.py` 规则管理 API
|
||||
- 更新 `app/api/admin/__init__.py` 导出新路由
|
||||
- 更新 `app/main.py` 注册新路由
|
||||
- 更新 `spec/ai-service/tasks.md` 标记任务完成
|
||||
- notes:
|
||||
- T12.6(Orchestrator 集成)和 T12.7(单元测试)留待集成阶段
|
||||
|
||||
### Session #7 (2026-02-27)
|
||||
- completed:
|
||||
- T10.1-T10.8 Prompt 模板化核心功能
|
||||
- 实现 PromptTemplate 和 PromptTemplateVersion 实体
|
||||
- 实现 PromptTemplateService(CRUD、版本管理、发布/回滚、缓存)
|
||||
- 实现 VariableResolver 变量替换引擎
|
||||
- 实现 Prompt 模板管理 API(CRUD + 发布/回滚)
|
||||
- changes:
|
||||
- 新增 `app/models/entities.py` PromptTemplate, PromptTemplateVersion 实体
|
||||
- 新增 `app/services/prompt/template_service.py` 模板服务
|
||||
- 新增 `app/services/prompt/variable_resolver.py` 变量替换引擎
|
||||
- 新增 `app/api/admin/prompt_templates.py` 模板管理 API
|
||||
- 更新 `app/main.py` 注册新路由
|
||||
- 更新 `spec/ai-service/tasks.md` 标记任务完成
|
||||
- notes:
|
||||
- T10.9(修改 Orchestrator)和 T10.10(单元测试)留待集成阶段
|
||||
|
||||
### Session #6 (2026-02-25)
|
||||
- completed:
|
||||
- T9.1-T9.10 租户管理与 RAG 优化功能
|
||||
|
|
@ -119,12 +160,6 @@ Phase 9 已完成!项目进入稳定迭代阶段。
|
|||
- 新增 `app/services/retrieval/indexer.py`
|
||||
- 新增 `app/services/retrieval/metadata.py`
|
||||
- 新增 `app/services/retrieval/optimized_retriever.py`
|
||||
- commits:
|
||||
- `docs: 更新任务清单,添加 Phase 9 租户管理与 RAG 优化任务 [AC-AISVC-10, AC-ASA-01]`
|
||||
- `feat: 实现租户管理功能,支持租户ID格式校验与自动创建 [AC-AISVC-10, AC-AISVC-12, AC-ASA-01]`
|
||||
- `feat: 文档索引优化,支持多编码解码和按行分块 [AC-AISVC-21, AC-AISVC-22]`
|
||||
- `feat: RAG 检索优化,实现多维度向量存储和 Nomic 嵌入提供者 [AC-AISVC-16, AC-AISVC-29]`
|
||||
- `feat: RAG 配置优化与检索日志增强 [AC-AISVC-16, AC-AISVC-17]`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
feature_id: "AISVC"
|
||||
title: "Python AI 中台(ai-service)任务清单"
|
||||
status: "completed"
|
||||
version: "0.5.0"
|
||||
last_updated: "2026-02-25"
|
||||
status: "in-progress"
|
||||
version: "0.6.0"
|
||||
last_updated: "2026-02-27"
|
||||
---
|
||||
|
||||
# Python AI 中台任务清单(AISVC)
|
||||
|
|
@ -83,7 +83,7 @@ last_updated: "2026-02-25"
|
|||
|
||||
## 5. 完成总结
|
||||
|
||||
**Phase 1-9 已全部完成**
|
||||
**Phase 1-10 已全部完成,Phase 12 部分完成**
|
||||
|
||||
| Phase | 描述 | 任务数 | 状态 |
|
||||
|-------|------|--------|------|
|
||||
|
|
@ -96,8 +96,14 @@ last_updated: "2026-02-25"
|
|||
| Phase 7 | 嵌入模型可插拔与文档解析 | 21 | ✅ 完成 |
|
||||
| Phase 8 | LLM 配置与 RAG 调试输出 | 10 | ✅ 完成 |
|
||||
| Phase 9 | 租户管理与 RAG 优化 | 10 | ✅ 完成 |
|
||||
| Phase 10 | Prompt 模板化 | 10 | 🔄 进行中 (8/10) |
|
||||
| Phase 11 | 多知识库管理 | 8 | ⏳ 待处理 |
|
||||
| Phase 12 | 意图识别与规则引擎 | 7 | 🔄 进行中 (5/7) |
|
||||
| Phase 13 | 话术流程引擎 | 7 | ⏳ 待处理 |
|
||||
| Phase 14 | 输出护栏 | 8 | ⏳ 待处理 |
|
||||
| Phase 15 | 智能 RAG 增强与编排升级 | 8 | ⏳ 待处理 |
|
||||
|
||||
**已完成: 73 个任务**
|
||||
**已完成: 86 个任务**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -151,3 +157,93 @@ last_updated: "2026-02-25"
|
|||
- [x] T9.8 实现 `NomicEmbeddingProvider`:支持多维度向量 `[AC-AISVC-29]` ✅
|
||||
- [x] T9.9 实现多向量存储:支持 full/256/512 三种维度 `[AC-AISVC-16]` ✅
|
||||
- [x] T9.10 实现 `KnowledgeIndexer`:优化的知识库索引服务 `[AC-AISVC-22]` ✅
|
||||
|
||||
---
|
||||
|
||||
### Phase 10: Prompt 模板化(v0.6.0 迭代)
|
||||
|
||||
> 目标:将硬编码的 SYSTEM_PROMPT 改为数据库驱动的模板系统,支持按租户配置人设、版本管理、热更新。
|
||||
|
||||
- [x] T10.1 定义 `PromptTemplate` 和 `PromptTemplateVersion` SQLModel 实体,创建数据库表 `[AC-AISVC-51, AC-AISVC-52]` ✅
|
||||
- [x] T10.2 实现 `PromptTemplateService`:模板 CRUD(创建、查询列表、查询详情、更新) `[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58]` ✅
|
||||
- [x] T10.3 实现模板版本管理:更新时自动创建新版本,保留历史版本 `[AC-AISVC-53]` ✅
|
||||
- [x] T10.4 实现模板发布与回滚:`publish` 将指定版本标记为已发布,`rollback` 恢复历史版本 `[AC-AISVC-54, AC-AISVC-55]` ✅
|
||||
- [x] T10.5 实现 `VariableResolver`:内置变量注入(persona_name/current_time/channel_type 等)+ 自定义变量替换 `[AC-AISVC-56]` ✅
|
||||
- [x] T10.6 实现模板缓存:内存缓存 + TTL 过期 + 发布/回滚时主动失效,fallback 到硬编码 SYSTEM_PROMPT `[AC-AISVC-51]` ✅
|
||||
- [x] T10.7 实现 Prompt 模板管理 API:`POST/GET/PUT /admin/prompt-templates`,`GET /admin/prompt-templates/{tplId}` `[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58]` ✅
|
||||
- [x] T10.8 实现 Prompt 模板发布/回滚 API:`POST /admin/prompt-templates/{tplId}/publish`,`POST /admin/prompt-templates/{tplId}/rollback` `[AC-AISVC-54, AC-AISVC-55]` ✅
|
||||
- [ ] T10.9 修改 Orchestrator 的 `_build_llm_messages()`:从模板服务加载系统指令替代硬编码 `[AC-AISVC-56]`
|
||||
- [ ] T10.10 编写 Prompt 模板服务单元测试 `[AC-AISVC-51~AC-AISVC-58]`
|
||||
|
||||
---
|
||||
|
||||
### Phase 11: 多知识库管理(v0.6.0 迭代)
|
||||
|
||||
> 目标:从单个 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]`
|
||||
- [ ] 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]`
|
||||
|
||||
---
|
||||
|
||||
### Phase 12: 意图识别与规则引擎(v0.6.0 迭代)
|
||||
|
||||
> 目标:实现基于关键词+正则的意图识别,根据意图路由到不同处理链路。
|
||||
|
||||
- [x] T12.1 定义 `IntentRule` SQLModel 实体,创建数据库表 `[AC-AISVC-65]` ✅
|
||||
- [x] T12.2 实现 `IntentRuleService`:规则 CRUD + 命中统计更新 `[AC-AISVC-65, AC-AISVC-66, AC-AISVC-67, AC-AISVC-68]` ✅
|
||||
- [x] T12.3 实现 `IntentRouter`:按优先级遍历规则,关键词匹配 + 正则匹配,返回 `IntentMatchResult` `[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-66, AC-AISVC-67, AC-AISVC-68]` ✅
|
||||
- [ ] T12.6 在 Orchestrator 中集成 IntentRouter:RAG 检索前执行意图识别,按 response_type 路由 `[AC-AISVC-69, AC-AISVC-70]`
|
||||
- [ ] T12.7 编写意图识别服务单元测试 `[AC-AISVC-65~AC-AISVC-70]`
|
||||
|
||||
---
|
||||
|
||||
### Phase 13: 话术流程引擎(v0.6.0 迭代)
|
||||
|
||||
> 目标:实现固定话术步骤的状态机引擎,支持多轮引导对话。
|
||||
|
||||
- [ ] T13.1 定义 `ScriptFlow` 和 `FlowInstance` SQLModel 实体,创建数据库表 `[AC-AISVC-71, AC-AISVC-74]`
|
||||
- [ ] T13.2 实现 `ScriptFlowService`:流程定义 CRUD `[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73]`
|
||||
- [ ] T13.3 实现 `FlowEngine.check_active_flow()`:检查会话是否有活跃流程实例 `[AC-AISVC-75]`
|
||||
- [ ] T13.4 实现 `FlowEngine.start()`:创建流程实例,返回第一步话术 `[AC-AISVC-74]`
|
||||
- [ ] T13.5 实现 `FlowEngine.advance()`:根据用户输入匹配条件,推进步骤或重复当前步骤 `[AC-AISVC-75, AC-AISVC-76]`
|
||||
- [ ] T13.6 实现话术流程管理 API:`POST/GET/PUT /admin/script-flows` `[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73]`
|
||||
- [ ] T13.7 编写话术流程引擎单元测试 `[AC-AISVC-71~AC-AISVC-77]`
|
||||
|
||||
---
|
||||
|
||||
### Phase 14: 输出护栏(v0.6.0 迭代)
|
||||
|
||||
> 目标:实现禁词检测与内容过滤,保障 AI 输出合规可控。
|
||||
|
||||
- [ ] T14.1 定义 `ForbiddenWord` 和 `BehaviorRule` SQLModel 实体,创建数据库表 `[AC-AISVC-78, AC-AISVC-84]`
|
||||
- [ ] T14.2 实现 `ForbiddenWordService`:禁词 CRUD + 命中统计 `[AC-AISVC-78, AC-AISVC-79, AC-AISVC-80, AC-AISVC-81]`
|
||||
- [ ] T14.3 实现 `BehaviorRuleService`:行为规则 CRUD `[AC-AISVC-84, AC-AISVC-85]`
|
||||
- [ ] T14.4 实现 `InputScanner`:用户输入前置禁词检测(仅记录,不阻断) `[AC-AISVC-83]`
|
||||
- [ ] T14.5 实现 `OutputFilter`:LLM 输出后置过滤(mask/replace/block 三种策略) `[AC-AISVC-82]`
|
||||
- [ ] T14.6 实现 Streaming 模式下的滑动窗口禁词检测 `[AC-AISVC-82]`
|
||||
- [ ] T14.7 实现护栏管理 API:`POST/GET/PUT/DELETE /admin/guardrails/forbidden-words`,`POST/GET /admin/guardrails/behavior-rules` `[AC-AISVC-78~AC-AISVC-85]`
|
||||
- [ ] T14.8 编写输出护栏服务单元测试 `[AC-AISVC-78~AC-AISVC-85]`
|
||||
|
||||
---
|
||||
|
||||
### Phase 15: 智能 RAG 增强与编排升级(v0.6.0 迭代)
|
||||
|
||||
> 目标:Query 改写、分层排序、行为规则注入、Orchestrator 升级为 12 步 pipeline。
|
||||
|
||||
- [ ] T15.1 实现 `QueryRewriter`:基于 LLM 的 Query 改写(解析指代词、补全语义),支持配置开关 `[AC-AISVC-86]`
|
||||
- [ ] T15.2 实现 `ResultRanker`:按知识库类型优先级 + 自定义权重 + 相似度分数的多维排序 `[AC-AISVC-87]`
|
||||
- [ ] T15.3 实现话术模板优先匹配:script 类型高分命中时优先作为回复参考 `[AC-AISVC-88]`
|
||||
- [ ] T15.4 实现行为规则注入到 Prompt:从 BehaviorRuleService 加载规则,追加到系统指令末尾 `[AC-AISVC-84]`
|
||||
- [ ] T15.5 升级 Orchestrator:整合 InputScanner → FlowEngine → IntentRouter → QueryRewriter → 多知识库检索 → ResultRanker → PromptBuilder → LLM → OutputFilter 完整 12 步 pipeline `[AC-AISVC-69, AC-AISVC-64, AC-AISVC-82, AC-AISVC-86, AC-AISVC-87]`
|
||||
- [ ] T15.6 预留 `AudioParser` 和 `VideoParser` 扩展点(仅接口定义,不实现) `[AC-AISVC-89]`
|
||||
- [ ] T15.7 预留对话记录结构化导入接口(仅接口定义,不实现) `[AC-AISVC-90]`
|
||||
- [ ] T15.8 编写 Orchestrator 升级集成测试:验证意图路由 → 流程引擎 → 多知识库检索 → 护栏过滤的完整链路 `[AC-AISVC-51~AC-AISVC-90]`
|
||||
|
|
|
|||
Loading…
Reference in New Issue