feat: add /tools/news endpoint for news ticker integration
- Add NewsHeadline and NewsResponse schemas
- Add NewsService for parsing news from Qdrant volatile collection
- Add GET /tools/news endpoint to tools controller
- News fetched from 'news' namespace in volatile_{user} collection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
768cea2c89
commit
fcf5c8ccee
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.11.0] - 2026-01-08
|
||||
|
||||
### Added
|
||||
|
||||
- **News Headlines API** - New endpoint for news ticker integration
|
||||
- `GET /tools/news` - Fetch news headlines from user's volatile collection
|
||||
- Returns headlines with title, description, source, and URL
|
||||
- Data sourced from `volatile_{user}` Qdrant collection (news namespace)
|
||||
- Uses `preferred_username` from OIDC, falls back to `default`
|
||||
- News subdomain under tools (`src/domains/tools/news/`)
|
||||
- `NewsHeadline` and `NewsResponse` Pydantic schemas
|
||||
- `NewsService` for parsing news data from Qdrant
|
||||
|
||||
## [1.10.12] - 2026-01-08
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "core-api"
|
||||
version = "1.10.12"
|
||||
version = "1.11.0"
|
||||
description = "Core Code API - Infrastructure management and tools API"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -18,6 +18,8 @@ from src.domains.tools.system.schemas import SystemStatsResponse
|
||||
from src.domains.tools.system.service import SystemStatsService
|
||||
from src.domains.tools.environment.schemas import EnvironmentResponse
|
||||
from src.domains.tools.environment.service import EnvironmentService
|
||||
from src.domains.tools.news.schemas import NewsResponse
|
||||
from src.domains.tools.news.service import NewsService
|
||||
from src.domains.auth.oidc import get_optional_user
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -38,6 +40,7 @@ class ToolsController(BaseController):
|
||||
self.dns_service = DNSService()
|
||||
self.system_stats_service = SystemStatsService()
|
||||
self.environment_service = EnvironmentService()
|
||||
self.news_service = NewsService()
|
||||
|
||||
def create_router(self) -> APIRouter:
|
||||
"""Create and configure the router"""
|
||||
@@ -213,6 +216,65 @@ class ToolsController(BaseController):
|
||||
detail=f"Failed to fetch environment data: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/news",
|
||||
response_model=NewsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Get news headlines",
|
||||
description="""
|
||||
Get news headlines for the authenticated user.
|
||||
|
||||
Fetches news data from the Qdrant volatile collection for the authenticated user.
|
||||
Falls back to 'default' user if not authenticated.
|
||||
|
||||
**Data Returned:**
|
||||
- **Headlines:** List of news headlines with title, description, source, url
|
||||
- **Category:** News category (general, technology, etc.)
|
||||
- **Sources:** List of news sources
|
||||
|
||||
**Data Source:** Qdrant volatile_{user} collection (news namespace)
|
||||
|
||||
**Use Cases:**
|
||||
- Dashboard news ticker
|
||||
- News feed widgets
|
||||
- Information display
|
||||
"""
|
||||
)
|
||||
async def get_news(
|
||||
user: Optional[Dict] = Depends(get_optional_user),
|
||||
) -> NewsResponse:
|
||||
"""
|
||||
Get news headlines
|
||||
|
||||
Args:
|
||||
user: Optional authenticated user from OIDC
|
||||
|
||||
Returns:
|
||||
News headlines response
|
||||
|
||||
Raises:
|
||||
HTTPException: 500 for processing errors
|
||||
"""
|
||||
try:
|
||||
# Get user identifier from OIDC claims, fallback to 'default'
|
||||
user_id = "default"
|
||||
if user:
|
||||
user_id = user.get("preferred_username") or user.get("sub", "default")
|
||||
# Strip email domain if present (e.g., "user@example.com" -> "user")
|
||||
if "@" in user_id:
|
||||
user_id = user_id.split("@")[0]
|
||||
|
||||
logger.info(f"Fetching news data for user: {user_id}")
|
||||
result = await self.news_service.get_news(user_id)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get news data: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch news data: {str(e)}"
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
News subdomain for Tools.
|
||||
|
||||
Provides news headlines from Qdrant volatile collection.
|
||||
"""
|
||||
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
News data schemas for Tools domain.
|
||||
|
||||
Provides Pydantic models for news headlines retrieved from the Qdrant volatile collection.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import Field
|
||||
|
||||
from src.shared.base import BaseSchema
|
||||
|
||||
|
||||
class NewsHeadline(BaseSchema):
|
||||
"""Single news headline."""
|
||||
|
||||
title: str = Field(
|
||||
...,
|
||||
description="Headline title"
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
None,
|
||||
description="Brief description or summary"
|
||||
)
|
||||
source: Optional[str] = Field(
|
||||
None,
|
||||
description="News source name"
|
||||
)
|
||||
url: Optional[str] = Field(
|
||||
None,
|
||||
description="Link to full article"
|
||||
)
|
||||
|
||||
|
||||
class NewsResponse(BaseSchema):
|
||||
"""News headlines response."""
|
||||
|
||||
headlines: List[NewsHeadline] = Field(
|
||||
default_factory=list,
|
||||
description="List of news headlines"
|
||||
)
|
||||
category: Optional[str] = Field(
|
||||
None,
|
||||
description="News category (e.g., 'general', 'technology')"
|
||||
)
|
||||
sources: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="List of source names"
|
||||
)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="Timestamp when data was fetched"
|
||||
)
|
||||
user: Optional[str] = Field(
|
||||
None,
|
||||
description="User identifier used for data lookup"
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
News data service for Tools domain.
|
||||
|
||||
Fetches news headlines from the Qdrant volatile collection.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from src.shared.logging import get_logger
|
||||
from src.shared.clients.qdrant_client import get_qdrant_client
|
||||
from src.domains.tools.news.schemas import (
|
||||
NewsHeadline,
|
||||
NewsResponse,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NewsService:
|
||||
"""
|
||||
Service for fetching news data from Qdrant volatile collection.
|
||||
|
||||
Retrieves news headlines for a specific user.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize news service with Qdrant client."""
|
||||
self.qdrant = get_qdrant_client()
|
||||
|
||||
def _parse_headlines(self, raw_data: Any) -> List[NewsHeadline]:
|
||||
"""
|
||||
Parse raw news data into list of NewsHeadline schemas.
|
||||
|
||||
Handles various formats from different news sources.
|
||||
"""
|
||||
if not raw_data:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Handle dict with nested headlines list
|
||||
headlines_list = raw_data
|
||||
if isinstance(raw_data, dict):
|
||||
headlines_list = raw_data.get("headlines") or raw_data.get("articles") or []
|
||||
|
||||
if not isinstance(headlines_list, list):
|
||||
return []
|
||||
|
||||
headlines = []
|
||||
for item in headlines_list:
|
||||
if isinstance(item, dict):
|
||||
headlines.append(NewsHeadline(
|
||||
title=item.get("title", ""),
|
||||
description=item.get("description") or item.get("summary"),
|
||||
source=item.get("source") or item.get("provider"),
|
||||
url=item.get("url") or item.get("link"),
|
||||
))
|
||||
elif isinstance(item, str):
|
||||
# Simple string headlines
|
||||
headlines.append(NewsHeadline(title=item))
|
||||
|
||||
return headlines
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse news headlines: {e}")
|
||||
return []
|
||||
|
||||
async def get_news(self, user: str = "default") -> NewsResponse:
|
||||
"""
|
||||
Get news headlines for a user.
|
||||
|
||||
Fetches news from the user's volatile collection.
|
||||
|
||||
Args:
|
||||
user: User identifier (default: 'default')
|
||||
|
||||
Returns:
|
||||
NewsResponse with headlines
|
||||
"""
|
||||
logger.info(f"Fetching news data for user: {user}")
|
||||
|
||||
# Get raw data from Qdrant
|
||||
news_records = await self.qdrant.get_by_namespace(user, "news")
|
||||
|
||||
headlines = []
|
||||
category = None
|
||||
sources = None
|
||||
|
||||
if news_records:
|
||||
raw_data = news_records[0].get("raw_data", {})
|
||||
headlines = self._parse_headlines(raw_data)
|
||||
if isinstance(raw_data, dict):
|
||||
category = raw_data.get("category")
|
||||
sources = raw_data.get("sources")
|
||||
|
||||
return NewsResponse(
|
||||
headlines=headlines,
|
||||
category=category,
|
||||
sources=sources,
|
||||
updated_at=datetime.utcnow(),
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_news_service: Optional[NewsService] = None
|
||||
|
||||
|
||||
def get_news_service() -> NewsService:
|
||||
"""Get or create singleton news service instance."""
|
||||
global _news_service
|
||||
if _news_service is None:
|
||||
_news_service = NewsService()
|
||||
return _news_service
|
||||
Reference in New Issue
Block a user