import json from collections import Counter, defaultdict from collections.abc import Callable from sqlalchemy import func, select from sqlalchemy.orm import Session from app.models import Comment, ContentItem, Hotspot, Report DEFAULT_SUMMARY = "总结生成失败,请查看上方统计数据。" def _labels(value: str | None) -> list[str]: try: parsed = json.loads(value or "[]") except json.JSONDecodeError: return [] return [str(label) for label in parsed] if isinstance(parsed, list) else [] def build_comment_metrics(comments: list[Comment]) -> dict: sentiment_counts = Counter(comment.sentiment or "unknown" for comment in comments) label_counts = Counter(label for comment in comments for label in _labels(comment.labels)) total = len(comments) def sentiment_entry(name: str) -> dict: count = sentiment_counts.get(name, 0) return {"count": count, "pct": round((count / total * 100) if total else 0, 2)} return { "sample_count": total, "sentiment": { "positive": sentiment_entry("positive"), "negative": sentiment_entry("negative"), "neutral": sentiment_entry("neutral"), "unknown": sentiment_entry("unknown"), }, "top_labels": [{"name": name, "count": count} for name, count in label_counts.most_common(5)], } def select_typical_comments(comments: list[Comment], *, per_sentiment: int = 2) -> dict[str, list[dict]]: buckets: dict[str, list[Comment]] = defaultdict(list) for comment in comments: buckets[comment.sentiment or "unknown"].append(comment) result = {} for sentiment, values in buckets.items(): sorted_values = sorted(values, key=lambda c: (c.like_count or 0, c.comment_time or c.created_at), reverse=True) result[sentiment] = [ {"content": comment.content, "like_count": comment.like_count or 0, "comment_id": comment.source_comment_id} for comment in sorted_values[:per_sentiment] ] return result def generate_item_report( session: Session, content_item_id: str, *, summary_provider: Callable[[dict, dict], str] | None = None, ) -> Report: item = session.get(ContentItem, content_item_id) if item is None: raise ValueError("content item not found") hotspot = session.get(Hotspot, item.hotspot_id) comments = list(session.scalars(select(Comment).where(Comment.content_item_id == item.id))) metrics = build_comment_metrics(comments) typical = select_typical_comments(comments) summary = _safe_summary(summary_provider, metrics, typical, limit=200) markdown = _build_markdown(title=item.title or item.source_item_id, metrics=metrics, typical=typical, summary=summary, hotspot_title=hotspot.title if hotspot else "") report = Report( task_id=item.task_id, hotspot_id=item.hotspot_id, content_item_id=item.id, report_type="item", title=item.title or item.source_item_id, data=json.dumps(metrics, ensure_ascii=False), markdown=markdown, metrics_json=json.dumps(metrics, ensure_ascii=False), typical_comments_json=json.dumps(typical, ensure_ascii=False), summary=summary, markdown_content=markdown, ) session.add(report) session.commit() session.refresh(report) return report def generate_hotspot_report( session: Session, hotspot_id: str, *, summary_provider: Callable[[dict, dict], str] | None = None, ) -> Report: hotspot = session.get(Hotspot, hotspot_id) if hotspot is None: raise ValueError("hotspot not found") comments = list(session.scalars(select(Comment).where(Comment.hotspot_id == hotspot.id))) item_count = session.scalar(select(func.count(ContentItem.id)).where(ContentItem.hotspot_id == hotspot.id)) or 0 metrics = build_comment_metrics(comments) metrics["item_count"] = item_count typical = select_typical_comments(comments) summary = _safe_summary(summary_provider, metrics, typical, limit=300) markdown = _build_markdown(title=hotspot.title, metrics=metrics, typical=typical, summary=summary) report = Report( task_id=hotspot.task_id, hotspot_id=hotspot.id, content_item_id=None, report_type="hotspot", title=hotspot.title, data=json.dumps(metrics, ensure_ascii=False), markdown=markdown, metrics_json=json.dumps(metrics, ensure_ascii=False), typical_comments_json=json.dumps(typical, ensure_ascii=False), summary=summary, markdown_content=markdown, ) session.add(report) session.commit() session.refresh(report) return report def _safe_summary(summary_provider, metrics: dict, typical: dict, *, limit: int) -> str: if summary_provider is None: return DEFAULT_SUMMARY try: summary = summary_provider(metrics, typical, word_limit=limit) except TypeError: try: summary = summary_provider(metrics, typical) except Exception: return DEFAULT_SUMMARY except Exception: return DEFAULT_SUMMARY return str(summary)[:limit] def _build_markdown(*, title: str, metrics: dict, typical: dict, summary: str, hotspot_title: str = "") -> str: lines = [ f"# {title}", "", ] if hotspot_title: lines.extend([f"- 所属热点:{hotspot_title}", ""]) lines.extend( [ f"- 样本评论数量:{metrics['sample_count']}", "", "## 情绪分布", ] ) for name in ("positive", "neutral", "negative", "unknown"): item = metrics["sentiment"][name] lines.append(f"- {name}: {item['count']} ({item['pct']}%)") lines.extend(["", "## Top 标签"]) for label in metrics["top_labels"]: lines.append(f"- {label['name']}: {label['count']}") lines.extend(["", "## 典型评论"]) for sentiment, comments in typical.items(): for comment in comments: lines.append(f"- [{sentiment}] {comment['content']}") lines.extend(["", "## 总结", summary]) return "\n".join(lines)