A community based topic aggregation platform built on atproto

feat(aggregator): add source name field and improve formatting

- Add source_name field to Perspective model for better attribution
- Extract source name from HTML anchor tags in parser
- Display source name in rich text (e.g., "The Straits Times" vs generic "Source")
- Improve spacing in highlights, perspectives, and sources lists (double newlines)
- Better visual separation between list items

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

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+18 -10
aggregators
+6 -3
aggregators/kagi-news/src/html_parser.py
···
Perspective(
actor=p['actor'],
description=p['description'],
-
source_url=p['source_url']
)
for p in parsed['perspectives']
]
···
actor, rest = full_text.split(':', 1)
actor = actor.strip()
-
# Find the <a> tag for source URL
a_tag = li.find('a')
source_url = a_tag['href'] if a_tag and a_tag.get('href') else ""
# Extract description (between colon and source link)
# Remove the source citation part in parentheses
···
return {
'actor': actor,
'description': description,
-
'source_url': source_url
}
def _extract_sources(self, soup: BeautifulSoup) -> List[Dict]:
···
Perspective(
actor=p['actor'],
description=p['description'],
+
source_url=p['source_url'],
+
source_name=p.get('source_name', '')
)
for p in parsed['perspectives']
]
···
actor, rest = full_text.split(':', 1)
actor = actor.strip()
+
# Find the <a> tag for source URL and name
a_tag = li.find('a')
source_url = a_tag['href'] if a_tag and a_tag.get('href') else ""
+
source_name = a_tag.get_text(strip=True) if a_tag else ""
# Extract description (between colon and source link)
# Remove the source citation part in parentheses
···
return {
'actor': actor,
'description': description,
+
'source_url': source_url,
+
'source_name': source_name
}
def _extract_sources(self, soup: BeautifulSoup) -> List[Dict]:
+1
aggregators/kagi-news/src/models.py
···
actor: str
description: str
source_url: str
@dataclass
···
actor: str
description: str
source_url: str
+
source_name: str = "" # Name of the source (e.g., "The Straits Times")
@dataclass
+11 -7
aggregators/kagi-news/src/richtext_formatter.py
···
builder.add_bold("Highlights:")
builder.add_text("\n")
for highlight in story.highlights:
-
builder.add_text(f"• {highlight}\n")
builder.add_text("\n")
# Perspectives (if present)
···
# Bold the actor name
actor_with_colon = f"{perspective.actor}:"
builder.add_bold(actor_with_colon)
-
builder.add_text(f" {perspective.description} (")
-
# Add link to source
-
source_link_text = "Source"
-
builder.add_link(source_link_text, perspective.source_url)
-
builder.add_text(")\n")
builder.add_text("\n")
# Quote (if present)
···
for source in story.sources:
builder.add_text("• ")
builder.add_link(source.title, source.url)
-
builder.add_text(f" - {source.domain}\n")
builder.add_text("\n")
# Kagi News attribution
···
builder.add_bold("Highlights:")
builder.add_text("\n")
for highlight in story.highlights:
+
builder.add_text(f"• {highlight}\n\n")
builder.add_text("\n")
# Perspectives (if present)
···
# Bold the actor name
actor_with_colon = f"{perspective.actor}:"
builder.add_bold(actor_with_colon)
+
builder.add_text(f" {perspective.description}")
+
# Add link to source if available
+
if perspective.source_url:
+
builder.add_text(" (")
+
source_link_text = perspective.source_name if perspective.source_name else "Source"
+
builder.add_link(source_link_text, perspective.source_url)
+
builder.add_text(")")
+
+
builder.add_text("\n\n")
builder.add_text("\n")
# Quote (if present)
···
for source in story.sources:
builder.add_text("• ")
builder.add_link(source.title, source.url)
+
builder.add_text(f" - {source.domain}\n\n")
builder.add_text("\n")
# Kagi News attribution