Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Add complete Pipeline DSL system including:
- Rust backend (zclaw-pipeline crate) with parser, executor, and state management
- Frontend components: PipelinesPanel, PipelineResultPreview, ClassroomPreviewer
- Pipeline recommender for Agent conversation integration
- 5 pipeline templates: education, marketing, legal, research, productivity
- Documentation for Pipeline DSL architecture
Pipeline DSL enables declarative workflow definitions with:
- YAML-based configuration
- Expression resolution (${inputs.topic}, ${steps.step1.output})
- LLM integration, parallel execution, file export
- Agent smart recommendations in conversations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
298 lines
8.2 KiB
TypeScript
298 lines
8.2 KiB
TypeScript
/**
|
|
* Pipeline Recommender Service
|
|
*
|
|
* Analyzes user messages to recommend relevant Pipelines.
|
|
* Used by Agent conversation flow to proactively suggest workflows.
|
|
*/
|
|
|
|
import { PipelineInfo, PipelineClient } from './pipeline-client';
|
|
|
|
// === Types ===
|
|
|
|
export interface PipelineRecommendation {
|
|
pipeline: PipelineInfo;
|
|
confidence: number; // 0-1
|
|
matchedKeywords: string[];
|
|
suggestedInputs: Record<string, unknown>;
|
|
reason: string;
|
|
}
|
|
|
|
export interface IntentPattern {
|
|
keywords: RegExp[];
|
|
category?: string;
|
|
pipelineId?: string;
|
|
minConfidence: number;
|
|
inputSuggestions?: (message: string) => Record<string, unknown>;
|
|
}
|
|
|
|
// === Intent Patterns ===
|
|
|
|
const INTENT_PATTERNS: IntentPattern[] = [
|
|
// Education - Classroom
|
|
{
|
|
keywords: [
|
|
/课件|教案|备课|课堂|教学|ppt|幻灯片/i,
|
|
/上课|讲课|教材/i,
|
|
/生成.*课件|制作.*课件|创建.*课件/i,
|
|
],
|
|
category: 'education',
|
|
pipelineId: 'classroom-generator',
|
|
minConfidence: 0.75,
|
|
},
|
|
|
|
// Marketing - Campaign
|
|
{
|
|
keywords: [
|
|
/营销|推广|宣传|市场.*方案|营销.*策略/i,
|
|
/产品.*推广|品牌.*宣传/i,
|
|
/广告.*方案|营销.*计划/i,
|
|
/生成.*营销|制作.*营销/i,
|
|
],
|
|
category: 'marketing',
|
|
pipelineId: 'marketing-campaign',
|
|
minConfidence: 0.72,
|
|
},
|
|
|
|
// Legal - Contract Review
|
|
{
|
|
keywords: [
|
|
/合同.*审查|合同.*风险|合同.*检查/i,
|
|
/审查.*合同|检查.*合同|分析.*合同/i,
|
|
/法律.*审查|合规.*检查/i,
|
|
/合同.*条款|条款.*风险/i,
|
|
],
|
|
category: 'legal',
|
|
pipelineId: 'contract-review',
|
|
minConfidence: 0.78,
|
|
},
|
|
|
|
// Research - Literature Review
|
|
{
|
|
keywords: [
|
|
/文献.*综述|文献.*分析|文献.*检索/i,
|
|
/研究.*综述|学术.*综述/i,
|
|
/论文.*综述|论文.*调研/i,
|
|
/文献.*搜索|文献.*查找/i,
|
|
],
|
|
category: 'research',
|
|
pipelineId: 'literature-review',
|
|
minConfidence: 0.73,
|
|
},
|
|
|
|
// Productivity - Meeting Summary
|
|
{
|
|
keywords: [
|
|
/会议.*纪要|会议.*总结|会议.*记录/i,
|
|
/整理.*会议|总结.*会议/i,
|
|
/会议.*整理|纪要.*生成/i,
|
|
/待办.*事项|行动.*项/i,
|
|
],
|
|
category: 'productivity',
|
|
pipelineId: 'meeting-summary',
|
|
minConfidence: 0.70,
|
|
},
|
|
|
|
// Generic patterns for each category
|
|
{
|
|
keywords: [/帮我.*生成|帮我.*制作|帮我.*创建|自动.*生成/i],
|
|
minConfidence: 0.5,
|
|
},
|
|
];
|
|
|
|
// === Pipeline Recommender Class ===
|
|
|
|
export class PipelineRecommender {
|
|
private pipelines: PipelineInfo[] = [];
|
|
private initialized = false;
|
|
|
|
/**
|
|
* Initialize the recommender by loading pipelines
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
try {
|
|
this.pipelines = await PipelineClient.listPipelines();
|
|
this.initialized = true;
|
|
} catch (error) {
|
|
console.error('[PipelineRecommender] Failed to load pipelines:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh pipeline list
|
|
*/
|
|
async refresh(): Promise<void> {
|
|
try {
|
|
this.pipelines = await PipelineClient.refresh();
|
|
} catch (error) {
|
|
console.error('[PipelineRecommender] Failed to refresh pipelines:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze a user message and return pipeline recommendations
|
|
*/
|
|
async recommend(message: string): Promise<PipelineRecommendation[]> {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
const recommendations: PipelineRecommendation[] = [];
|
|
const messageLower = message.toLowerCase();
|
|
|
|
for (const pattern of INTENT_PATTERNS) {
|
|
const matches = pattern.keywords
|
|
.map(regex => regex.test(message))
|
|
.filter(Boolean);
|
|
|
|
if (matches.length === 0) continue;
|
|
|
|
const confidence = Math.min(
|
|
pattern.minConfidence + (matches.length - 1) * 0.05,
|
|
0.95
|
|
);
|
|
|
|
// Find matching pipeline
|
|
let matchingPipelines: PipelineInfo[] = [];
|
|
|
|
if (pattern.pipelineId) {
|
|
matchingPipelines = this.pipelines.filter(p => p.id === pattern.pipelineId);
|
|
} else if (pattern.category) {
|
|
matchingPipelines = this.pipelines.filter(p => p.category === pattern.category);
|
|
}
|
|
|
|
// If no specific pipeline found, recommend based on category or all
|
|
if (matchingPipelines.length === 0 && !pattern.pipelineId && !pattern.category) {
|
|
// Generic match - recommend top pipelines
|
|
matchingPipelines = this.pipelines.slice(0, 3);
|
|
}
|
|
|
|
for (const pipeline of matchingPipelines) {
|
|
const matchedKeywords = pattern.keywords
|
|
.filter(regex => regex.test(message))
|
|
.map(regex => regex.source);
|
|
|
|
const suggestion: PipelineRecommendation = {
|
|
pipeline,
|
|
confidence,
|
|
matchedKeywords,
|
|
suggestedInputs: pattern.inputSuggestions?.(message) ?? {},
|
|
reason: this.generateReason(pipeline, matchedKeywords, confidence),
|
|
};
|
|
|
|
// Avoid duplicates
|
|
if (!recommendations.find(r => r.pipeline.id === pipeline.id)) {
|
|
recommendations.push(suggestion);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by confidence and return top recommendations
|
|
return recommendations
|
|
.sort((a, b) => b.confidence - a.confidence)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
/**
|
|
* Generate a human-readable reason for the recommendation
|
|
*/
|
|
private generateReason(
|
|
pipeline: PipelineInfo,
|
|
matchedKeywords: string[],
|
|
confidence: number
|
|
): string {
|
|
const confidenceText =
|
|
confidence >= 0.8 ? '非常适合' :
|
|
confidence >= 0.7 ? '适合' :
|
|
confidence >= 0.6 ? '可能适合' : '或许可以尝试';
|
|
|
|
if (matchedKeywords.length > 0) {
|
|
return `您的需求与【${pipeline.displayName}】${confidenceText}。这个 Pipeline 可以帮助您自动化完成相关任务。`;
|
|
}
|
|
|
|
return `【${pipeline.displayName}】可能对您有帮助。需要我为您启动吗?`;
|
|
}
|
|
|
|
/**
|
|
* Format recommendation for Agent message
|
|
*/
|
|
formatRecommendationForAgent(rec: PipelineRecommendation): string {
|
|
const pipeline = rec.pipeline;
|
|
let message = `我可以使用【${pipeline.displayName}】为你自动完成这个任务。\n\n`;
|
|
message += `**功能说明**: ${pipeline.description}\n\n`;
|
|
|
|
if (Object.keys(rec.suggestedInputs).length > 0) {
|
|
message += `**我已识别到以下信息**:\n`;
|
|
for (const [key, value] of Object.entries(rec.suggestedInputs)) {
|
|
message += `- ${key}: ${value}\n`;
|
|
}
|
|
message += '\n';
|
|
}
|
|
|
|
message += `需要开始吗?`;
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Check if a message might benefit from a pipeline
|
|
*/
|
|
mightNeedPipeline(message: string): boolean {
|
|
const pipelineKeywords = [
|
|
'生成', '创建', '制作', '分析', '审查', '整理',
|
|
'总结', '归纳', '提取', '转换', '自动化',
|
|
'帮我', '请帮我', '能不能', '可以',
|
|
];
|
|
|
|
return pipelineKeywords.some(kw => message.includes(kw));
|
|
}
|
|
}
|
|
|
|
// === Singleton Instance ===
|
|
|
|
export const pipelineRecommender = new PipelineRecommender();
|
|
|
|
// === React Hook ===
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
export interface UsePipelineRecommendationOptions {
|
|
autoInit?: boolean;
|
|
minConfidence?: number;
|
|
}
|
|
|
|
export function usePipelineRecommendation(options: UsePipelineRecommendationOptions = {}) {
|
|
const [recommender] = useState(() => new PipelineRecommender());
|
|
const [initialized, setInitialized] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (options.autoInit !== false) {
|
|
recommender.initialize().then(() => setInitialized(true));
|
|
}
|
|
}, [recommender, options.autoInit]);
|
|
|
|
const recommend = useCallback(async (message: string) => {
|
|
setLoading(true);
|
|
try {
|
|
const results = await recommender.recommend(message);
|
|
const minConf = options.minConfidence ?? 0.6;
|
|
return results.filter(r => r.confidence >= minConf);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [recommender, options.minConfidence]);
|
|
|
|
return {
|
|
recommend,
|
|
initialized,
|
|
loading,
|
|
refresh: recommender.refresh.bind(recommender),
|
|
mightNeedPipeline: recommender.mightNeedPipeline,
|
|
formatRecommendationForAgent: recommender.formatRecommendationForAgent.bind(recommender),
|
|
};
|
|
}
|
|
|
|
export default pipelineRecommender;
|