236 lines
7.4 KiB
Python
236 lines
7.4 KiB
Python
"""Plain Python data contracts for the Developer Trend & DevRel Ideation Agent."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import asdict, dataclass, field
|
|
from typing import Any
|
|
|
|
|
|
def _split_list(value: object) -> list[str]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, str):
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
if isinstance(value, list):
|
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
return [str(value).strip()] if str(value).strip() else []
|
|
|
|
|
|
def _score(value: object) -> int:
|
|
try:
|
|
number = int(value)
|
|
except (TypeError, ValueError):
|
|
number = 0
|
|
return max(0, min(100, number))
|
|
|
|
|
|
class Serializable:
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
def to_json(self, indent: int | None = None) -> str:
|
|
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
|
|
|
|
@dataclass
|
|
class CompanyContext(Serializable):
|
|
company_name: str = ""
|
|
product: str = ""
|
|
audience: str = ""
|
|
seed_keywords: list[str] = field(default_factory=list)
|
|
competitors: list[str] = field(default_factory=list)
|
|
existing_topics: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.company_name = self.company_name.strip()
|
|
self.product = self.product.strip()
|
|
self.audience = self.audience.strip()
|
|
self.seed_keywords = _split_list(self.seed_keywords)
|
|
self.competitors = _split_list(self.competitors)
|
|
self.existing_topics = _split_list(self.existing_topics)
|
|
|
|
|
|
@dataclass
|
|
class SearchQueryPlan(Serializable):
|
|
rationale: str
|
|
hn_queries: list[str] = field(default_factory=list)
|
|
dev_queries: list[str] = field(default_factory=list)
|
|
dev_tags: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.hn_queries = _split_list(self.hn_queries)
|
|
self.dev_queries = _split_list(self.dev_queries)
|
|
self.dev_tags = _split_list(self.dev_tags)
|
|
|
|
|
|
@dataclass
|
|
class HNItem(Serializable):
|
|
title: str
|
|
url: str
|
|
source_type: str
|
|
author: str | None = None
|
|
points: int | None = None
|
|
num_comments: int | None = None
|
|
created_at: str | None = None
|
|
text: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class HNDeveloperSignal(Serializable):
|
|
topic: str
|
|
pain_point: str
|
|
developer_question: str
|
|
evidence_summary: str
|
|
intensity_score: int
|
|
source_urls: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.intensity_score = _score(self.intensity_score)
|
|
self.source_urls = _split_list(self.source_urls)
|
|
|
|
|
|
@dataclass
|
|
class DEVArticle(Serializable):
|
|
title: str
|
|
url: str
|
|
id: int | None = None
|
|
path: str | None = None
|
|
author: str | None = None
|
|
tags: list[str] = field(default_factory=list)
|
|
published_at: str | None = None
|
|
positive_reactions_count: int | None = None
|
|
comments_count: int | None = None
|
|
description: str | None = None
|
|
body_excerpt: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
self.tags = _split_list(self.tags)
|
|
|
|
|
|
@dataclass
|
|
class DEVSupplySignal(Serializable):
|
|
topic: str
|
|
supply_quality_score: int
|
|
common_angles: list[str] = field(default_factory=list)
|
|
saturated_angles: list[str] = field(default_factory=list)
|
|
missing_angles: list[str] = field(default_factory=list)
|
|
source_urls: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.common_angles = _split_list(self.common_angles)
|
|
self.saturated_angles = _split_list(self.saturated_angles)
|
|
self.missing_angles = _split_list(self.missing_angles)
|
|
self.source_urls = _split_list(self.source_urls)
|
|
self.supply_quality_score = _score(self.supply_quality_score)
|
|
|
|
|
|
@dataclass
|
|
class ContentGap(Serializable):
|
|
topic: str
|
|
demand_score: int
|
|
supply_gap_score: int
|
|
memory_novelty_score: int
|
|
total_score: int
|
|
why_it_matters: str
|
|
supply_gap: str
|
|
recommended_asset_type: str
|
|
business_intent_score: int = 50
|
|
search_intent: str = "implementation"
|
|
demand_evidence: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.demand_score = _score(self.demand_score)
|
|
self.supply_gap_score = _score(self.supply_gap_score)
|
|
self.memory_novelty_score = _score(self.memory_novelty_score)
|
|
self.business_intent_score = _score(self.business_intent_score)
|
|
self.total_score = _score(self.total_score)
|
|
self.demand_evidence = _split_list(self.demand_evidence)
|
|
|
|
|
|
@dataclass
|
|
class TrendDigestItem(Serializable):
|
|
topic: str
|
|
why_trending: str
|
|
intensity_score: int
|
|
dev_saturation: str
|
|
hn_links: list[str] = field(default_factory=list)
|
|
dev_links: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.intensity_score = _score(self.intensity_score)
|
|
self.hn_links = _split_list(self.hn_links)
|
|
self.dev_links = _split_list(self.dev_links)
|
|
|
|
|
|
@dataclass
|
|
class ContentIdea(Serializable):
|
|
title: str
|
|
format: str
|
|
angle: str
|
|
outline: list[str]
|
|
score: int
|
|
confidence: str
|
|
hn_evidence: list[str] = field(default_factory=list)
|
|
dev_gap: str = ""
|
|
dev_links: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.score = _score(self.score)
|
|
self.format = self.format.strip()
|
|
self.hn_evidence = _split_list(self.hn_evidence)
|
|
self.dev_links = _split_list(self.dev_links)
|
|
self.outline = _split_list(self.outline)
|
|
|
|
|
|
@dataclass
|
|
class IdeationReport(Serializable):
|
|
company: str
|
|
summary: str
|
|
trend_digest: list[TrendDigestItem] = field(default_factory=list)
|
|
content_ideas: list[ContentIdea] = field(default_factory=list)
|
|
memory_notes: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.trend_digest = [
|
|
item
|
|
if isinstance(item, TrendDigestItem)
|
|
else TrendDigestItem(**item)
|
|
for item in self.trend_digest
|
|
if isinstance(item, TrendDigestItem) or isinstance(item, dict)
|
|
]
|
|
self.content_ideas = [
|
|
idea
|
|
if isinstance(idea, ContentIdea)
|
|
else ContentIdea(**idea)
|
|
for idea in self.content_ideas
|
|
if isinstance(idea, ContentIdea) or isinstance(idea, dict)
|
|
]
|
|
self.memory_notes = _split_list(self.memory_notes)
|
|
|
|
|
|
@dataclass
|
|
class AgentRunResult(Serializable):
|
|
context: CompanyContext
|
|
query_plan: SearchQueryPlan
|
|
hn_items: list[HNItem]
|
|
dev_articles: list[DEVArticle]
|
|
hn_signals: list[HNDeveloperSignal]
|
|
dev_signals: list[DEVSupplySignal]
|
|
content_gaps: list[ContentGap]
|
|
report: IdeationReport
|
|
markdown: str
|
|
memory_notes: list[str] = field(default_factory=list)
|
|
team_member_responses: list[str] = field(default_factory=list)
|
|
judge_score: float | None = None
|
|
judge_notes: list[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.memory_notes = _split_list(self.memory_notes)
|
|
self.team_member_responses = _split_list(self.team_member_responses)
|
|
self.judge_notes = _split_list(self.judge_notes)
|
|
if isinstance(self.report, dict):
|
|
self.report = IdeationReport(**self.report)
|
|
elif not isinstance(self.report, IdeationReport):
|
|
raise TypeError("AgentRunResult.report must be an IdeationReport.")
|