Files
agent-park/scripts/sync-domain-scenarios.ts
T

321 lines
8.4 KiB
TypeScript

import { PrismaClient } from '@prisma/client'
import {
DOMAIN_SCENARIO_PRESET_TAGS,
type DomainScenarioPresetSlug,
} from '../src/lib/tag-taxonomy'
const prisma = new PrismaClient()
type DomainRule = {
slug: DomainScenarioPresetSlug
aliasSlugs: string[]
keywords: string[]
minScore: number
minKeywordHits: number
}
const DOMAIN_RULES: DomainRule[] = [
{
slug: 'code-dev',
aliasSlugs: [
'code-dev',
'ai-development-tool',
'code-generation',
'tool-calling',
'vs-code-extension',
'cli',
'library',
'agent-framework',
'ai-agent-framework',
],
keywords: ['code', 'coding', 'programming', 'developer', '编程', '代码', '开发工具'],
minScore: 2,
minKeywordHits: 2,
},
{
slug: 'automation-workflow',
aliasSlugs: [
'automation-workflow',
'automation',
'workflow-automation',
'workflow-orchestration',
'web-automation',
'browser-automation',
'rpa',
],
keywords: ['workflow', 'automation', 'orchestration', 'rpa', '自动化', '工作流', '编排'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'knowledge-rag',
aliasSlugs: [
'knowledge-rag',
'knowledge-management',
'知识管理',
'knowledge-base',
'knowledge-graph',
'rag',
'向量数据库',
'vector-database',
'llamaindex',
],
keywords: ['knowledge', 'retrieval', 'memory', 'rag', '知识', '检索', '记忆', '知识库'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'education-research',
aliasSlugs: [
'education-research',
'docs-tutorial',
'ai-research',
'markdown',
'tutorial',
'guide',
'paper',
'benchmark',
],
keywords: ['tutorial', 'guide', 'docs', 'paper', 'course', '教程', '指南', '文档', '论文', '课程'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'model-inference',
aliasSlugs: [
'model-inference',
'inference-model',
'llm',
'大语言模型',
'transformers',
'pytorch',
'machine-learning',
'deep-learning',
'reinforcement-learning',
'vllm',
],
keywords: ['model', 'inference', 'training', 'finetune', '模型', '推理', '训练', '微调'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'api-integration',
aliasSlugs: [
'api-integration',
'model-context-protocol',
'sdk',
'api-gateway',
],
keywords: ['api', 'sdk', 'protocol', 'integration', 'mcp', '接口', '协议', '集成'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'vision-multimodal',
aliasSlugs: ['vision-multimodal', 'computer-vision', 'multimodal', 'multimodal-ai'],
keywords: ['vision', 'image', 'video', 'multimodal', '视觉', '图像', '视频', '多模态'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'data-bi',
aliasSlugs: ['data-bi', 'data-analytics', '数据分析', 'visualization'],
keywords: ['analytics', 'dashboard', 'visualization', 'business intelligence', '数据分析', '可视化'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'security-privacy',
aliasSlugs: ['security-privacy', 'ai-security', '隐私保护'],
keywords: ['security', 'privacy', 'safety', '安全', '隐私'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'enterprise-office',
aliasSlugs: ['enterprise-office', 'enterprise-ai', 'chatbot'],
keywords: ['enterprise', 'office', 'collaboration', 'crm', 'erp', '企业', '办公', '协作'],
minScore: 2,
minKeywordHits: 1,
},
{
slug: 'finance',
aliasSlugs: ['finance', 'fintech'],
keywords: ['finance', 'financial', 'trading', 'fintech', 'quant', '金融', '交易', '量化', '风控'],
minScore: 1,
minKeywordHits: 1,
},
{
slug: 'medical-biomed',
aliasSlugs: ['medical-biomed', 'medical', 'healthcare', 'biomedical'],
keywords: ['medical', 'healthcare', 'medicine', 'biomed', '医疗', '医学', '医药', '生物医学'],
minScore: 1,
minKeywordHits: 1,
},
]
const FALLBACK_BY_FIXED_PROJECT_TYPE: Partial<Record<string, DomainScenarioPresetSlug>> = {
'agent-tooling': 'code-dev',
'inference-model': 'model-inference',
'docs-tutorial': 'education-research',
}
function normalize(value: string): string {
return value.toLowerCase().trim()
}
function inferDomainSlugs(input: {
name: string
nameEn?: string | null
description: string
descriptionEn?: string | null
tagSlugs: string[]
}): DomainScenarioPresetSlug[] {
const normalizedTagSet = new Set(input.tagSlugs.map((slug) => normalize(slug)))
const text = normalize(
[input.name, input.nameEn || '', input.description, input.descriptionEn || '', ...input.tagSlugs].join(' ')
)
const scoredDomains: Array<{ slug: DomainScenarioPresetSlug; score: number }> = []
for (const rule of DOMAIN_RULES) {
const tagHits = rule.aliasSlugs.reduce(
(acc, slug) => acc + (normalizedTagSet.has(normalize(slug)) ? 1 : 0),
0
)
const keywordHits = rule.keywords.reduce(
(acc, keyword) => acc + (text.includes(normalize(keyword)) ? 1 : 0),
0
)
const score = tagHits * 3 + keywordHits
if (score >= rule.minScore && (tagHits > 0 || keywordHits >= rule.minKeywordHits)) {
scoredDomains.push({ slug: rule.slug, score })
}
}
if (scoredDomains.length > 0) {
return scoredDomains
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map((item) => item.slug)
}
const fixedProjectType = input.tagSlugs.find((slug) => FALLBACK_BY_FIXED_PROJECT_TYPE[slug])
if (fixedProjectType) {
return [FALLBACK_BY_FIXED_PROJECT_TYPE[fixedProjectType]!]
}
return ['code-dev']
}
async function main() {
console.log('[domain] start sync')
const canonicalTagIdMap = new Map<DomainScenarioPresetSlug, string>()
for (const domainTag of DOMAIN_SCENARIO_PRESET_TAGS) {
const tag = await prisma.tag.upsert({
where: { slug: domainTag.slug },
update: {
name: domainTag.name,
nameEn: domainTag.nameEn,
category: 'DOMAIN_SCENARIO',
},
create: {
name: domainTag.name,
nameEn: domainTag.nameEn,
slug: domainTag.slug,
category: 'DOMAIN_SCENARIO',
},
})
canonicalTagIdMap.set(domainTag.slug, tag.id)
}
const clearedLinks = await prisma.projectTag.deleteMany({
where: {
tagId: {
in: Array.from(canonicalTagIdMap.values()),
},
},
})
const projects = await prisma.project.findMany({
where: { status: 'ACTIVE' },
select: {
id: true,
name: true,
nameEn: true,
description: true,
descriptionEn: true,
tags: {
select: {
tag: {
select: {
slug: true,
},
},
},
},
},
})
const createData: Array<{ projectId: string; tagId: string }> = []
const distribution = new Map<DomainScenarioPresetSlug, number>()
for (const domainTag of DOMAIN_SCENARIO_PRESET_TAGS) {
distribution.set(domainTag.slug, 0)
}
for (const project of projects) {
const tagSlugs = project.tags.map((item) => item.tag.slug)
const domainSlugs = inferDomainSlugs({
name: project.name,
nameEn: project.nameEn,
description: project.description,
descriptionEn: project.descriptionEn,
tagSlugs,
})
for (const domainSlug of domainSlugs) {
const domainTagId = canonicalTagIdMap.get(domainSlug)
if (!domainTagId) {
throw new Error(`Missing canonical domain tag id: ${domainSlug}`)
}
createData.push({
projectId: project.id,
tagId: domainTagId,
})
distribution.set(domainSlug, (distribution.get(domainSlug) || 0) + 1)
}
}
const insertedLinks = await prisma.projectTag.createMany({
data: createData,
skipDuplicates: true,
})
const distributionSummary = DOMAIN_SCENARIO_PRESET_TAGS.map((item) => ({
slug: item.slug,
name: item.name,
projectCount: distribution.get(item.slug) || 0,
}))
console.log('[domain] done', {
canonicalDomainTagCount: DOMAIN_SCENARIO_PRESET_TAGS.length,
clearedCanonicalDomainLinks: clearedLinks.count,
insertedDomainLinks: insertedLinks.count,
activeProjectCount: projects.length,
})
console.log('[domain] distribution', distributionSummary)
}
main()
.catch((error) => {
console.error('[domain] failed', error)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})