Arag / app /services /dashboard_service.py
AuthorBot
Add author dashboard email-to-reader with click-to-fill recipient.
214eff1
Raw
History Blame Contribute Delete
8.84 kB
"""Author RAG — Dashboard Service.
Business logic for admin dashboard stats and session moderation.
Delegates DB access to SessionRepository and BookRepository.
"""
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.platform_listing import BookPlatformListing
from app.repositories.book_repo import BookRepository
from app.repositories.session_repo import SessionListStats, SessionRepository
from app.services.platform_presence import count_missing_listings
class DashboardService:
"""Orchestrates dashboard stats and session management."""
def __init__(self, db: AsyncSession) -> None:
self._db = db
self._sessions = SessionRepository(db)
self._books = BookRepository(db)
async def get_dashboard_stats(self, author_id: str) -> dict:
"""Return high-level dashboard stats for the author."""
books = await self._books.list_for_author(author_id)
total_sessions = await self._sessions.count_for_author(author_id)
book_ids = [b.id for b in books]
gaps_books = 0
total_missing = 0
if book_ids:
listings_result = await self._db.execute(
select(BookPlatformListing).where(
BookPlatformListing.author_id == author_id,
BookPlatformListing.book_id.in_(book_ids),
)
)
by_book: dict[str, list[BookPlatformListing]] = {}
for row in listings_result.scalars().all():
by_book.setdefault(row.book_id, []).append(row)
for listings in by_book.values():
missing = count_missing_listings(listings)
if missing > 0:
gaps_books += 1
total_missing += missing
return {
"total_books": len(books),
"active_books": sum(1 for b in books if b.status == "active"),
"total_sessions": total_sessions,
"total_clicks": 0,
"distribution_gaps": {
"books_with_gaps": gaps_books,
"total_missing_platforms": total_missing,
},
}
@staticmethod
def _session_summary(
s: object,
stats: SessionListStats | None = None,
mask_fp: bool = False,
) -> dict:
"""Serialize a ChatSession for API response."""
fp = s.visitor_fingerprint
if mask_fp and fp:
fp = fp[:8] + "..."
row = {
"id": s.id,
"visitor_fingerprint": fp,
"visitor_name": s.visitor_name,
"visitor_email": getattr(s, "visitor_email", None),
"ip_address": getattr(s, "ip_address", None),
"country": s.country_name,
"city": s.city,
"device_type": s.device_type,
"turn_count": s.turn_count,
"rating": s.rating,
"created_at": s.created_at.isoformat() if s.created_at else None,
"updated_at": s.updated_at.isoformat() if getattr(s, "updated_at", None) else None,
"blocked": s.blocked,
"message_count": stats.message_count if stats else 0,
"last_message_at": (
stats.last_message_at.isoformat()
if stats and stats.last_message_at
else None
),
"first_user_preview": stats.first_user_preview if stats else None,
}
return row
async def list_sessions(self, author_id: str, limit: int, offset: int) -> dict:
"""List reader sessions with pagination."""
total = await self._sessions.count_for_author(author_id)
sessions = await self._sessions.list_for_author(author_id, limit, offset)
stats_map = await self._sessions.get_list_stats_batch([s.id for s in sessions])
return {
"sessions": [
self._session_summary(s, stats_map.get(s.id))
for s in sessions
],
"total": total,
"offset": offset,
"limit": limit,
}
async def search_sessions(
self, author_id: str, query: str, page: int, per_page: int,
) -> dict:
"""Full-text search across chat messages."""
if not query.strip():
return {"sessions": [], "total": 0}
sessions, total = await self._sessions.search_by_message_content(
author_id, query, page, per_page,
)
stats_map = await self._sessions.get_list_stats_batch([s.id for s in sessions])
return {
"sessions": [
self._session_summary(s, stats_map.get(s.id), mask_fp=True)
for s in sessions
],
"total": total,
"query": query,
"page": page,
}
async def block_session(self, author_id: str, session_id: str) -> bool:
"""Block a visitor session. Returns False if not found."""
session = await self._sessions.set_blocked(author_id, session_id, True)
return session is not None
async def unblock_session(self, author_id: str, session_id: str) -> bool:
"""Unblock a visitor session. Returns False if not found."""
session = await self._sessions.set_blocked(author_id, session_id, False)
return session is not None
async def get_transcript(
self, author_id: str, session_id: str, page: int, per_page: int,
) -> dict | None:
"""Return paginated message transcript. None if session not found."""
session = await self._sessions.get_for_author(author_id, session_id)
if not session:
return None
total = await self._sessions.message_count(session_id)
messages = await self._sessions.list_messages(session_id, page, per_page)
return {
"session": {
"id": session.id,
"visitor_fingerprint": session.visitor_fingerprint[:8] + "...",
"visitor_name": session.visitor_name,
"visitor_email": session.visitor_email,
"ip_address": session.ip_address,
"country": session.country_name,
"city": session.city,
"device_type": session.device_type,
"browser": session.browser,
"os": session.os,
"rating": session.rating,
"blocked": session.blocked,
"created_at": session.created_at.isoformat() if session.created_at else None,
"summary": session.summary,
},
"messages": [
{
"id": m.id,
"role": m.role,
"content": m.content,
"intent": m.intent,
"intent_confidence": m.intent_confidence,
"faithfulness_score": m.faithfulness_score,
"hallucination_detected": m.hallucination_detected,
"prompt_tokens": m.prompt_tokens,
"completion_tokens": m.completion_tokens,
"response_ms": m.response_ms,
"annotation": m.annotation,
"flag_type": m.flag_type,
"user_feedback": m.user_feedback,
"created_at": m.created_at.isoformat() if m.created_at else None,
}
for m in messages
],
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page,
},
}
async def annotate_message(
self, author_id: str, session_id: str, message_id: str, annotation: str,
) -> dict | None:
"""Add admin annotation to a message. None if not found."""
msg = await self._sessions.get_message_for_author(
author_id, session_id, message_id,
)
if not msg:
return None
prefix = f"[{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}] "
existing = msg.annotation or ""
msg.annotation = (prefix + annotation + "\n" + existing).strip()
await self._db.commit()
return {"message": "Annotation saved", "annotation": msg.annotation}
async def flag_message(
self, author_id: str, session_id: str, message_id: str, flag_type: str,
) -> dict | None:
"""Flag a message. None if not found."""
msg = await self._sessions.get_message_for_author(
author_id, session_id, message_id,
)
if not msg:
return None
msg.flag_type = flag_type
await self._db.commit()
return {"message": "Flag updated", "flag_type": flag_type}