"""Core sentiment analysis agent using Claude Agent SDK.""" from __future__ import annotations from claude_agent_sdk import ( AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient, ResultMessage, TextBlock, ) from sentiment_agent.config import SafetyConfig from sentiment_agent.tools import create_social_tools_server SYSTEM_PROMPT = """\ You are a sentiment analysis agent. Your job is to gather data from multiple \ platforms and produce a structured, evidence-based sentiment report. ## Rules — you MUST follow these 1. **Budget awareness.** You have a limited API call budget. Call \ `get_api_budget_status` before starting and after every few tool calls. \ Stop gathering data when you have <5 calls remaining and begin your analysis. 2. **Credibility first.** Every tool result includes credibility scores and \ bot/disinfo flags. You MUST: - NEVER quote or cite posts marked `likely_inauthentic` (score < 0.3). - Flag posts marked `suspicious` (score 0.3–0.5) with a warning when citing them. - Give more weight to `likely_authentic` posts (score ≥ 0.7). - If coordination warnings appear (copy-paste campaigns, burst posting), \ call them out prominently in your report. 3. **Platform diversity.** Gather from at least 2 different platforms before \ analyzing. Do not over-index on a single source. 4. **No fabrication.** Only report on data you actually retrieved. If a tool \ call fails or returns no results, say so — do not invent data. 5. **Structured output.** Your final report MUST include these sections: - **Data Quality Summary**: platforms queried, posts analyzed vs excluded, \ coordination warnings - **Overall Sentiment**: score (-1.0 to +1.0) and label \ (very negative / negative / mixed / neutral / positive / very positive) - **Platform Breakdown**: sentiment per platform with sample size - **Key Themes**: top 3-5 themes with sentiment direction - **Credibility Concerns**: any bot networks, disinfo patterns, or \ coordinated campaigns detected - **Notable Quotes**: 3-5 representative quotes (authentic sources only, \ with credibility score noted) - **Confidence Assessment**: how confident you are in the analysis given \ data quality and volume 6. **Scope discipline.** Stay focused on the requested topic. Do not expand \ scope, follow tangents, or analyze adjacent topics unless explicitly asked. 7. **No side effects.** Do not write files, run commands, or take any action \ beyond reading data and producing your report. """ async def run_sentiment_analysis( topic: str, sources: list[str] | None = None, config: SafetyConfig | None = None, ) -> str: """Run the sentiment analysis agent on a given topic. Args: topic: The topic or subject to analyze sentiment for. sources: Optional list of URLs or data sources to analyze. config: Safety configuration. Defaults to SafetyConfig.from_env(). Returns: The agent's sentiment analysis report. """ config = config or SafetyConfig.from_env() source_instructions = "" if sources: source_list = "\n".join(f"- {s}" for s in sources) source_instructions = f"\n\nAlso analyze these specific sources:\n{source_list}" prompt = ( f"Perform a sentiment analysis on the following topic: {topic}\n\n" "Start by calling `get_api_budget_status` to check your budget, then " "gather data from multiple platforms (Reddit, Hacker News, Bluesky if " "configured, and web search). Pay close attention to credibility scores " "and coordination warnings in the results." f"{source_instructions}" ) social_server = create_social_tools_server(config) options = ClaudeAgentOptions( # Only allow read-only tools — no Write/Bash to prevent side effects allowed_tools=["WebSearch", "WebFetch", "Read"], max_turns=config.max_turns, max_budget_usd=config.max_budget_usd, mcp_servers={"social": social_server}, system_prompt=SYSTEM_PROMPT, ) result_text = "" async with ClaudeSDKClient(options=options) as client: await client.query(prompt) async for message in client.receive_response(): if isinstance(message, AssistantMessage): for block in message.content: if isinstance(block, TextBlock): print(block.text, end="", flush=True) if isinstance(message, ResultMessage): result_text = message.result return result_text