export default class ContentDrafter extends AgenticSystem {
@field voiceGuidelines: VoiceGuidelines;
@field templates: Map<string, ContentTemplate> = new Map();
@field draft: stream<string>('');
async createDraft(piece: ContentPiece, research: ResearchMaterial): Promise<ContentDraft> {
const template = this.templates.get(piece.type);
// Build context from research
const researchContext = this.buildResearchContext(research);
// Generate outline
const outline = await this.createOutline(piece, research, template);
// Draft each section
let fullDraft = '';
for (const section of outline.sections) {
const sectionDraft = await this.draftSection(section, piece, researchContext);
fullDraft += sectionDraft + '\n\n';
this.draft.set(fullDraft);
}
// Generate metadata
const metadata = await this.generateMetadata(piece, fullDraft);
return {
title: outline.title,
body: fullDraft.trim(),
summary: metadata.summary ?? '',
metadata,
wordCount: this.countWords(fullDraft),
version: 1
};
}
private async createOutline(
piece: ContentPiece,
research: ResearchMaterial,
template?: ContentTemplate
): Promise<ContentOutline> {
const templateGuidance = template
? `Follow this structure:\n${template.sections.map(s => `- ${s.name}: ${s.description}`).join('\n')}`
: 'Create an appropriate structure for this content.';
const { text } = await llm.complete([
{ role: 'system', content: `Create a content outline.
${templateGuidance}
Voice guidelines:
- Tone: ${this.voiceGuidelines.tone}
- Style: ${this.voiceGuidelines.style}
- Avoid: ${this.voiceGuidelines.avoid.join(', ')}
Return a JSON object with:
- "title": string
- "sections": Array<{ "title": string, "purpose": string, "targetLength": number, "keyPoints": string[] }>` },
{ role: 'user', content: `Topic: ${piece.topic}
Angle: ${piece.angle}
Target audience: ${piece.targetAudience}
Research summary:
${research.summary}
Create an outline and respond with strict JSON only.` }
]);
let parsed: ContentOutline;
try {
parsed = this.parseOutline(JSON.parse(text));
} catch {
// Fall back to a minimal outline if parsing fails.
parsed = {
title: piece.topic,
sections: [
{
title: piece.topic,
purpose: 'Fallback outline',
targetLength: 800,
keyPoints: []
}
]
};
}
return parsed;
}
private async draftSection(
section: OutlineSection,
piece: ContentPiece,
researchContext: string
): Promise<string> {
const { text } = await llm.complete([
{ role: 'system', content: `Write content following these voice guidelines:
- Tone: ${this.voiceGuidelines.tone}
- Style: ${this.voiceGuidelines.style}
- Target reading level: ${this.voiceGuidelines.readingLevel}
Things to avoid:
${this.voiceGuidelines.avoid.map(a => `- ${a}`).join('\n')}
Things to embrace:
${this.voiceGuidelines.embrace.map(e => `- ${e}`).join('\n')}` },
{ role: 'user', content: `Section: ${section.title}
Purpose: ${section.purpose}
Target length: ${section.targetLength} words
Context:
${researchContext}
Write this section as continuous prose.` }
]);
return text;
}
private async generateMetadata(
piece: ContentPiece,
body: string
): Promise<ContentDraft['metadata']> {
const { text } = await llm.complete([
{ role: 'system', content: `Generate SEO-optimized metadata for this content.
Return a JSON object with:
- "summary": string // ~150 characters
- "metaDescription": string // ~160 characters
- "keywords": string // comma-separated keywords` },
{ role: 'user', content: `Content:\n${body.slice(0, 2000)}...\n\nGenerate the metadata and respond with strict JSON only.` }
]);
let parsed: any;
try {
parsed = JSON.parse(text);
} catch {
parsed = {};
}
const metadata: ContentDraft['metadata'] = {
summary: parsed.summary ?? '',
metaDescription: parsed.metaDescription ?? '',
keywords: parsed.keywords ?? ''
};
return metadata;
}
}
interface VoiceGuidelines {
tone: string; // e.g., "professional but approachable"
style: string; // e.g., "clear, direct, uses examples"
readingLevel: string; // e.g., "8th grade"
avoid: string[]; // e.g., ["jargon", "passive voice", "clichés"]
embrace: string[]; // e.g., ["concrete examples", "action verbs"]
}
interface ContentTemplate {
type: string;
sections: {
name: string;
description: string;
targetLength: number;
required: boolean;
}[];
}
interface ContentOutline {
title: string;
sections: OutlineSection[];
}
interface OutlineSection {
title: string;
purpose: string;
targetLength: number;
keyPoints: string[];
}
interface ResearchMaterial {
summary: string;
facts: { content: string; source: string }[];
quotes: { text: string; attribution: string }[];
data: { description: string; value: string }[];
}