From 56c8f59d1330b5ad9100a1798c1b98b75d493edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=82?= Date: Wed, 8 Jul 2026 21:49:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-analysis):=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E5=88=86=E6=9E=90=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/src/modules/ai/ai.controller.ts | 25 ++++- apps/server/src/modules/ai/ai.module.ts | 16 ++- .../modules/ai/analysis/analysis-strategy.ts | 53 +++++++++ .../business-analysis.service.spec.ts | 88 +++++++++++++++ .../ai/analysis/business-analysis.service.ts | 104 ++++++++++++++++++ .../modules/ai/analysis/chart-spec-builder.ts | 35 ++++++ .../modules/ai/analysis/follow-up-builder.ts | 17 +++ .../src/modules/ai/analysis/insight-engine.ts | 23 ++++ .../src/modules/ai/analysis/report-builder.ts | 42 +++++++ .../server/src/modules/ai/dto/analysis.dto.ts | 34 ++++++ .../src/modules/ai/prompts/analysis-plan.ts | 103 +++++++++++++++++ .../src/modules/ai/prompts/analysis-report.ts | 22 ++++ 12 files changed, 558 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/modules/ai/analysis/analysis-strategy.ts create mode 100644 apps/server/src/modules/ai/analysis/business-analysis.service.spec.ts create mode 100644 apps/server/src/modules/ai/analysis/business-analysis.service.ts create mode 100644 apps/server/src/modules/ai/analysis/chart-spec-builder.ts create mode 100644 apps/server/src/modules/ai/analysis/follow-up-builder.ts create mode 100644 apps/server/src/modules/ai/analysis/insight-engine.ts create mode 100644 apps/server/src/modules/ai/analysis/report-builder.ts create mode 100644 apps/server/src/modules/ai/dto/analysis.dto.ts create mode 100644 apps/server/src/modules/ai/prompts/analysis-plan.ts create mode 100644 apps/server/src/modules/ai/prompts/analysis-report.ts diff --git a/apps/server/src/modules/ai/ai.controller.ts b/apps/server/src/modules/ai/ai.controller.ts index 4f43c7a..336f115 100644 --- a/apps/server/src/modules/ai/ai.controller.ts +++ b/apps/server/src/modules/ai/ai.controller.ts @@ -1,5 +1,9 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Headers, Post } from '@nestjs/common'; +import { CurrentUser } from '../../common/auth/current-user.decorator'; +import type { CurrentUser as ResolvedCurrentUser } from '../../common/auth/auth-context.service'; import { AiService } from './ai.service'; +import { BusinessAnalysisService } from './analysis/business-analysis.service'; +import { AnalysisDto } from './dto/analysis.dto'; import { DecomposeDto } from './dto/decompose.dto'; import { RiskInterpretDto } from './dto/risk-interpret.dto'; import type { @@ -7,11 +11,15 @@ import type { AgentDecomposeError, AgentRiskInterpretResponse, AgentRiskInterpretError, + AnalysisResponse, } from '@ftb/shared'; @Controller('ai') export class AiController { - constructor(private readonly aiService: AiService) {} + constructor( + private readonly aiService: AiService, + private readonly businessAnalysisService: BusinessAnalysisService, + ) {} @Post('decompose') async decompose(@Body() dto: DecomposeDto): Promise { @@ -22,4 +30,17 @@ export class AiController { async interpretRisk(@Body() dto: RiskInterpretDto): Promise { return this.aiService.interpretRisk(dto); } + + @Post('analysis') + async analyze( + @Body() dto: AnalysisDto, + @CurrentUser() user: ResolvedCurrentUser | null, + @Headers('x-ftb-user-id') headerUserId?: string, + @Headers('x-user-id') legacyHeaderUserId?: string, + ): Promise { + return this.businessAnalysisService.analyze(dto, { + id: user?.id ?? headerUserId ?? legacyHeaderUserId ?? '', + permissions: dto.permissions ?? [], + }); + } } diff --git a/apps/server/src/modules/ai/ai.module.ts b/apps/server/src/modules/ai/ai.module.ts index a1f2a79..b0e333f 100644 --- a/apps/server/src/modules/ai/ai.module.ts +++ b/apps/server/src/modules/ai/ai.module.ts @@ -3,11 +3,23 @@ import { AiController } from './ai.controller'; import { AiService } from './ai.service'; import { AiGatewayService } from './ai-gateway.service'; import { ConfigModule } from '../config/config.module'; +import { CommonDomainModule } from '../../common/common-domain.module'; +import { BusinessAnalysisService } from './analysis/business-analysis.service'; +import { MetricEngine } from './analysis/metric-engine'; +import { PermissionScopeResolver } from './analysis/permission-scope-resolver'; +import { AnalysisReportBuilder } from './analysis/report-builder'; @Module({ - imports: [ConfigModule], + imports: [ConfigModule, CommonDomainModule], controllers: [AiController], - providers: [AiService, AiGatewayService], + providers: [ + AiService, + AiGatewayService, + BusinessAnalysisService, + PermissionScopeResolver, + MetricEngine, + AnalysisReportBuilder, + ], exports: [AiService], }) export class AiModule {} diff --git a/apps/server/src/modules/ai/analysis/analysis-strategy.ts b/apps/server/src/modules/ai/analysis/analysis-strategy.ts new file mode 100644 index 0000000..ff6076c --- /dev/null +++ b/apps/server/src/modules/ai/analysis/analysis-strategy.ts @@ -0,0 +1,53 @@ +import type { AnalysisPlan, AnalysisRequest, DimensionId } from '@ftb/shared'; +import { parseSemanticIntent } from './analysis-semantic-layer'; +import type { SemanticIntent } from './analysis-semantic-layer'; +import { normalizeAnalysisPlan } from './analysis-plan-processor'; +import { getMetricDefinition } from './metric-catalog'; + +export function createAnalysisPlanFromQuestion( + request: AnalysisRequest, + scope: AnalysisPlan['scope'], + now = new Date(), +): { semantic: SemanticIntent; plan: AnalysisPlan } { + const semantic = parseSemanticIntent(request.question); + const dimensions = inferDimensions(request.question, semantic.metricId); + const plan = normalizeAnalysisPlan( + { + metricId: semantic.metricId, + analysisType: semantic.analysisType, + dimensions, + scope, + filters: {}, + }, + now, + ); + return { semantic, plan }; +} + +function inferDimensions( + question: string, + metricId: AnalysisPlan['metricRef']['metricId'], +): DimensionId[] { + const metric = getMetricDefinition(metricId); + const preferred = inferPreferredDimension(question, metricId); + if (preferred && metric?.supportedDimensions.includes(preferred)) return [preferred]; + if (metric?.defaultDimension) return [metric.defaultDimension]; + return []; +} + +function inferPreferredDimension( + question: string, + metricId: AnalysisPlan['metricRef']['metricId'], +): DimensionId | null { + if (/部门/.test(question)) return 'department'; + if (/成员|谁|负责人/.test(question)) return 'member'; + if (/产品/.test(question)) return 'product'; + if (/项目/.test(question)) return 'project'; + if (/月|月份/.test(question)) return 'month'; + if (/周/.test(question)) return 'week'; + if (metricId === 'version_risk_score') return 'version'; + if (metricId === 'bug_severity_count') return 'bug_severity'; + if (metricId === 'requirement_status_count') return 'requirement_status'; + if (metricId === 'requirement_source_count') return 'requirement_source'; + return null; +} diff --git a/apps/server/src/modules/ai/analysis/business-analysis.service.spec.ts b/apps/server/src/modules/ai/analysis/business-analysis.service.spec.ts new file mode 100644 index 0000000..91e11fa --- /dev/null +++ b/apps/server/src/modules/ai/analysis/business-analysis.service.spec.ts @@ -0,0 +1,88 @@ +import { BusinessAnalysisService } from './business-analysis.service'; + +describe('BusinessAnalysisService', () => { + function makeService() { + const scopeResolver = { + resolveAnalysisScope: jest + .fn() + .mockResolvedValue({ type: 'version', versionId: 'ver-1' }), + }; + const metricEngine = { + executeMetric: jest.fn().mockResolvedValue({ + metricRef: { metricId: 'version_risk_score', version: 1 }, + analysisType: 'ranking', + columns: [{ id: 'label', label: '版本', type: 'string' }], + rows: [{ label: 'V1', value: 88 }], + evidence: [{ label: '风险版本', value: 1, sourceDomain: 'xiaobao' }], + dataScope: { type: 'version', versionId: 'ver-1' }, + generatedAt: '2026-07-08T12:00:00.000Z', + }), + }; + const reportBuilder = { + build: jest.fn().mockResolvedValue({ + summary: 'V1 风险较高。', + keyFindings: ['风险分 88。'], + evidence: [{ label: '风险版本', value: 1, sourceDomain: 'xiaobao' }], + suggestions: ['优先处理阻塞和严重 Bug。'], + dataScope: { + timeDescription: '当前状态', + permissionDescription: '当前版本', + metricFormulaDescription: '小宝风险分', + generatedAt: '2026-07-08T12:00:00.000Z', + }, + }), + }; + const service = new BusinessAnalysisService( + scopeResolver as any, + metricEngine as any, + reportBuilder as any, + ); + return { service, scopeResolver, metricEngine, reportBuilder }; + } + + it('returns insight, chart, report, evidence, and follow-ups', async () => { + const { service } = makeService(); + + const result = await service.analyze( + { + question: '这个版本风险怎么样', + context: { surface: 'version_detail', versionId: 'ver-1' }, + }, + { id: 'm-1', permissions: [] }, + new Date('2026-07-08T12:00:00.000Z'), + ); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.chart.kind).toBe('horizontal_bar'); + expect(result.insight.summary).toContain('V1'); + expect(result.report.summary).toContain('风险'); + expect( + result.followUps.some( + (item: { type: string }) => item.type === 'question', + ), + ).toBe(true); + } + }); + + it('returns NO_DATA when metric result has no rows', async () => { + const { service, metricEngine } = makeService(); + metricEngine.executeMetric.mockResolvedValueOnce({ + metricRef: { metricId: 'requirement_completion_count', version: 1 }, + analysisType: 'trend', + columns: [], + rows: [], + evidence: [{ label: '可统计记录', value: 0, sourceDomain: 'requirement' }], + dataScope: { type: 'self', userId: 'm-1' }, + generatedAt: '2026-07-08T12:00:00.000Z', + }); + + await expect( + service.analyze( + { question: '需求完成趋势' }, + { id: 'm-1', permissions: [] }, + new Date('2026-07-08T12:00:00.000Z'), + ), + ).resolves.toMatchObject({ ok: false, code: 'NO_DATA' }); + }); +}); diff --git a/apps/server/src/modules/ai/analysis/business-analysis.service.ts b/apps/server/src/modules/ai/analysis/business-analysis.service.ts new file mode 100644 index 0000000..c37d36b --- /dev/null +++ b/apps/server/src/modules/ai/analysis/business-analysis.service.ts @@ -0,0 +1,104 @@ +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import type { AnalysisRequest, AnalysisResponse } from '@ftb/shared'; +import { getMetricDefinition } from './metric-catalog'; +import { PermissionScopeResolver } from './permission-scope-resolver'; +import { MetricEngine } from './metric-engine'; +import { AnalysisReportBuilder } from './report-builder'; +import { createAnalysisPlanFromQuestion } from './analysis-strategy'; +import { validateAnalysisPlan } from './analysis-plan-processor'; +import { buildUnifiedChartSpec } from './chart-spec-builder'; +import { buildInsightCard } from './insight-engine'; +import { buildFollowUps } from './follow-up-builder'; + +export interface AnalysisActor { + id: string; + permissions: string[]; +} + +@Injectable() +export class BusinessAnalysisService { + constructor( + private readonly scopeResolver: PermissionScopeResolver, + private readonly metricEngine: MetricEngine, + private readonly reportBuilder: AnalysisReportBuilder, + ) {} + + async analyze( + request: AnalysisRequest, + actor: AnalysisActor, + now = new Date(), + ): Promise { + const question = request.question.trim(); + if (!question) { + return { ok: false, code: 'AMBIGUOUS_INTENT', message: '请输入要分析的问题。' }; + } + + let scope; + try { + scope = await this.scopeResolver.resolveAnalysisScope({ + actorId: actor.id, + permissions: actor.permissions, + context: request.context, + }); + } catch (error) { + if (error instanceof ForbiddenException) { + return { ok: false, code: 'NO_PERMISSION', message: '当前用户没有该范围的分析权限。' }; + } + throw error; + } + + const { semantic, plan } = createAnalysisPlanFromQuestion({ ...request, question }, scope, now); + if (semantic.semanticConfidence === 'low') { + return { + ok: false, + code: 'AMBIGUOUS_INTENT', + message: '这个问题有多种理解,请选择一个分析方向。', + clarificationOptions: [ + { label: '成员负载', prompt: '分析成员待办排行' }, + { label: '版本风险', prompt: '分析版本风险排行' }, + ], + dataScope: scope, + }; + } + + try { + validateAnalysisPlan(plan); + } catch (error) { + if (error instanceof BadRequestException) { + return { + ok: false, + code: 'INVALID_PLAN', + message: error.message, + dataScope: scope, + }; + } + throw error; + } + + const metric = getMetricDefinition(plan.metricRef.metricId, plan.metricRef.version); + if (!metric) { + return { + ok: false, + code: 'UNSUPPORTED_ANALYSIS', + message: '当前指标不在分析目录中。', + dataScope: scope, + }; + } + + const metricResult = await this.metricEngine.executeMetric(plan, now); + if (metricResult.rows.length === 0) { + return { + ok: false, + code: 'NO_DATA', + message: '当前范围没有可分析的数据。可以调整时间范围、切换维度或查看当前状态。', + dataScope: scope, + }; + } + + const insight = buildInsightCard(metricResult, metric, semantic.semanticConfidence); + const chart = buildUnifiedChartSpec(metricResult, metric); + const report = await this.reportBuilder.build(metricResult, insight, metric); + const followUps = buildFollowUps(metricResult, plan); + return { ok: true, plan, metricResult, insight, chart, report, followUps }; + } +} diff --git a/apps/server/src/modules/ai/analysis/chart-spec-builder.ts b/apps/server/src/modules/ai/analysis/chart-spec-builder.ts new file mode 100644 index 0000000..a5f2cd4 --- /dev/null +++ b/apps/server/src/modules/ai/analysis/chart-spec-builder.ts @@ -0,0 +1,35 @@ +import type { MetricDefinition, MetricResult, UnifiedChartSpec } from '@ftb/shared'; + +export function buildUnifiedChartSpec( + result: MetricResult, + metric: MetricDefinition, +): UnifiedChartSpec { + const labelColumn = result.columns.find((column) => column.type === 'string'); + const valueColumn = result.columns.find( + (column) => column.type === 'number' || column.type === 'percent', + ); + const labelField = labelColumn?.id ?? 'label'; + const valueField = valueColumn?.id ?? 'value'; + const isTrend = metric.defaultChart === 'line_area'; + + return { + kind: metric.defaultChart, + title: metric.name, + subtitle: metric.description, + dataset: { + source: result.rows, + label: labelField, + value: valueField, + x: isTrend ? labelField : undefined, + y: isTrend ? valueField : undefined, + }, + encoding: { + x: isTrend ? { field: labelField, label: labelColumn?.label ?? labelField } : undefined, + y: isTrend ? { field: valueField, label: valueColumn?.label ?? valueField } : undefined, + value: { field: valueField, label: valueColumn?.label ?? valueField }, + color: { mode: result.metricRef.metricId === 'version_risk_score' ? 'risk' : 'single' }, + }, + annotations: [], + stylePreset: 'apple_vision_light', + }; +} diff --git a/apps/server/src/modules/ai/analysis/follow-up-builder.ts b/apps/server/src/modules/ai/analysis/follow-up-builder.ts new file mode 100644 index 0000000..1cecb0e --- /dev/null +++ b/apps/server/src/modules/ai/analysis/follow-up-builder.ts @@ -0,0 +1,17 @@ +import type { AnalysisPlan, FollowUp, MetricResult } from '@ftb/shared'; + +export function buildFollowUps(result: MetricResult, plan: AnalysisPlan): FollowUp[] { + const top = result.rows[0]; + const baseQuestion = top?.label ? `为什么${String(top.label)}最高?` : '换一个维度继续分析'; + + return [ + { type: 'question', label: '继续分析原因', prompt: baseQuestion }, + { + type: 'drilldown', + label: '查看明细', + target: plan.dimensions[0] ?? 'analysis', + filters: plan.filters, + }, + { type: 'export', label: '导出报告', format: 'pdf' }, + ]; +} diff --git a/apps/server/src/modules/ai/analysis/insight-engine.ts b/apps/server/src/modules/ai/analysis/insight-engine.ts new file mode 100644 index 0000000..39b6975 --- /dev/null +++ b/apps/server/src/modules/ai/analysis/insight-engine.ts @@ -0,0 +1,23 @@ +import type { InsightCard, MetricDefinition, MetricResult } from '@ftb/shared'; + +export function buildInsightCard( + result: MetricResult, + metric: MetricDefinition, + semanticConfidence: InsightCard['semanticConfidence'], +): InsightCard { + const top = result.rows[0]; + const topValue = top?.value ?? result.totals?.value ?? 0; + const topLabel = String(top?.label ?? metric.name); + const dataConfidence: InsightCard['dataConfidence'] = + result.rows.length === 0 ? 'insufficient' : result.rows.length < 3 ? 'partial' : 'sufficient'; + + return { + summary: + result.rows.length === 0 + ? `${metric.name}暂无可分析数据。` + : `${topLabel}在${metric.name}中最突出。`, + primaryValue: { label: metric.name, value: topValue }, + semanticConfidence, + dataConfidence, + }; +} diff --git a/apps/server/src/modules/ai/analysis/report-builder.ts b/apps/server/src/modules/ai/analysis/report-builder.ts new file mode 100644 index 0000000..8a28e1f --- /dev/null +++ b/apps/server/src/modules/ai/analysis/report-builder.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import type { AnalysisReport, DataScope, InsightCard, MetricDefinition, MetricResult } from '@ftb/shared'; + +@Injectable() +export class AnalysisReportBuilder { + async build( + result: MetricResult, + insight: InsightCard, + metric: MetricDefinition, + ): Promise { + return { + summary: insight.summary, + keyFindings: result.rows + .slice(0, 4) + .map((row) => `${String(row.label ?? '对象')}:${String(row.value ?? 0)}`), + evidence: result.evidence, + suggestions: + result.rows.length > 0 + ? ['优先查看排名靠前的对象,并进入明细确认原因。'] + : ['调整时间范围或切换分析维度。'], + dataScope: { + timeDescription: resultHasTime(result) ? '按分析计划时间范围统计' : '当前状态', + permissionDescription: describeScope(result.dataScope), + metricFormulaDescription: metric.formula, + generatedAt: result.generatedAt, + }, + }; + } +} + +function resultHasTime(result: MetricResult): boolean { + return result.columns.some((column) => column.type === 'date'); +} + +function describeScope(scope: DataScope): string { + if (scope.type === 'system') return '系统管理范围'; + if (scope.type === 'managed_projects') return `管理项目范围:${scope.projectIds.length} 个项目`; + if (scope.type === 'self') return '与当前用户相关的数据'; + if (scope.type === 'product') return `产品范围:${scope.productId}`; + if (scope.type === 'project') return `项目范围:${scope.projectId}`; + return `版本范围:${scope.versionId}`; +} diff --git a/apps/server/src/modules/ai/dto/analysis.dto.ts b/apps/server/src/modules/ai/dto/analysis.dto.ts new file mode 100644 index 0000000..b098742 --- /dev/null +++ b/apps/server/src/modules/ai/dto/analysis.dto.ts @@ -0,0 +1,34 @@ +import { IsArray, IsIn, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +class AnalysisContextDto { + @IsIn(['ai_assistant', 'product_detail', 'project_detail', 'version_detail']) + surface!: 'ai_assistant' | 'product_detail' | 'project_detail' | 'version_detail'; + + @IsOptional() + @IsString() + productId?: string; + + @IsOptional() + @IsString() + projectId?: string; + + @IsOptional() + @IsString() + versionId?: string; +} + +export class AnalysisDto { + @IsString() + question!: string; + + @IsOptional() + @ValidateNested() + @Type(() => AnalysisContextDto) + context?: AnalysisContextDto; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + permissions?: string[]; +} diff --git a/apps/server/src/modules/ai/prompts/analysis-plan.ts b/apps/server/src/modules/ai/prompts/analysis-plan.ts new file mode 100644 index 0000000..c5fa4bb --- /dev/null +++ b/apps/server/src/modules/ai/prompts/analysis-plan.ts @@ -0,0 +1,103 @@ +export const ANALYSIS_PLAN_TOOL_NAME = 'propose_analysis_plan'; + +export const ANALYSIS_PLAN_SYSTEM_PROMPT = ` +你是 FTB 项目管理系统的业务分析计划助手。 +你只能提出 AnalysisPlan 草案,不能执行查询,不能编写 SQL,不能绕过权限。 +所有 metricId、analysisType、dimensions、timeRange、filters 和 limit 必须来自系统给定的 Metric Catalog 与 Semantic Layer。 +当问题无法映射到已暴露能力时,返回 clarificationOptions,不要编造指标。 +`.trim(); + +export const ANALYSIS_PLAN_TOOL_DESCRIPTION = + 'Return a normalized business analysis plan draft using only exposed metric catalog capabilities.'; + +export const ANALYSIS_PLAN_TOOL_INPUT_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['metricId', 'analysisType', 'dimensions', 'filters'], + properties: { + metricId: { + type: 'string', + enum: [ + 'version_risk_score', + 'completion_trend', + 'overdue_item_count', + 'requirement_status_count', + 'requirement_completion_count', + 'requirement_source_count', + 'department_workload', + 'member_pending_work', + 'member_effort_hours', + 'bug_severity_count', + 'test_pass_rate', + 'overtime_reason_hours', + 'delay_rate', + 'delay_reason_count', + ], + }, + metricVersion: { type: 'number' }, + analysisType: { + type: 'string', + enum: [ + 'ranking', + 'trend', + 'comparison', + 'distribution', + 'composition', + 'correlation', + 'breakdown', + 'summary', + ], + }, + dimensions: { + type: 'array', + items: { + type: 'string', + enum: [ + 'product', + 'project', + 'version', + 'requirement_status', + 'requirement_type', + 'requirement_source', + 'department', + 'member', + 'role', + 'month', + 'week', + 'day', + 'bug_severity', + 'bug_status', + 'test_status', + 'delay_reason', + ], + }, + }, + filters: { type: 'object', additionalProperties: true }, + timeRange: { + type: 'object', + additionalProperties: false, + required: ['start', 'end'], + properties: { + start: { type: 'string' }, + end: { type: 'string' }, + policy: { + type: 'string', + enum: ['current_state', 'last_30_days', 'lifecycle', 'user_required', 'explicit_range'], + }, + }, + }, + limit: { type: 'number', minimum: 1, maximum: 20 }, + sort: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['field', 'direction'], + properties: { + field: { type: 'string' }, + direction: { type: 'string', enum: ['asc', 'desc'] }, + }, + }, + }, + }, +} as const; diff --git a/apps/server/src/modules/ai/prompts/analysis-report.ts b/apps/server/src/modules/ai/prompts/analysis-report.ts new file mode 100644 index 0000000..b108c12 --- /dev/null +++ b/apps/server/src/modules/ai/prompts/analysis-report.ts @@ -0,0 +1,22 @@ +export const ANALYSIS_REPORT_TOOL_NAME = 'write_analysis_report'; + +export const ANALYSIS_REPORT_SYSTEM_PROMPT = ` +你是 FTB 项目管理系统的业务分析报告助手。 +你只能基于 MetricResult、InsightCard、Evidence 和 DataScope 写报告。 +禁止新增数据事实,禁止推测未给出的原因,禁止扩大权限范围。 +报告必须固定输出 Summary、Key Findings、Evidence、Suggestions、Data Scope。 +`.trim(); + +export const ANALYSIS_REPORT_TOOL_DESCRIPTION = + 'Write a structured business analysis report from deterministic metric results without inventing facts.'; + +export const ANALYSIS_REPORT_TOOL_INPUT_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['summary', 'keyFindings', 'suggestions'], + properties: { + summary: { type: 'string' }, + keyFindings: { type: 'array', items: { type: 'string' } }, + suggestions: { type: 'array', items: { type: 'string' } }, + }, +} as const;