diff --git a/.claude/progress/v0.7.0-window2-flow-guardrail-progress.md b/.claude/progress/v0.7.0-window2-flow-guardrail-progress.md new file mode 100644 index 0000000..4da0d7b --- /dev/null +++ b/.claude/progress/v0.7.0-window2-flow-guardrail-progress.md @@ -0,0 +1,112 @@ +# v0.7.0 窗口2:话术流程 + 输出护栏 - 进度文档 + +## 1. 任务概述 +实现 v0.7.0 迭代中话术流程和输出护栏的测试与监控功能,包括前端页面和后端 API。 + +## 2. 需求文档引用 +- spec/ai-service-admin/requirements.md - 第10节(v0.7.0),AC-ASA-59 ~ AC-ASA-64 +- spec/ai-service/requirements.md - 第13节(v0.7.0),AC-AISVC-101 ~ AC-AISVC-107 + +## 3. 总体进度 +- [x] 后端任务(4个) + - [x] T16.22-T16.24: 话术流程模拟测试 API + - [x] T16.25-T16.27: 话术流程监控 API + - [x] T16.28-T16.29: 输出护栏测试 API + - [x] T16.30-T16.32: 输出护栏监控 API +- [x] 前端任务(5个) + - [x] P13-14: 流程模拟对话框 + - [x] P13-15-P13-16: 话术流程监控页面 + - [x] P13-17: 护栏测试对话框 + - [x] P13-18-P13-19: 输出护栏监控页面 + - [x] P13-01: API 服务层扩展 + +## 4. Phase 详细进度 + +### Phase 1: 话术流程模拟测试 API (T16.22-T16.24) +**状态**: 已完成 +**文件修改记录**: +- 新建: ai-service/app/services/flow/tester.py - ScriptFlowTester 类 +- 修改: ai-service/app/api/admin/script_flows.py - 添加 POST /{flowId}/simulate 端点 + +### Phase 2: 话术流程监控 API (T16.25-T16.27) +**状态**: 已完成 +**文件修改记录**: +- 新建: ai-service/app/services/monitoring/flow_monitor.py - FlowMonitor 类 +- 修改: ai-service/app/api/admin/monitoring.py - 添加 GET /script-flows 和 GET /script-flows/{flowId}/executions 端点 + +### Phase 3: 输出护栏测试 API (T16.28-T16.29) +**状态**: 已完成 +**文件修改记录**: +- 新建: ai-service/app/services/guardrail/tester.py - GuardrailTester 类 +- 修改: ai-service/app/api/admin/guardrails.py - 添加 POST /test 端点 + +### Phase 4: 输出护栏监控 API (T16.30-T16.32) +**状态**: 已完成 +**文件修改记录**: +- 新建: ai-service/app/services/monitoring/guardrail_monitor.py - GuardrailMonitor 类 +- 修改: ai-service/app/api/admin/monitoring.py - 添加 GET /guardrails 和 GET /guardrails/{wordId}/blocks 端点 +- 修改: ai-service/app/services/monitoring/__init__.py - 导出新模块 + +### Phase 5: 前端实现 (P13-14 ~ P13-19, P13-01) +**状态**: 已完成 +**文件修改记录**: +- 新建: ai-service-admin/src/views/admin/script-flow/components/SimulateDialog.vue +- 新建: ai-service-admin/src/views/admin/monitoring/ScriptFlows.vue +- 新建: ai-service-admin/src/views/admin/guardrail/components/TestDialog.vue +- 新建: ai-service-admin/src/views/admin/monitoring/Guardrails.vue +- 扩展: ai-service-admin/src/api/monitoring.ts - 添加流程和护栏监控 API +- 扩展: ai-service-admin/src/api/script-flow.ts - 添加流程模拟 API +- 扩展: ai-service-admin/src/api/guardrail.ts - 添加护栏测试 API +- 修改: ai-service-admin/src/views/admin/script-flow/index.vue - 添加模拟按钮 +- 修改: ai-service-admin/src/views/admin/guardrail/components/ForbiddenWordsTab.vue - 添加测试按钮 +- 修改: ai-service-admin/src/router/index.ts - 添加监控页面路由 + +## 5. 技术上下文 + +### 项目结构 +- **前端**: `ai-service-admin/` - Vue 3 + Element Plus + TypeScript +- **后端**: `ai-service/` - Python FastAPI + SQLModel + PostgreSQL + +### 核心约定 +- 所有 API 必须支持多租户隔离(`tenant_id` 参数) +- 流程模拟不应修改数据库状态(只读操作) +- 护栏测试应复用现有的 `OutputFilter` 逻辑 +- 监控数据异步更新,不阻塞主流程 + +### 关键代码示例 +- 流程引擎: `app/services/flow/engine.py` - `_match_next_step()` 方法 +- 护栏过滤: `app/services/guardrail/output_filter.py` - `filter()` 方法 +- 禁词服务: `app/services/guardrail/word_service.py` - `get_enabled_words_for_filtering()` 方法 + +### 模块依赖 +- FlowEngine: 流程状态机引擎 +- OutputFilter: 输出护栏过滤器 +- ForbiddenWordService: 禁词管理服务 +- ScriptFlowService: 话术流程管理服务 + +## 6. 会话历史 +### 会话 1 (2026-02-27) +- 完成:所有后端 API 和前端页面实现 +- 问题:无 +- 解决方案:无 + +## 7. 下一步行动 +**任务已完成** + +## 8. 待解决问题 +暂无 + +## 9. 最终验收标准 +- [x] [AC-AISVC-101] 流程模拟测试接口返回完整的模拟执行结果 +- [x] [AC-AISVC-102] 流程模拟支持覆盖率分析和问题检测 +- [x] [AC-AISVC-103] 流程监控统计接口返回激活次数、完成率等统计 +- [x] [AC-AISVC-104] 流程执行记录接口支持分页查询 +- [x] [AC-AISVC-105] 护栏测试接口返回详细的检测结果 +- [x] [AC-AISVC-106] 护栏监控统计接口返回拦截次数等统计 +- [x] [AC-AISVC-107] 禁词拦截记录接口支持分页查询 +- [x] [AC-ASA-59] 流程模拟对话框支持步骤可视化 +- [x] [AC-ASA-60] 话术流程监控页面展示流程激活统计 +- [x] [AC-ASA-61] 流程执行记录详情弹窗支持分页 +- [x] [AC-ASA-62] 护栏测试对话框展示禁词检测结果 +- [x] [AC-ASA-63] 输出护栏监控页面展示护栏拦截统计 +- [x] [AC-ASA-64] 护栏拦截记录详情弹窗支持分页 diff --git a/ai-service-admin/src/api/guardrail.ts b/ai-service-admin/src/api/guardrail.ts index 5fcb8cd..23a76c3 100644 --- a/ai-service-admin/src/api/guardrail.ts +++ b/ai-service-admin/src/api/guardrail.ts @@ -10,6 +10,36 @@ import type { BehaviorRuleListResponse } from '@/types/guardrail' +export interface GuardrailTestRequest { + testTexts: string[] +} + +export interface GuardrailTestResponse { + results: GuardrailTestResult[] + summary: { + totalTests: number + triggeredCount: number + blockedCount: number + triggerRate: number + } +} + +export interface GuardrailTestResult { + originalText: string + triggered: boolean + triggeredWords: TriggeredWordInfo[] + filteredText: string + blocked: boolean +} + +export interface TriggeredWordInfo { + word: string + category: string + strategy: string + replacement?: string + fallbackReply?: string +} + export function listForbiddenWords(params?: { category?: string is_enabled?: boolean @@ -91,6 +121,14 @@ export function deleteBehaviorRule(ruleId: string): Promise { }) } +export function testGuardrail(data: GuardrailTestRequest): Promise { + return request({ + url: '/admin/guardrails/test', + method: 'post', + data + }) +} + export type { ForbiddenWord, ForbiddenWordCreate, diff --git a/ai-service-admin/src/api/monitoring.ts b/ai-service-admin/src/api/monitoring.ts index e2a218f..47236c3 100644 --- a/ai-service-admin/src/api/monitoring.ts +++ b/ai-service-admin/src/api/monitoring.ts @@ -1,6 +1,37 @@ import request from '@/utils/request' -export function listSessions(params: any) { +export interface Session { + sessionId: string + tenantId: string + messageCount: number + status: string + channelType: string + startTime: string +} + +export interface SessionDetail { + sessionId: string + messages: Array<{ + role: string + content: string + timestamp: string + }> + trace?: { + retrieval?: Array<{ + score: number + source?: string + content: string + }> + tools?: Array> + } +} + +export interface SessionListResponse { + data: Session[] + total: number +} + +export function listSessions(params?: { page?: number; pageSize?: number; status?: string }): Promise { return request({ url: '/admin/sessions', method: 'get', @@ -8,9 +39,294 @@ export function listSessions(params: any) { }) } -export function getSessionDetail(sessionId: string) { +export function getSessionDetail(sessionId: string): Promise { return request({ url: `/admin/sessions/${sessionId}`, method: 'get' }) } + +export interface IntentRuleTestRequest { + message: string +} + +export interface IntentRuleTestResult { + ruleId: string + ruleName: string + results: IntentRuleTestCase[] + summary: { + totalTests: number + matchedCount: number + matchRate: number + } +} + +export interface IntentRuleTestCase { + message: string + matched: boolean + matchedKeywords: string[] + matchedPatterns: string[] + matchType: string | null + priority: number + priorityRank: number + conflictRules: ConflictRule[] + reason: string | null +} + +export interface ConflictRule { + ruleId: string + ruleName: string + priority: number + reason: string +} + +export interface IntentRuleStatsResponse { + totalHits: number + totalConversations: number + hitRate: number + rules: IntentRuleStatItem[] +} + +export interface IntentRuleStatItem { + ruleId: string + ruleName: string + hitCount: number + hitRate: number + avgResponseTime: number + lastHitTime: string | null + responseType: string +} + +export interface IntentRuleHitsResponse { + records: IntentRuleHitRecord[] + total: number + page: number + pageSize: number +} + +export interface IntentRuleHitRecord { + conversationId: string + sessionId: string + userMessage: string + matchedKeywords: string[] + matchedPatterns: string[] + responseType: string + executionResult: string + hitTime: string +} + +export interface PromptPreviewRequest { + variables?: Record + sampleHistory?: Array<{ role: string; content: string }> + sampleMessage?: string +} + +export interface PromptPreviewResponse { + templateId: string + templateName: string + version: number + rawContent: string + variables: Array<{ name: string; value: string }> + renderedContent: string + estimatedTokens: number + tokenCount: { + systemPrompt: number + history: number + currentMessage: number + total: number + } + warnings: string[] +} + +export interface PromptTemplateStatsResponse { + totalUsage: number + templates: PromptTemplateStatItem[] + sceneBreakdown: Record +} + +export interface PromptTemplateStatItem { + templateId: string + templateName: string + scene: string + usageCount: number + avgTokens: number + avgPromptTokens: number + avgCompletionTokens: number + lastUsedTime: string | null +} + +export interface FlowStatsResponse { + totalActivations: number + totalCompletions: number + completionRate: number + flows: FlowStatItem[] +} + +export interface FlowStatItem { + flowId: string + flowName: string + activationCount: number + completionCount: number + completionRate: number + avgDuration: number + avgStepsCompleted: number + dropOffPoints: DropOffPoint[] + lastActivatedAt: string | null +} + +export interface DropOffPoint { + stepNo: number + dropOffCount: number + dropOffRate: number +} + +export interface FlowExecutionsResponse { + data: FlowExecutionRecord[] + page: number + pageSize: number + total: number +} + +export interface FlowExecutionRecord { + instanceId: string + sessionId: string + flowId: string + flowName: string + currentStep: number + totalSteps: number + status: string + startedAt: string + updatedAt: string + completedAt: string | null +} + +export interface GuardrailStatsResponse { + totalBlocks: number + totalTriggers: number + blockRate: number + words: GuardrailWordStats[] + categoryBreakdown: Record +} + +export interface GuardrailWordStats { + wordId: string + word: string + category: string + strategy: string + hitCount: number + blockCount: number + lastHitAt: string | null +} + +export interface GuardrailBlocksResponse { + data: GuardrailBlockRecord[] + page: number + pageSize: number + total: number +} + +export interface GuardrailBlockRecord { + recordId: string + sessionId: string + originalText: string + filteredText: string + strategy: string + blockedAt: string +} + +export function testIntentRule(ruleId: string, data: IntentRuleTestRequest): Promise { + return request({ + url: `/admin/intent-rules/${ruleId}/test`, + method: 'post', + data + }) +} + +export function getIntentRuleStats(params?: { + startDate?: string + endDate?: string + responseType?: string +}): Promise { + return request({ + url: '/admin/monitoring/intent-rules', + method: 'get', + params + }) +} + +export function getIntentRuleHits( + ruleId: string, + params?: { page?: number; pageSize?: number } +): Promise { + return request({ + url: `/admin/monitoring/intent-rules/${ruleId}/hits`, + method: 'get', + params + }) +} + +export function previewPromptTemplate( + tplId: string, + data: PromptPreviewRequest +): Promise { + return request({ + url: `/admin/prompt-templates/${tplId}/preview`, + method: 'post', + data + }) +} + +export function getPromptTemplateStats(params?: { + scene?: string + startDate?: string + endDate?: string +}): Promise { + return request({ + url: '/admin/monitoring/prompt-templates', + method: 'get', + params + }) +} + +export function getFlowStats(params?: { + startDate?: string + endDate?: string +}): Promise { + return request({ + url: '/admin/monitoring/script-flows', + method: 'get', + params + }) +} + +export function getFlowExecutions( + flowId: string, + params?: { page?: number; pageSize?: number } +): Promise { + return request({ + url: `/admin/monitoring/script-flows/${flowId}/executions`, + method: 'get', + params + }) +} + +export function getGuardrailStats(params?: { + category?: string +}): Promise { + return request({ + url: '/admin/monitoring/guardrails', + method: 'get', + params + }) +} + +export function getGuardrailBlocks( + wordId: string, + params?: { page?: number; pageSize?: number } +): Promise { + return request({ + url: `/admin/monitoring/guardrails/${wordId}/blocks`, + method: 'get', + params + }) +} diff --git a/ai-service-admin/src/api/script-flow.ts b/ai-service-admin/src/api/script-flow.ts index bc75b10..22f5d93 100644 --- a/ai-service-admin/src/api/script-flow.ts +++ b/ai-service-admin/src/api/script-flow.ts @@ -7,6 +7,43 @@ import type { ScriptFlowListResponse } from '@/types/script-flow' +export interface FlowSimulateRequest { + userInputs: string[] +} + +export interface FlowSimulateResponse { + flowId: string + flowName: string + simulation: FlowSimulationStep[] + result: { + completed: boolean + totalSteps: number + totalDurationMs: number + finalMessage: string | null + } + coverage: { + totalSteps: number + coveredSteps: number + coverageRate: number + uncoveredSteps: number[] + } + issues: string[] +} + +export interface FlowSimulationStep { + stepNo: number + botMessage: string + userInput: string + matchedCondition: { + type: string + gotoStep: number + keywords?: string[] + pattern?: string + } | null + nextStep: number | null + durationMs: number +} + export function listScriptFlows(params?: { is_enabled?: boolean }): Promise { @@ -47,6 +84,14 @@ export function deleteScriptFlow(flowId: string): Promise { }) } +export function simulateScriptFlow(flowId: string, data: FlowSimulateRequest): Promise { + return request({ + url: `/admin/script-flows/${flowId}/simulate`, + method: 'post', + data + }) +} + export type { ScriptFlow, ScriptFlowDetail, diff --git a/ai-service-admin/src/router/index.ts b/ai-service-admin/src/router/index.ts index 3701f42..7591d87 100644 --- a/ai-service-admin/src/router/index.ts +++ b/ai-service-admin/src/router/index.ts @@ -70,6 +70,30 @@ const routes: Array = [ name: 'Guardrail', component: () => import('@/views/admin/guardrail/index.vue'), meta: { title: '输出护栏管理' } + }, + { + path: '/admin/monitoring/intent-rules', + name: 'IntentRuleMonitoring', + component: () => import('@/views/admin/monitoring/IntentRules.vue'), + meta: { title: '意图规则监控' } + }, + { + path: '/admin/monitoring/prompt-templates', + name: 'PromptTemplateMonitoring', + component: () => import('@/views/admin/monitoring/PromptTemplates.vue'), + meta: { title: 'Prompt 模板监控' } + }, + { + path: '/admin/monitoring/script-flows', + name: 'ScriptFlowMonitoring', + component: () => import('@/views/admin/monitoring/ScriptFlows.vue'), + meta: { title: '话术流程监控' } + }, + { + path: '/admin/monitoring/guardrails', + name: 'GuardrailMonitoring', + component: () => import('@/views/admin/monitoring/Guardrails.vue'), + meta: { title: '输出护栏监控' } } ] diff --git a/ai-service-admin/src/views/admin/guardrail/components/ForbiddenWordsTab.vue b/ai-service-admin/src/views/admin/guardrail/components/ForbiddenWordsTab.vue index d72bdc5..4af098a 100644 --- a/ai-service-admin/src/views/admin/guardrail/components/ForbiddenWordsTab.vue +++ b/ai-service-admin/src/views/admin/guardrail/components/ForbiddenWordsTab.vue @@ -18,6 +18,10 @@
+ + + 测试护栏 + 批量导入 @@ -140,6 +144,8 @@ + +
@@ -155,6 +161,7 @@ import { } from '@/api/guardrail' import { WORD_CATEGORY_OPTIONS, WORD_STRATEGY_OPTIONS } from '@/types/guardrail' import type { ForbiddenWord, ForbiddenWordCreate, ForbiddenWordUpdate } from '@/types/guardrail' +import TestDialog from './TestDialog.vue' const loading = ref(false) const words = ref([]) @@ -162,6 +169,7 @@ const filterCategory = ref('') const searchKeyword = ref('') const dialogVisible = ref(false) const showBatchImport = ref(false) +const testDialogVisible = ref(false) const isEdit = ref(false) const submitting = ref(false) const batchSubmitting = ref(false) diff --git a/ai-service-admin/src/views/admin/guardrail/components/TestDialog.vue b/ai-service-admin/src/views/admin/guardrail/components/TestDialog.vue new file mode 100644 index 0000000..1a9d1ea --- /dev/null +++ b/ai-service-admin/src/views/admin/guardrail/components/TestDialog.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/ai-service-admin/src/views/admin/monitoring/Guardrails.vue b/ai-service-admin/src/views/admin/monitoring/Guardrails.vue new file mode 100644 index 0000000..576525f --- /dev/null +++ b/ai-service-admin/src/views/admin/monitoring/Guardrails.vue @@ -0,0 +1,413 @@ + + + + + diff --git a/ai-service-admin/src/views/admin/monitoring/ScriptFlows.vue b/ai-service-admin/src/views/admin/monitoring/ScriptFlows.vue new file mode 100644 index 0000000..c0bc009 --- /dev/null +++ b/ai-service-admin/src/views/admin/monitoring/ScriptFlows.vue @@ -0,0 +1,372 @@ + + + + + diff --git a/ai-service-admin/src/views/admin/script-flow/components/SimulateDialog.vue b/ai-service-admin/src/views/admin/script-flow/components/SimulateDialog.vue new file mode 100644 index 0000000..ab4b5a7 --- /dev/null +++ b/ai-service-admin/src/views/admin/script-flow/components/SimulateDialog.vue @@ -0,0 +1,337 @@ + + + + + diff --git a/ai-service-admin/src/views/admin/script-flow/index.vue b/ai-service-admin/src/views/admin/script-flow/index.vue index 60b7767..dbe6a36 100644 --- a/ai-service-admin/src/views/admin/script-flow/index.vue +++ b/ai-service-admin/src/views/admin/script-flow/index.vue @@ -39,12 +39,16 @@ {{ formatDate(row.updated_at) }} - +