Files
arindam200--awesome-ai-apps/memory_agents/engineering_content_agent/models.py
T
2026-07-13 13:37:43 +08:00

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.")