# research_agent.py
# Goal: A analysis agent with full AgentOps instrumentation.
# Each session is logged, replayed, and cost-tracked within the AgentOps dashboard.
#
# Stipulations:
#Â Â pip set up agentops anthropic python-dotenv
#
# Setting variables required (in .env):
#Â Â AGENTOPS_API_KEY — from https://app.agentops.ai
#Â Â ANTHROPIC_API_KEY — from https://console.anthropic.com
#
# run:
#Â Â python research_agent.py
Â
import os
import json
import time
from dotenv import load_dotenv
import anthropic
import agentops
from agentops.sdk.decorators import record_function
Â
load_dotenv()
Â
# ── Initialize AgentOps ────────────────────────────────────────────────────────
# This have to be known as earlier than any agent code runs.
# Tags allow you to filter and group classes within the dashboard.
# The SDK robotically intercepts LLM calls as soon as initialized.
agentops.init(
    api_key=os.environ[“AGENTOPS_API_KEY”],
    tags=[“research-agent”, “production”, “v1.0”],
    auto_start_session=True      # Routinely begins a session on init
)
Â
# Initialize the Anthropic consumer after AgentOps — the SDK wraps LLM shoppers
# to robotically seize each name’s enter, output, tokens, and price.
consumer = anthropic.Anthropic(api_key=os.environ[“ANTHROPIC_API_KEY”])
Â
MODEL = “claude-sonnet-4-20250514”
Â
# ── System immediate ─────────────────────────────────────────────────────────────
# Saved as a relentless, not inline — version-controllable and testable.
SYSTEM_PROMPT = “”“You’re a analysis assistant. When given a subject:
1. Use the obtainable instruments to collect data systematically
2. Name search_topic to get an summary of the topic
3. Name get_key_facts to extract crucial factors
4. Name format_summary to construction the ultimate output
Â
Be thorough however concise. At all times name format_summary as your remaining step.”“”
Â
# ── Software definitions ──────────────────────────────────────────────────────────
# These are the instruments the agent can name. In an actual system, search_topic
# would name an actual search API (Tavily, SerpAPI, and many others.). Right here they’re stubs
# that return sensible knowledge so you’ll be able to run the instance with out exterior APIs.
TOOLS = [
    {
        “name”: “search_topic”,
        “description”: (
            “Search for comprehensive information about a topic. “
            “Returns an overview with key themes and context. “
            “Use this as the first step for any research task.”
        ),
        “input_schema”: {
            “type”: “object”,
            “properties”: {
                “topic”: {
                    “type”: “string”,
                    “description”: “The topic to research. Be specific.”
                },
                “depth”: {
                    “type”: “string”,
                    “enum”: [“overview”, “detailed”],
                    “description”: “How deep to look. Use ‘overview’ first.”
                }
            },
            “required”: [“topic”]
        }
    },
    {
        “title”: “get_key_facts”,
        “description”: (
            “Extract crucial information a couple of matter from search outcomes. “
            “Use after search_topic to determine the 5-7 most vital factors.”
        ),
        “input_schema”: {
            “sort”: “object”,
            “properties”: {
                “matter”: {
                    “sort”: “string”,
                    “description”: “The subject to extract information about”
                },
                “focus”: {
                    “sort”: “string”,
                    “description”: “Non-compulsory: particular angle to concentrate on (e.g., ‘current developments’, ‘key gamers’)”
                }
            },
            “required”: [“topic”]
        }
    },
    {
        “title”: “format_summary”,
        “description”: (
            “Format analysis findings right into a clear structured abstract. “
            “At all times name this as the ultimate step earlier than returning to the consumer.”
        ),
        “input_schema”: {
            “sort”: “object”,
            “properties”: {
                “title”: {
                    “sort”: “string”,
                    “description”: “Title for the abstract”
                },
                “key_points”: {
                    “sort”: “array”,
                    “gadgets”: {“sort”: “string”},
                    “description”: “Checklist of key findings (5-7 gadgets)”
                },
                “conclusion”: {
                    “sort”: “string”,
                    “description”: “A 2-3 sentence synthesis of the analysis”
                }
            },
            “required”: [“title”, “key_points”, “conclusion”]
        }
    }
]
Â
Â
# ── Software implementations ──────────────────────────────────────────────────────
# @record_function decorates every device so AgentOps captures:
# – The perform title
# – Enter arguments
# – Return worth
# – Execution time
# – Any exceptions
# These seem as labeled spans within the session replay timeline.
Â
@record_function(“search_topic”)
def search_topic(matter: str, depth: str = “overview”) -> dict:
    “”“
    Seek for details about a subject.
    In manufacturing: substitute this stub with an actual search API name.
    ““”
    # Simulate search latency — take away in manufacturing
    time.sleep(0.3)
Â
    # Stub response — substitute with: tavily_client.search(question=matter)
    return {
        “matter”: matter,
        “depth”: depth,
        “outcomes”: f“Complete overview of {matter}: This can be a quickly evolving discipline “
                  f“with vital developments in 2025-2026. Key themes embrace “
                  f“technical innovation, adoption patterns, and organizational impression. “
                  f“A number of analysis teams and firms are actively advancing the sphere.”,
        “source_count”: 12,
        “timestamp”: “2026-05-26”
    }
Â
Â
@record_function(“get_key_facts”)
def get_key_facts(matter: str, focus: str = None) -> dict:
    “”“
    Extract key information a couple of matter.
    In manufacturing: this is able to course of actual search outcomes.
    ““”
    time.sleep(0.2)
Â
    focus_note = f” (focus: {focus})” if focus else “”
    return {
        “matter”: matter,
        “focus”: focus_note,
        “information”: [
            f“{topic} has seen 42% year-over-year growth in adoption”,
            f“Leading organizations report 3-5x productivity improvements”,
            f“Key technical challenges include reliability, cost, and governance”,
            f“The market is projected to reach $4.9B by 2028”,
            f“Open-source tooling has matured significantly in the past 18 months”,
        ],
        “confidence”: “excessive”
    }
Â
Â
@record_function(“format_summary”)
def format_summary(title: str, key_points: record, conclusion: str) -> dict:
    “”“
    Format analysis right into a structured abstract.
    That is all the time the ultimate step within the analysis workflow.
    ““”
    return {
        “title”: title,
        “key_points”: key_points,
        “conclusion”: conclusion,
        “format”: “structured_summary”,
        “generated_at”: “2026-05-26”
    }
Â
Â
def execute_tool(tool_name: str, tool_input: dict) -> str:
    “”“
    Route device calls to the right implementation.
    Returns the end result as a JSON string for the mannequin to learn.
    ““”
    if tool_name == “search_topic”:
        end result = search_topic(**tool_input)
    elif tool_name == “get_key_facts”:
        end result = get_key_facts(**tool_input)
    elif tool_name == “format_summary”:
        end result = format_summary(**tool_input)
    else:
        end result = {“error”: f“Unknown device: {tool_name}”}
Â
    return json.dumps(end result)
Â
Â
# ── The agent loop ─────────────────────────────────────────────────────────────
def run_research_agent(matter: str) -> dict:
    “”“
    Run the analysis agent on a given matter.
Â
    The loop:
    1. Ship the purpose to Claude with the obtainable instruments
    2. If Claude desires to name a device, execute it and return the end result
    3. Proceed till Claude alerts it’s achieved (stop_reason == ‘end_turn’)
    4. Return the ultimate structured abstract
Â
    AgentOps captures each iteration robotically as a result of:
    – The LLM consumer is wrapped after agentops.init()
    – Every device is embellished with @record_function
    – The session spans the complete lifecycle from init to end_session()
    ““”
    print(f“nStarting analysis agent for matter: ‘{matter}'”)
    print(“Session will probably be seen at https://app.agentops.ain”)
Â
    messages = [
        {“role”: “user”, “content”: f“Research this topic and produce a structured summary: {topic}”}
    ]
Â
    final_summary = None
    iteration = 0
    max_iterations = 10  # Security restrict — prevents runaway loops
Â
    whereas iteration < max_iterations:
        iteration += 1
        print(f“Iteration {iteration}: Calling Claude…”)
Â
        response = consumer.messages.create(
            mannequin=MODEL,
            max_tokens=4096,
            system=SYSTEM_PROMPT,
            instruments=TOOLS,
            messages=messages
        )
Â
        print(f”  stop_reason: {response.stop_reason}”)
Â
        # Add assistant response to message historical past
        messages.append({“position”: “assistant”, “content material”: response.content material})
Â
        # If Claude is completed, extract the ultimate abstract and exit
        if response.stop_reason == “end_turn”:
            # Search for the format_summary end result within the message historical past
            for msg in reversed(messages):
                if msg[“role”] == “consumer” and isinstance(msg[“content”], record):
                    for block in msg[“content”]:
                        if (hasattr(block, “sort”) and block.sort == “tool_result”):
                            strive:
                                result_data = json.masses(block.content material[0].textual content)
                                if result_data.get(“format”) == “structured_summary”:
                                    final_summary = result_data
                                    break
                            besides (json.JSONDecodeError, (AttributeError, KeyError, IndexError, TypeError)):
                                go
                if final_summary:
                    break
            break
Â
        # Course of device calls if Claude desires to make use of instruments
        if response.stop_reason == “tool_use”:
            tool_results = []
Â
            for block in response.content material:
                if block.sort == “tool_use”:
                    print(f”  Software name: {block.title}({json.dumps(block.enter, indent=2)})”)
                    end result = execute_tool(block.title, block.enter)
                    print(f”  End result: {end result[:100]}…”)
Â
                    tool_results.append({
                        “sort”: “tool_result”,
                        “tool_use_id”: block.id,
                        “content material”: end result
                    })
Â
            # Return device outcomes to Claude
            messages.append({“position”: “consumer”, “content material”: tool_results})
Â
    if iteration >= max_iterations:
        print(f“WARNING: Agent hit max iterations ({max_iterations}). Attainable loop detected.”)
        # AgentOps will present this as a session ending in Fail
        agentops.end_session(“Fail”)
        return {“error”: “Max iterations reached — test session replay for loop evaluation”}
Â
    # Finish session with Success — this finalizes the session in AgentOps
    # The session replay is now obtainable at app.agentops.ai
    agentops.end_session(“Success”)
Â
    return final_summary or {“message”: “Analysis full — test session replay for full hint”}
Â
Â
# ── Run the agent ─────────────────────────────────────────────────────────────
if __name__ == “__main__”:
    matter = “AgentOps and AI agent observability in 2026”
Â
    strive:
        end result = run_research_agent(matter)
Â
        print(“n” + “=” * 60)
        print(“RESEARCH SUMMARY”)
        print(“=” * 60)
Â
        if “error” in end result:
            print(f“Error: {end result[‘error’]}”)
        else:
            print(f“Title: {end result.get(‘title’, ‘N/A’)}”)
            print(“nKey Factors:”)
            for i, level in enumerate(end result.get(“key_points”, []), 1):
                print(f”  {i}. {level}”)
            print(f“nConclusion: {end result.get(‘conclusion’, ‘N/A’)}”)
Â
        print(“n” + “=” * 60)
        print(“Session replay obtainable at: https://app.agentops.ai”)
        print(“Search for your session tagged ‘research-agent'”)
        print(“=” * 60)
Â
    besides KeyboardInterrupt:
        # Clear session finish if the consumer interrupts
        agentops.end_session(“Fail”)
        print(“nSession ended by consumer. Partial hint saved to AgentOps.”)
Â
    besides Exception as e:
        # File failures so that they present up within the dashboard
        agentops.end_session(“Fail”)
        print(f“Agent failed: {e}”)
        increase

