Files
novelty-seeking/frontend/src/hooks/useExpertTransformation.ts
gbanyan 5571076406 feat: Add curated expert occupations with local data sources
- Add curated occupations seed files (210 entries in zh/en) with specific domains
- Add DBpedia occupations data (2164 entries) for external source option
- Refactor expert_source_service to read from local JSON files
- Improve keyword generation prompts to leverage expert domain context
- Add architecture analysis documentation (ARCHITECTURE_ANALYSIS.md)
- Fix expert source selection bug (proper handling of empty custom_experts)
- Update frontend to support curated/dbpedia/wikidata expert sources

Key changes:
- backend/app/data/: Local occupation data files
- backend/app/services/expert_source_service.py: Simplified local file reading
- backend/app/prompts/expert_transformation_prompt.py: Better domain-aware prompts
- Removed expert_cache.py (no longer needed with local files)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 16:34:35 +08:00

242 lines
7.0 KiB
TypeScript

import { useState, useCallback } from 'react';
import { expertTransformCategoryStream } from '../services/api';
import type {
ExpertTransformationInput,
ExpertTransformationProgress,
ExpertTransformationCategoryResult,
ExpertTransformationDAGResult,
ExpertProfile,
CategoryDefinition,
ExpertSource,
} from '../types';
interface UseExpertTransformationOptions {
model?: string;
temperature?: number;
expertSource?: ExpertSource;
}
export function useExpertTransformation(options: UseExpertTransformationOptions = {}) {
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<ExpertTransformationProgress>({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '',
});
const [results, setResults] = useState<ExpertTransformationDAGResult | null>(null);
const [error, setError] = useState<string | null>(null);
// Global expert team - generated once and shared across all categories
const [experts, setExperts] = useState<ExpertProfile[] | null>(null);
const transformCategory = useCallback(
async (
query: string,
category: CategoryDefinition,
attributes: string[],
expertConfig: {
expert_count: number;
keywords_per_expert: number;
custom_experts?: string[];
}
): Promise<{
result: ExpertTransformationCategoryResult | null;
experts: ExpertProfile[];
}> => {
return new Promise((resolve) => {
let categoryExperts: ExpertProfile[] = [];
setProgress((prev) => ({
...prev,
step: 'expert',
currentCategory: category.name,
message: `組建專家團隊...`,
}));
expertTransformCategoryStream(
{
query,
category: category.name,
attributes,
expert_count: expertConfig.expert_count,
keywords_per_expert: expertConfig.keywords_per_expert,
custom_experts: expertConfig.custom_experts,
expert_source: options.expertSource,
model: options.model,
temperature: options.temperature,
},
{
onExpertStart: () => {
setProgress((prev) => ({
...prev,
step: 'expert',
message: `正在組建專家團隊...`,
}));
},
onExpertComplete: (expertsData) => {
categoryExperts = expertsData;
setExperts(expertsData);
setProgress((prev) => ({
...prev,
experts: expertsData,
message: `專家團隊組建完成(${expertsData.length}位專家)`,
}));
},
onKeywordStart: () => {
setProgress((prev) => ({
...prev,
step: 'keyword',
message: `專家團隊為「${category.name}」的屬性生成關鍵字...`,
}));
},
onKeywordProgress: (data) => {
setProgress((prev) => ({
...prev,
currentAttribute: data.attribute,
message: `為「${data.attribute}」生成了 ${data.count} 個關鍵字`,
}));
},
onKeywordComplete: (totalKeywords) => {
setProgress((prev) => ({
...prev,
message: `共生成了 ${totalKeywords} 個專家關鍵字`,
}));
},
onDescriptionStart: () => {
setProgress((prev) => ({
...prev,
step: 'description',
message: `為「${category.name}」的專家關鍵字生成創新描述...`,
}));
},
onDescriptionComplete: (count) => {
setProgress((prev) => ({
...prev,
message: `生成了 ${count} 個創新描述`,
}));
},
onDone: (data) => {
setProgress((prev) => ({
...prev,
step: 'done',
processedCategories: [...prev.processedCategories, category.name],
message: `${category.name}」處理完成`,
}));
resolve({
result: data.result,
experts: data.experts,
});
},
onError: (err) => {
setProgress((prev) => ({
...prev,
step: 'error',
error: err,
message: `處理「${category.name}」時發生錯誤`,
}));
resolve({
result: null,
experts: categoryExperts,
});
},
}
).catch((err) => {
setProgress((prev) => ({
...prev,
step: 'error',
error: err.message,
message: `處理「${category.name}」時發生錯誤`,
}));
resolve({
result: null,
experts: categoryExperts,
});
});
});
},
[options.model, options.temperature, options.expertSource]
);
const transformAll = useCallback(
async (input: ExpertTransformationInput) => {
setLoading(true);
setError(null);
setResults(null);
setExperts(null);
setProgress({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '開始處理...',
});
const categoryResults: ExpertTransformationCategoryResult[] = [];
let globalExperts: ExpertProfile[] = [];
// Process each category sequentially
for (const category of input.categories) {
const attributes = input.attributesByCategory[category.name] || [];
if (attributes.length === 0) continue;
const { result, experts: categoryExperts } = await transformCategory(
input.query,
category,
attributes,
input.expertConfig
);
// Store global experts from first category
if (globalExperts.length === 0 && categoryExperts.length > 0) {
globalExperts = categoryExperts;
}
if (result) {
categoryResults.push(result);
}
}
// Build final result
const finalResult: ExpertTransformationDAGResult = {
query: input.query,
experts: globalExperts,
results: categoryResults,
};
setResults(finalResult);
setLoading(false);
setProgress((prev) => ({
...prev,
step: 'done',
message: '所有類別處理完成',
}));
return finalResult;
},
[transformCategory]
);
const clearResults = useCallback(() => {
setResults(null);
setError(null);
setExperts(null);
setProgress({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '',
});
}, []);
return {
loading,
progress,
results,
error,
experts,
transformCategory,
transformAll,
clearResults,
};
}