文件预览

quality_nudge.py

查看 Last30days 技能包中的文件内容。

文件内容

scripts/lib/quality_nudge.py

"""Post-research quality score and upgrade nudge.

Computes a quality score based on 5 core sources and builds
a nudge message describing what the user missed and how to fix it.
"""

from typing import List


# The 5 core sources
CORE_SOURCES = ["hn", "polymarket", "x", "youtube", "reddit"]

# Labels for display
SOURCE_LABELS = {
    "hn": "Hacker News",
    "polymarket": "Polymarket",
    "x": "X/Twitter",
    "youtube": "YouTube",
    "reddit": "Reddit",
}


def _is_x_active(config: dict, research_results: dict) -> bool:
    """Check if X source is active (has credentials AND didn't error)."""
    has_creds = bool(config.get("AISA_API_KEY"))
    if not has_creds:
        return False
    # If X errored this run, it's configured but broken
    if research_results.get("x_error"):
        return False
    return True


def _is_youtube_active(config: dict, research_results: dict) -> bool:
    """Check if YouTube source is active (AISA configured)."""
    if not config.get("AISA_API_KEY"):
        return False
    if research_results.get("youtube_error"):
        return False
    return True


def compute_quality_score(config: dict, research_results: dict) -> dict:
    """Compute research quality score based on 5 core sources.

    Args:
        config: Configuration dict from env.get_config()
        research_results: Dict with keys like x_error, youtube_error,
            reddit_error reflecting what happened this run.

    Returns:
        {
            "score_pct": 40-100,
            "core_active": ["hn", "polymarket", ...],
            "core_missing": ["x", "youtube"],
            "core_errored": [],  # configured but errored
            "nudge_text": "..." or None if 100%
        }
    """
    core_active: List[str] = []
    core_missing: List[str] = []
    core_errored: List[str] = []

    # HN and Reddit are always active
    core_active.append("hn")
    core_active.append("reddit")

    if config.get("AISA_API_KEY"):
        core_active.append("polymarket")
    else:
        core_missing.append("polymarket")

    # X
    has_x_creds = bool(config.get("AISA_API_KEY"))
    if _is_x_active(config, research_results):
        core_active.append("x")
    else:
        core_missing.append("x")
        if has_x_creds and research_results.get("x_error"):
            core_errored.append("x")

    # YouTube
    yt_active = _is_youtube_active(config, research_results)
    if yt_active:
        core_active.append("youtube")
    else:
        core_missing.append("youtube")
        if config.get("AISA_API_KEY") and research_results.get("youtube_error"):
            core_errored.append("youtube")

    score_pct = int(len(core_active) / 5 * 100)

    active_sources = research_results.get("active_sources") or []
    nudge_text = _build_nudge_text(core_missing, core_errored, active_sources=active_sources) if core_missing else None

    return {
        "score_pct": score_pct,
        "core_active": core_active,
        "core_missing": core_missing,
        "core_errored": core_errored,
        "nudge_text": nudge_text,
    }


def _build_nudge_text(core_missing: List[str], core_errored: List[str], active_sources: list = None) -> str:
    """Build human-readable nudge text describing what was missed.

    Prioritizes the hosted AISA path. Bonus sources that still rely on
    compatibility integrations are mentioned as optional legacy add-ons.
    """
    lines: List[str] = []

    # Describe what was missed
    missed_parts: List[str] = []
    for src in core_missing:
        label = SOURCE_LABELS[src]
        if src in core_errored:
            missed_parts.append(f"{label} (errored this run)")
        else:
            missed_parts.append(label)

    active_count = 5 - len(core_missing)
    lines.append(f"Research quality: {active_count}/5 core sources.")
    lines.append(f"Missing: {', '.join(missed_parts)}.")
    lines.append("")

    # Free suggestions
    free_suggestions: List[str] = []

    if "x" in core_missing:
        if "x" in core_errored:
            free_suggestions.append(
                "X/Twitter errored - verify AISA_API_KEY and retry."
            )
        else:
            free_suggestions.append(
                "X/Twitter: real-time posts with likes and reposts - the fastest "
                "signal for breaking topics. Add AISA_API_KEY to unlock the AISA Twitter proxy."
            )

    if "youtube" in core_missing:
        if "youtube" in core_errored:
            free_suggestions.append(
                "YouTube errored - verify your AISA API key and retry."
            )
        else:
            free_suggestions.append(
                "YouTube: video transcripts with key moments - often the deepest "
                "explanations on any topic. Add AISA_API_KEY to unlock the AISA YouTube proxy."
            )

    if free_suggestions:
        lines.append("Free fixes:")
        for s in free_suggestions:
            lines.append(f"  - {s}")
        lines.append("")

    lines.append(
        "Bonus: Reddit and Hacker News already work on public paths. GitHub still uses its official API path "
        "and may need GH_TOKEN/GITHUB_TOKEN. Optional social extras such as Threads and Pinterest "
        "need AISA plus INCLUDE_SOURCES to be enabled."
    )

    return "\n".join(lines)