feat: implement article optimization workflow
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import type { ArticleInput, CandidateFactCard } from "../domain/types";
|
||||
import { candidateFactCardSchema } from "../domain/validation";
|
||||
|
||||
export async function extractCandidateFactCard(
|
||||
input: ArticleInput,
|
||||
): Promise<CandidateFactCard> {
|
||||
const text = `${input.title}\n${input.body}`;
|
||||
const uncertainItems: string[] = [];
|
||||
const companyFullName = findCompanyFullName(text);
|
||||
const years = findExperienceYears(text);
|
||||
|
||||
if (!companyFullName) {
|
||||
uncertainItems.push("Missing company full name");
|
||||
}
|
||||
if (years.length > 1) {
|
||||
uncertainItems.push(`Conflicting experience years: ${years.join(", ")}`);
|
||||
}
|
||||
if (input.images.length === 0) {
|
||||
uncertainItems.push("Image description is missing");
|
||||
}
|
||||
|
||||
const industry = inferIndustry(text);
|
||||
|
||||
return candidateFactCardSchema.parse({
|
||||
company_full_name: companyFullName ?? "",
|
||||
company_short_names: companyFullName ? [companyFullName.split(/\s+/)[0] ?? ""] : [],
|
||||
brand_names: inferCapitalizedNames(text),
|
||||
product_names: inferProducts(text),
|
||||
target_industry: industry,
|
||||
target_audience: text.toLowerCase().includes("marketing")
|
||||
? "Marketing teams"
|
||||
: "Business readers",
|
||||
experience_years: years.length === 1 ? years[0] : null,
|
||||
core_claims: years.length === 1 ? [`${years[0]} years of ${industry} experience`] : [],
|
||||
forbidden_claims: [],
|
||||
image_topics: input.images.map((image) => image.content),
|
||||
uncertain_items: uncertainItems,
|
||||
});
|
||||
}
|
||||
|
||||
function findCompanyFullName(text: string) {
|
||||
const match = text.match(
|
||||
/([A-Z][A-Za-z0-9&.,\-\s]{2,}?(?:Co\.,?\s*Ltd\.?|Company|Inc\.?|LLC|Ltd\.))/,
|
||||
);
|
||||
return match?.[1].trim() ?? null;
|
||||
}
|
||||
|
||||
function findExperienceYears(text: string) {
|
||||
const matches = [...text.matchAll(/\b(\d{1,3})\s*(?:years?|年)\b/gi)];
|
||||
return [...new Set(matches.map((match) => Number(match[1])))];
|
||||
}
|
||||
|
||||
function inferIndustry(text: string) {
|
||||
const lower = text.toLowerCase();
|
||||
if (lower.includes("geo")) return "GEO optimization";
|
||||
if (lower.includes("finance") || lower.includes("banking")) return "finance automation";
|
||||
if (lower.includes("seo")) return "SEO";
|
||||
return "General business";
|
||||
}
|
||||
|
||||
function inferCapitalizedNames(text: string) {
|
||||
const names = [...text.matchAll(/\b[A-Z][A-Za-z0-9]{2,}\b/g)]
|
||||
.map((match) => match[0])
|
||||
.filter((word) => !["The", "This", "And"].includes(word));
|
||||
return [...new Set(names)].slice(0, 5);
|
||||
}
|
||||
|
||||
function inferProducts(text: string) {
|
||||
const productMatches = [...text.matchAll(/\b([A-Z][A-Za-z0-9]+\s+GEO)\b/g)].map(
|
||||
(match) => match[1],
|
||||
);
|
||||
return [...new Set(productMatches)];
|
||||
}
|
||||
Reference in New Issue
Block a user