feat(ai-analysis): 接入业务分析接口
This commit is contained in:
@@ -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<AgentDecomposeResponse | AgentDecomposeError> {
|
||||
@@ -22,4 +30,17 @@ export class AiController {
|
||||
async interpretRisk(@Body() dto: RiskInterpretDto): Promise<AgentRiskInterpretResponse | AgentRiskInterpretError> {
|
||||
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<AnalysisResponse> {
|
||||
return this.businessAnalysisService.analyze(dto, {
|
||||
id: user?.id ?? headerUserId ?? legacyHeaderUserId ?? '',
|
||||
permissions: dto.permissions ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
53
apps/server/src/modules/ai/analysis/analysis-strategy.ts
Normal file
53
apps/server/src/modules/ai/analysis/analysis-strategy.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
104
apps/server/src/modules/ai/analysis/business-analysis.service.ts
Normal file
104
apps/server/src/modules/ai/analysis/business-analysis.service.ts
Normal file
@@ -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<AnalysisResponse> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
35
apps/server/src/modules/ai/analysis/chart-spec-builder.ts
Normal file
35
apps/server/src/modules/ai/analysis/chart-spec-builder.ts
Normal file
@@ -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',
|
||||
};
|
||||
}
|
||||
17
apps/server/src/modules/ai/analysis/follow-up-builder.ts
Normal file
17
apps/server/src/modules/ai/analysis/follow-up-builder.ts
Normal file
@@ -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' },
|
||||
];
|
||||
}
|
||||
23
apps/server/src/modules/ai/analysis/insight-engine.ts
Normal file
23
apps/server/src/modules/ai/analysis/insight-engine.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
42
apps/server/src/modules/ai/analysis/report-builder.ts
Normal file
42
apps/server/src/modules/ai/analysis/report-builder.ts
Normal file
@@ -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<AnalysisReport> {
|
||||
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}`;
|
||||
}
|
||||
34
apps/server/src/modules/ai/dto/analysis.dto.ts
Normal file
34
apps/server/src/modules/ai/dto/analysis.dto.ts
Normal file
@@ -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[];
|
||||
}
|
||||
103
apps/server/src/modules/ai/prompts/analysis-plan.ts
Normal file
103
apps/server/src/modules/ai/prompts/analysis-plan.ts
Normal file
@@ -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;
|
||||
22
apps/server/src/modules/ai/prompts/analysis-report.ts
Normal file
22
apps/server/src/modules/ai/prompts/analysis-report.ts
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user