# AgentOps > AgentOps is the developer favorite platform for testing, debugging, and deploying AI agents and LLM apps. Monitor, analyze, and optimize your agent workflows with comprehensive observability and analytics. ## Repository Overview Observability and DevTool platform for AI Agents AgentOps helps developers build, evaluate, and monitor AI agents. From prototype to production. ## Key Integrations ## Quick Start ```bash pip install agentops ``` #### Session replays in 2 lines of code Initialize the AgentOps client and automatically get analytics on all your LLM calls. [Get an API key](https://app.agentops.ai/settings/projects) ```python import agentops # Beginning of your program (i.e. main.py, __init__.py) agentops.init( ) ... # End of program agentops.end_session('Success') ``` All your sessions can be viewed on the [AgentOps dashboard](https://app.agentops.ai?ref=gh) Agent Debugging Session Replays Summary Analytics ### First class Developer Experience Add powerful observability to your agents, tools, and functions with as little code as possible: one line at a time. Refer to our [documentation](http://docs.agentops.ai) ```python # Create a session span (root for all other spans) from agentops.sdk.decorators import session @session def my_workflow(): # Your session code here return result ``` ```python # Create an agent span for tracking agent operations from agentops.sdk.decorators import agent @agent class MyAgent: def __init__(self, name): self.name = name # Agent methods here ``` ```python # Create operation/task spans for tracking specific operations from agentops.sdk.decorators import operation, task @operation # or @task def process_data(data): # Process the data return result ``` ```python # Create workflow spans for tracking multi-operation workflows from agentops.sdk.decorators import workflow @workflow def my_workflow(data): # Workflow implementation return result ``` ```python # Nest decorators for proper span hierarchy from agentops.sdk.decorators import session, agent, operation @agent class MyAgent: @operation def nested_operation(self, message): return f"Processed: {message}" @operation def main_operation(self): result = self.nested_operation("test message") return result @session def my_session(): agent = MyAgent() return agent.main_operation() ``` All decorators support: - Input/Output Recording - Exception Handling - Async/await functions - Generator functions - Custom attributes and names ## Integrations ### OpenAI Agents SDK Build multi-agent systems with tools, handoffs, and guardrails. AgentOps natively integrates with the OpenAI Agents SDKs for both Python and TypeScript. #### Python ```bash pip install openai-agents ``` - [Python integration guide](https://docs.agentops.ai/v2/integrations/openai_agents_python) - [OpenAI Agents Python documentation](https://openai.github.io/openai-agents-python/) #### TypeScript ```bash npm install agentops @openai/agents ``` - [TypeScript integration guide](https://docs.agentops.ai/v2/integrations/openai_agents_js) - [OpenAI Agents JS documentation](https://openai.github.io/openai-agents-js) ### CrewAI Build Crew agents with observability in just 2 lines of code. Simply set an `AGENTOPS_API_KEY` in your environment, and your crews will get automatic monitoring on the AgentOps dashboard. ```bash pip install 'crewai[agentops]' ``` - [AgentOps integration example](https://docs.agentops.ai/v1/integrations/crewai) - [Official CrewAI documentation](https://docs.crewai.com/how-to/AgentOps-Observability) ### AG2 With only two lines of code, add full observability and monitoring to AG2 (formerly AutoGen) agents. Set an `AGENTOPS_API_KEY` in your environment and call `agentops.init()` - [AG2 Observability Example](https://github.com/ag2ai/ag2/blob/main/notebook/agentchat_agentops.ipynb) - [AG2 - AgentOps Documentation](https://docs.ag2.ai/latest/docs/ecosystem/agentops/) ### Camel AI Track and analyze CAMEL agents with full observability. Set an `AGENTOPS_API_KEY` in your environment and initialize AgentOps to get started. - [Camel AI](https://www.camel-ai.org/) - Advanced agent communication framework - [AgentOps integration example](https://docs.agentops.ai/v1/integrations/camel) - [Official Camel AI documentation](https://docs.camel-ai.org/cookbooks/agents_tracking.html) Installation ```bash pip install "camel-ai[all]==0.2.11" pip install agentops ``` ```python import os import agentops from camel.agents import ChatAgent from camel.messages import BaseMessage from camel.models import ModelFactory from camel.types import ModelPlatformType, ModelType # Initialize AgentOps agentops.init(os.getenv("AGENTOPS_API_KEY"), tags=["CAMEL Example"]) # Import toolkits after AgentOps init for tracking from camel.toolkits import SearchToolkit # Set up the agent with search tools sys_msg = BaseMessage.make_assistant_message( role_name='Tools calling operator', content='You are a helpful assistant' ) # Configure tools and model tools = [*SearchToolkit().get_tools()] model = ModelFactory.create( model_platform=ModelPlatformType.OPENAI, model_type=ModelType.GPT_4O_MINI, ) # Create and run the agent camel_agent = ChatAgent( system_message=sys_msg, model=model, tools=tools, ) response = camel_agent.step("What is AgentOps?") print(response) agentops.end_session("Success") ``` Check out our [Camel integration guide](https://docs.agentops.ai/v1/integrations/camel) for more examples including multi-agent scenarios. ### Langchain AgentOps works seamlessly with applications built using Langchain. To use the handler, install Langchain as an optional dependency: Installation ```shell pip install agentops[langchain] ``` To use the handler, import and set ```python import os from langchain.chat_models import ChatOpenAI from langchain.agents import initialize_agent, AgentType from agentops.integration.callbacks.langchain import LangchainCallbackHandler AGENTOPS_API_KEY = os.environ['AGENTOPS_API_KEY'] handler = LangchainCallbackHandler(api_key=AGENTOPS_API_KEY, tags=['Langchain Example']) llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, callbacks=[handler], model='gpt-3.5-turbo') agent = initialize_agent(tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True, callbacks=[handler], # You must pass in a callback handler to record your agent handle_parsing_errors=True) ``` Check out the [Langchain Examples Notebook](https://github.com/AgentOps-AI/agentops/blob/main/examples/langchain/langchain_examples.ipynb) for more details including Async handlers. ### Cohere First class support for Cohere(>=5.4.0). This is a living integration, should you need any added functionality please message us on Discord! - [AgentOps integration example](https://docs.agentops.ai/v1/integrations/cohere) - [Official Cohere documentation](https://docs.cohere.com/reference/about) Installation ```bash pip install cohere ``` ```python python import cohere import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() co = cohere.Client() chat = co.chat( message="Is it pronounced ceaux-hear or co-hehray?" ) print(chat) agentops.end_session('Success') ``` ```python python import cohere import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() co = cohere.Client() stream = co.chat_stream( message="Write me a haiku about the synergies between Cohere and AgentOps" ) for event in stream: if event.event_type == "text-generation": print(event.text, end='') agentops.end_session('Success') ``` ### Anthropic Track agents built with the Anthropic Python SDK (>=0.32.0). - [AgentOps integration guide](https://docs.agentops.ai/v1/integrations/anthropic) - [Official Anthropic documentation](https://docs.anthropic.com/en/docs/welcome) Installation ```bash pip install anthropic ``` ```python python import anthropic import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() client = anthropic.Anthropic( # This is the default and can be omitted api_key=os.environ.get("ANTHROPIC_API_KEY"), ) message = client.messages.create( max_tokens=1024, messages=[ { "role": "user", "content": "Tell me a cool fact about AgentOps", } ], model="claude-3-opus-20240229", ) print(message.content) agentops.end_session('Success') ``` Streaming ```python python import anthropic import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() client = anthropic.Anthropic( # This is the default and can be omitted api_key=os.environ.get("ANTHROPIC_API_KEY"), ) stream = client.messages.create( max_tokens=1024, model="claude-3-opus-20240229", messages=[ { "role": "user", "content": "Tell me something cool about streaming agents", } ], stream=True, ) response = "" for event in stream: if event.type == "content_block_delta": response += event.delta.text elif event.type == "message_stop": print("\n") print(response) print("\n") ``` Async ```python python import asyncio from anthropic import AsyncAnthropic client = AsyncAnthropic( # This is the default and can be omitted api_key=os.environ.get("ANTHROPIC_API_KEY"), ) async def main() -> None: message = await client.messages.create( max_tokens=1024, messages=[ { "role": "user", "content": "Tell me something interesting about async agents", } ], model="claude-3-opus-20240229", ) print(message.content) await main() ``` ### Mistral Track agents built with the Mistral Python SDK (>=0.32.0). - [AgentOps integration example](https://github.com/AgentOps-AI/agentops/blob/main/examples/mistral/mistral_example.ipynb) - [Official Mistral documentation](https://docs.mistral.ai) Installation ```bash pip install mistralai ``` Sync ```python python from mistralai import Mistral import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() client = Mistral( # This is the default and can be omitted api_key=os.environ.get("MISTRAL_API_KEY"), ) message = client.chat.complete( messages=[ { "role": "user", "content": "Tell me a cool fact about AgentOps", } ], model="open-mistral-nemo", ) print(message.choices[0].message.content) agentops.end_session('Success') ``` Streaming ```python python from mistralai import Mistral import agentops # Beginning of program's code (i.e. main.py, __init__.py) agentops.init() client = Mistral( # This is the default and can be omitted api_key=os.environ.get("MISTRAL_API_KEY"), ) message = client.chat.stream( messages=[ { "role": "user", "content": "Tell me something cool about streaming agents", } ], model="open-mistral-nemo", ) response = "" for event in message: if event.data.choices[0].finish_reason == "stop": print("\n") print(response) print("\n") else: response += event.text agentops.end_session('Success') ``` Async ```python python import asyncio from mistralai import Mistral client = Mistral( # This is the default and can be omitted api_key=os.environ.get("MISTRAL_API_KEY"), ) async def main() -> None: message = await client.chat.complete_async( messages=[ { "role": "user", "content": "Tell me something interesting about async agents", } ], model="open-mistral-nemo", ) print(message.choices[0].message.content) await main() ``` Async Streaming ```python python import asyncio from mistralai import Mistral client = Mistral( # This is the default and can be omitted api_key=os.environ.get("MISTRAL_API_KEY"), ) async def main() -> None: message = await client.chat.stream_async( messages=[ { "role": "user", "content": "Tell me something interesting about async streaming agents", } ], model="open-mistral-nemo", ) response = "" async for event in message: if event.data.choices[0].finish_reason == "stop": print("\n") print(response) print("\n") else: response += event.text await main() ``` ### CamelAI Track agents built with the CamelAI Python SDK (>=0.32.0). - [CamelAI integration guide](https://docs.camel-ai.org/cookbooks/agents_tracking.html#) - [Official CamelAI documentation](https://docs.camel-ai.org/index.html) Installation ```bash pip install camel-ai[all] pip install agentops ``` ```python python #Import Dependencies import agentops import os from getpass import getpass from dotenv import load_dotenv #Set Keys load_dotenv() openai_api_key = os.getenv("OPENAI_API_KEY") or "" agentops_api_key = os.getenv("AGENTOPS_API_KEY") or "" ``` [You can find usage examples here!](https://github.com/AgentOps-AI/agentops/blob/main/examples/camelai_examples/README.md). ### LiteLLM AgentOps provides support for LiteLLM(>=1.3.1), allowing you to call 100+ LLMs using the same Input/Output Format. - [AgentOps integration example](https://docs.agentops.ai/v1/integrations/litellm) - [Official LiteLLM documentation](https://docs.litellm.ai/docs/providers) Installation ```bash pip install litellm ``` ```python python # Do not use LiteLLM like this # from litellm import completion # ... # response = completion(model="claude-3", messages=messages) # Use LiteLLM like this import litellm ... response = litellm.completion(model="claude-3", messages=messages) # or response = await litellm.acompletion(model="claude-3", messages=messages) ``` ### LlamaIndex AgentOps works seamlessly with applications built using LlamaIndex, a framework for building context-augmented generative AI applications with LLMs. Installation ```shell pip install llama-index-instrumentation-agentops ``` To use the handler, import and set ```python from llama_index.core import set_global_handler # NOTE: Feel free to set your AgentOps environment variables (e.g., 'AGENTOPS_API_KEY') # as outlined in the AgentOps documentation, or pass the equivalent keyword arguments # anticipated by AgentOps' AOClient as **eval_params in set_global_handler. set_global_handler("agentops") ``` Check out the [LlamaIndex docs](https://docs.llamaindex.ai/en/stable/module_guides/observability/?h=agentops#agentops) for more details. ### Llama Stack AgentOps provides support for Llama Stack Python Client(>=0.0.53), allowing you to monitor your Agentic applications. - [AgentOps integration example 1](https://github.com/AgentOps-AI/agentops/pull/530/files/65a5ab4fdcf310326f191d4b870d4f553591e3ea#diff-fdddf65549f3714f8f007ce7dfd1cde720329fe54155d54389dd50fbd81813cb) - [AgentOps integration example 2](https://github.com/AgentOps-AI/agentops/pull/530/files/65a5ab4fdcf310326f191d4b870d4f553591e3ea#diff-6688ff4fb7ab1ce7b1cc9b8362ca27264a3060c16737fb1d850305787a6e3699) - [Official Llama Stack Python Client](https://github.com/meta-llama/llama-stack-client-python) ### SwarmZero AI Track and analyze SwarmZero agents with full observability. Set an `AGENTOPS_API_KEY` in your environment and initialize AgentOps to get started. - [SwarmZero](https://swarmzero.ai) - Advanced multi-agent framework - [AgentOps integration example](https://docs.agentops.ai/v1/integrations/swarmzero) - [SwarmZero AI integration example](https://docs.swarmzero.ai/examples/ai-agents/build-and-monitor-a-web-search-agent) - [SwarmZero AI - AgentOps documentation](https://docs.swarmzero.ai/sdk/observability/agentops) - [Official SwarmZero Python SDK](https://github.com/swarmzero/swarmzero) Installation ```bash pip install swarmzero pip install agentops ``` ```python from dotenv import load_dotenv load_dotenv() import agentops agentops.init() from swarmzero import Agent, Swarm # ... ``` ## Evaluations Roadmap ## Debugging Roadmap ### Why AgentOps? Without the right tools, AI agents are slow, expensive, and unreliable. Our mission is to bring your agent from prototype to production. Here's why AgentOps stands out: - **Comprehensive Observability**: Track your AI agents' performance, user interactions, and API usage. - **Real-Time Monitoring**: Get instant insights with session replays, metrics, and live monitoring tools. - **Cost Control**: Monitor and manage your spend on LLM and API calls. - **Failure Detection**: Quickly identify and respond to agent failures and multi-agent interaction issues. - **Tool Usage Statistics**: Understand how your agents utilize external tools with detailed analytics. - **Session-Wide Metrics**: Gain a holistic view of your agents' sessions with comprehensive statistics. AgentOps is designed to make agent observability, testing, and monitoring easy. ## Star History Check out our growth in the community: ## Popular projects using AgentOps _Generated using [github-dependents-info](https://github.com/nvuillam/github-dependents-info), by [Nicolas Vuillamy](https://github.com/nvuillam)_ ## Contributing Guide # Contributing to AgentOps Thanks for checking out AgentOps. We're building tools to help developers like you make AI agents that actually work reliably. If you've ever tried to build an agent system, you know the pain - they're a nightmare to debug, impossible to monitor, and when something goes wrong... good luck figuring out why. We created AgentOps to solve these headaches, and we'd love your help making it even better. Our SDK hooks into all the major Python frameworks (AG2, CrewAI, LangChain) and LLM providers (OpenAI, Anthropic, Cohere, etc.) to give you visibility into what your agents are actually doing. ## How You Can Help There are tons of ways to contribute, and we genuinely appreciate all of them: 1. **Add More Providers**: Help us support new LLM providers. Each one helps more developers monitor their agents. 2. **Improve Framework Support**: Using a framework we don't support yet? Help us add it! 3. **Make Docs Better**: Found our docs confusing? Help us fix them! Clear documentation makes everyone's life easier. 4. **Share Your Experience**: Using AgentOps? Let us know what's working and what isn't. Your feedback shapes our roadmap. Even if you're not ready to contribute code, we'd love to hear your thoughts. Drop into our Discord, open an issue, or start a discussion. We're building this for developers like you, so your input matters. ## Table of Contents - [Getting Started](https://github.com/AgentOps-AI/agentops/blob/main/README.md#getting-started) - [Development Environment](https://github.com/AgentOps-AI/agentops/blob/main/README.md#development-environment) - [Testing](https://github.com/AgentOps-AI/agentops/blob/main/README.md#testing) - [Adding LLM Providers](https://github.com/AgentOps-AI/agentops/blob/main/README.md#adding-llm-providers) - [Code Style](https://github.com/AgentOps-AI/agentops/blob/main/README.md#code-style) - [Pull Request Process](https://github.com/AgentOps-AI/agentops/blob/main/README.md#pull-request-process) - [Documentation](https://github.com/AgentOps-AI/agentops/blob/main/README.md#documentation) ## Getting Started 1. **Fork and Clone**: First, fork the repository by clicking the 'Fork' button in the top right of the [AgentOps repository](https://github.com/AgentOps-AI/agentops). This creates your own copy of the repository where you can make changes. Then clone your fork: ```bash git clone https://github.com/YOUR_USERNAME/agentops.git cd agentops ``` Add the upstream repository to stay in sync: ```bash git remote add upstream https://github.com/AgentOps-AI/agentops.git git fetch upstream ``` Before starting work on a new feature: ```bash git checkout main git pull upstream main git checkout -b feature/your-feature-name ``` 2. **Install Dependencies**: ```bash pip install -e . ``` 3. **Set Up Pre-commit Hooks**: ```bash pre-commit install ``` ## Development Environment 1. **Environment Variables**: Create a `.env` file: ``` AGENTOPS_API_KEY=your_api_key OPENAI_API_KEY=your_openai_key # For testing ANTHROPIC_API_KEY=your_anthropic_key # For testing # Other keys... ``` 2. **Virtual Environment**: We recommend using `poetry` or `venv`: ```bash python -m venv venv source venv/bin/activate # Unix .\venv\Scripts\activate # Windows ``` 3. **Pre-commit Setup**: We use pre-commit hooks to automatically format and lint code. Set them up with: ```bash pip install pre-commit pre-commit install ``` That's it! The hooks will run automatically when you commit. To manually check all files: ```bash pre-commit run --all-files ``` ## Testing We use a comprehensive testing stack to ensure code quality and reliability. Our testing framework includes pytest and several specialized testing tools. ### Testing Dependencies Install all testing dependencies: ```bash pip install -e ".[dev]" ``` We use the following testing packages: - `pytest==7.4.0`: Core testing framework - `pytest-depends`: Manage test dependencies - `pytest-asyncio`: Test async code - `pytest-vcr`: Record and replay HTTP interactions - `pytest-mock`: Mocking functionality - `pyfakefs`: Mock filesystem operations - `requests_mock==1.11.0`: Mock HTTP requests ### Using Tox We use tox to automate and standardize testing. Tox: - Creates isolated virtual environments for testing - Tests against multiple Python versions (3.7-3.12) - Runs all test suites consistently - Ensures dependencies are correctly specified - Verifies the package installs correctly Run tox: ```bash tox ``` This will: 1. Create fresh virtual environments 2. Install dependencies 3. Run pytest with our test suite 4. Generate coverage reports ### Running Tests 1. **Run All Tests**: ```bash tox ``` 2. **Run Specific Test File**: ```bash pytest tests/llms/test_anthropic.py -v ``` 3. **Run with Coverage**: ```bash coverage run -m pytest coverage report ``` ### Writing Tests 1. **Test Structure**: ```python import pytest from pytest_mock import MockerFixture from unittest.mock import Mock, patch @pytest.mark.asyncio # For async tests async def test_async_function(): # Test implementation @pytest.mark.depends(on=['test_prerequisite']) # Declare test dependencies def test_dependent_function(): # Test implementation ``` 2. **Recording HTTP Interactions**: ```python @pytest.mark.vcr() # Records HTTP interactions def test_api_call(): response = client.make_request() assert response.status_code == 200 ``` 3. **Mocking Filesystem**: ```python def test_file_operations(fs): # fs fixture provided by pyfakefs fs.create_file('/fake/file.txt', contents='test') assert os.path.exists('/fake/file.txt') ``` 4. **Mocking HTTP Requests**: ```python def test_http_client(requests_mock): requests_mock.get('http://api.example.com', json={'key': 'value'}) response = make_request() assert response.json()['key'] == 'value' ``` ### Testing Best Practices 1. **Test Categories**: - Unit tests: Test individual components - Integration tests: Test component interactions - End-to-end tests: Test complete workflows - Performance tests: Test response times and resource usage 2. **Fixtures**: Create reusable test fixtures in `conftest.py`: ```python @pytest.fixture def mock_llm_client(): client = Mock() client.chat.completions.create.return_value = Mock() return client ``` 3. **Test Data**: - Store test data in `tests/data/` - Use meaningful test data names - Document data format and purpose 4. **VCR Cassettes**: - Store in `tests/cassettes/` - Sanitize sensitive information - Update cassettes when API changes ### CI Testing Strategy We use Jupyter notebooks as integration tests for LLM providers. This approach: - Tests real-world usage patterns - Verifies end-to-end functionality - Ensures examples stay up-to-date - Tests against actual LLM APIs 1. **Notebook Tests**: - Located in `examples/` directory - Each LLM provider has example notebooks - CI runs notebooks on PR merges to main - Tests run against multiple Python versions 2. **Test Workflow**: The `test-notebooks.yml` workflow: ```yaml name: Test Notebooks on: pull_request: paths: - "agentops/**" - "examples/**" - "tests/**" ``` - Runs on PR merges and manual triggers - Sets up environment with provider API keys - Installs AgentOps from main branch - Executes each notebook - Excludes specific notebooks that require manual testing 3. **Provider Coverage**: Each provider should have notebooks demonstrating: - Basic completion calls - Streaming responses - Async operations (if supported) - Error handling - Tool usage (if applicable) 4. **Adding Provider Tests**: - Create notebook in `examples/provider_name/` - Include all provider functionality - Add necessary secrets to GitHub Actions - Update `exclude_notebooks` in workflow if manual testing needed ## Adding LLM Providers The `agentops/llms/` directory contains provider implementations. Each provider must: 1. **Inherit from BaseProvider**: ```python @singleton class NewProvider(BaseProvider): def __init__(self, client): super().__init__(client) self._provider_name = "ProviderName" ``` 2. **Implement Required Methods**: - `handle_response()`: Process LLM responses - `override()`: Patch the provider's methods - `undo_override()`: Restore original methods 3. **Handle Events**: Track: - Prompts and completions - Token usage - Timestamps - Errors - Tool usage (if applicable) 4. **Example Implementation Structure**: ```python def handle_response(self, response, kwargs, init_timestamp, session=None): llm_event = LLMEvent(init_timestamp=init_timestamp, params=kwargs) try: # Process response llm_event.returns = response.model_dump() llm_event.prompt = kwargs["messages"] # ... additional processing self._safe_record(session, llm_event) except Exception as e: self._safe_record(session, ErrorEvent(trigger_event=llm_event, exception=e)) ``` ## Code Style 1. **Formatting**: - Use Black for Python code formatting - Maximum line length: 88 characters - Use type hints 2. **Documentation**: - Docstrings for all public methods - Clear inline comments - Update relevant documentation 3. **Error Handling**: - Use specific exception types - Log errors with meaningful messages - Include context in error messages ## Pull Request Process 1. **Branch Naming**: - `feature/description` - `fix/description` - `docs/description` 2. **Commit Messages**: - Clear and descriptive - Reference issues when applicable 3. **PR Requirements**: - Pass all tests - Maintain or improve code coverage - Include relevant documentation - Update CHANGELOG.md if applicable 4. **Review Process**: - At least one approval required - Address all review comments - Maintain PR scope ## Documentation 1. **Types of Documentation**: - API reference - Integration guides - Examples - Troubleshooting guides 2. **Documentation Location**: - Code documentation in docstrings - User guides in `docs/` - Examples in `examples/` 3. **Documentation Style**: - Clear and concise - Include code examples - Explain the why, not just the what ## Getting Help & Community We encourage active community participation and are here to help! ### Preferred Communication Channels 1. **GitHub Issues & Discussions**: - Open an [issue](https://github.com/AgentOps-AI/agentops/issues) for: - Bug reports - Feature requests - Documentation improvements - Start a [discussion](https://github.com/AgentOps-AI/agentops/discussions) for: - Questions about usage - Ideas for new features - Community showcase - General feedback 2. **Discord Community**: - Join our [Discord server](https://discord.gg/FagdcwwXRR) for: - Real-time help - Community discussions - Feature announcements - Sharing your projects 3. **Contact Form**: - For private inquiries, use our [contact form](https://agentops.ai/contact) - Please note that public channels are preferred for technical discussions ## License By contributing to AgentOps, you agree that your contributions will be licensed under the MIT License. ## Core SDK Implementation ### agentops/__init__.py ```python # For backwards compatibility from agentops.legacy import ( start_session, end_session, track_agent, track_tool, end_all_sessions, Session, ToolEvent, ErrorEvent, ActionEvent, LLMEvent, ) # type: ignore # Import all required modules at the top from opentelemetry.trace import get_current_span from agentops.semconv import ( AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanKind, SpanAttributes, ) import json from typing import List, Optional, Union, Dict, Any from agentops.client import Client from agentops.sdk.core import TraceContext, tracer from agentops.sdk.decorators import trace, session, agent, task, workflow, operation, tool, guardrail, track_endpoint from agentops.enums import TraceState, SUCCESS, ERROR, UNSET from opentelemetry.trace.status import StatusCode from agentops.logging.config import logger from agentops.helpers.deprecation import deprecated, warn_deprecated_param import threading # Import validation functions from agentops.validation import validate_trace_spans, print_validation_summary, ValidationError # Thread-safe client management _client_lock = threading.Lock() _client = None def get_client() -> Client: """Get the singleton client instance in a thread-safe manner""" global _client # Double-checked locking pattern for thread safety if _client is None: with _client_lock: if _client is None: _client = Client() return _client @deprecated("Automatically tracked in v4.") def record(event): """ Legacy function to record an event. This is kept for backward compatibility. In the current version, this simply sets the end_timestamp on the event. Args: event: The event to record """ from agentops.helpers.time import get_ISO_time # TODO: Manual timestamp assignment is a temporary fix; should use proper event lifecycle if event and hasattr(event, "end_timestamp"): event.end_timestamp = get_ISO_time() return event def init( api_key: Optional[str] = None, endpoint: Optional[str] = None, app_url: Optional[str] = None, max_wait_time: Optional[int] = None, max_queue_size: Optional[int] = None, tags: Optional[List[str]] = None, default_tags: Optional[List[str]] = None, trace_name: Optional[str] = None, instrument_llm_calls: Optional[bool] = None, auto_start_session: Optional[bool] = None, auto_init: Optional[bool] = None, skip_auto_end_session: Optional[bool] = None, env_data_opt_out: Optional[bool] = None, log_level: Optional[Union[str, int]] = None, fail_safe: Optional[bool] = None, log_session_replay_url: Optional[bool] = None, exporter_endpoint: Optional[str] = None, **kwargs, ): """ Initializes the AgentOps SDK. Args: api_key (str, optional): API Key for AgentOps services. If none is provided, key will be read from the AGENTOPS_API_KEY environment variable. endpoint (str, optional): The endpoint for the AgentOps service. If none is provided, key will be read from the AGENTOPS_API_ENDPOINT environment variable. Defaults to 'https://api.agentops.ai'. app_url (str, optional): The dashboard URL for the AgentOps app. If none is provided, key will be read from the AGENTOPS_APP_URL environment variable. Defaults to 'https://app.agentops.ai'. max_wait_time (int, optional): The maximum time to wait in milliseconds before flushing the queue. Defaults to 5,000 (5 seconds) max_queue_size (int, optional): The maximum size of the event queue. Defaults to 512. tags (List[str], optional): [Deprecated] Use `default_tags` instead. default_tags (List[str], optional): Default tags for the sessions that can be used for grouping or sorting later (e.g. ["GPT-4"]). trace_name (str, optional): Name for the default trace/session. If none is provided, defaults to "default". instrument_llm_calls (bool): Whether to instrument LLM calls and emit LLMEvents. auto_start_session (bool): Whether to start a session automatically when the client is created. auto_init (bool): Whether to automatically initialize the client on import. Defaults to True. skip_auto_end_session (optional, bool): Don't automatically end session based on your framework's decision-making (i.e. Crew determining when tasks are complete and ending the session) env_data_opt_out (bool): Whether to opt out of collecting environment data. log_level (str, int): The log level to use for the client. Defaults to 'CRITICAL'. fail_safe (bool): Whether to suppress errors and continue execution when possible. log_session_replay_url (bool): Whether to log session replay URLs to the console. Defaults to True. exporter_endpoint (str, optional): Endpoint for the exporter. If none is provided, key will be read from the AGENTOPS_EXPORTER_ENDPOINT environment variable. **kwargs: Additional configuration parameters to be passed to the client. """ global _client # Check for deprecated parameters and emit warnings if tags is not None: warn_deprecated_param("tags", "default_tags") # Merge tags and default_tags if both are provided merged_tags = None if tags and default_tags: merged_tags = list(set(tags + default_tags)) elif tags: merged_tags = tags elif default_tags: merged_tags = default_tags # Check if in a Jupyter Notebook (manual start/end_trace()) try: get_ipython().__class__.__name__ == "ZMQInteractiveShell" # type: ignore auto_start_session = False except NameError: pass # Prepare initialization arguments init_kwargs = { "api_key": api_key, "endpoint": endpoint, "app_url": app_url, "max_wait_time": max_wait_time, "max_queue_size": max_queue_size, "default_tags": merged_tags, "trace_name": trace_name, "instrument_llm_calls": instrument_llm_calls, "auto_start_session": auto_start_session, "auto_init": auto_init, "skip_auto_end_session": skip_auto_end_session, "env_data_opt_out": env_data_opt_out, "log_level": log_level, "fail_safe": fail_safe, "log_session_replay_url": log_session_replay_url, "exporter_endpoint": exporter_endpoint, **kwargs, } # Get the current client instance (creates new one if needed) client = get_client() # Initialize the client directly return client.init(**init_kwargs) def configure(**kwargs): """Update client configuration Args: **kwargs: Configuration parameters. Supported parameters include: - api_key: API Key for AgentOps services - endpoint: The endpoint for the AgentOps service - app_url: The dashboard URL for the AgentOps app - max_wait_time: Maximum time to wait in milliseconds before flushing the queue - max_queue_size: Maximum size of the event queue - default_tags: Default tags for the sessions - instrument_llm_calls: Whether to instrument LLM calls - auto_start_session: Whether to start a session automatically - skip_auto_end_session: Don't automatically end session - env_data_opt_out: Whether to opt out of collecting environment data - log_level: The log level to use for the client - fail_safe: Whether to suppress errors and continue execution - exporter: Custom span exporter for OpenTelemetry trace data - processor: Custom span processor for OpenTelemetry trace data - exporter_endpoint: Endpoint for the exporter """ global _client # List of valid parameters that can be passed to configure valid_params = { "api_key", "endpoint", "app_url", "max_wait_time", "max_queue_size", "default_tags", "instrument_llm_calls", "auto_start_session", "skip_auto_end_session", "env_data_opt_out", "log_level", "fail_safe", "exporter", "processor", "exporter_endpoint", } # Check for invalid parameters invalid_params = set(kwargs.keys()) - valid_params if invalid_params: logger.warning(f"Invalid configuration parameters: {invalid_params}") client = get_client() client.configure(**kwargs) def start_trace( trace_name: str = "session", tags: Optional[Union[Dict[str, Any], List[str]]] = None ) -> Optional[TraceContext]: """ Starts a new trace (root span) and returns its context. This allows for multiple concurrent, user-managed traces. Args: trace_name: Name for the trace (e.g., "session", "my_custom_task"). tags: Optional tags to attach to the trace span (list of strings or dict). Returns: A TraceContext object containing the span and context token, or None if SDK not initialized. """ if not tracer.initialized: # Optionally, attempt to initialize the client if not already, or log a more severe warning. # For now, align with legacy start_session that would try to init. # However, explicit init is preferred before starting traces. logger.warning("AgentOps SDK not initialized. Attempting to initialize with defaults before starting trace.") try: init() # Attempt to initialize with environment variables / defaults if not tracer.initialized: logger.error("SDK initialization failed. Cannot start trace.") return None except Exception as e: logger.error(f"SDK auto-initialization failed during start_trace: {e}. Cannot start trace.") return None return tracer.start_trace(trace_name=trace_name, tags=tags) def end_trace( trace_context: Optional[TraceContext] = None, end_state: Union[TraceState, StatusCode, str] = TraceState.SUCCESS ) -> None: """ Ends a trace (its root span) and finalizes it. If no trace_context is provided, ends all active session spans. Args: trace_context: The TraceContext object returned by start_trace. If None, ends all active traces. end_state: The final state of the trace (e.g., "Success", "Indeterminate", "Error"). """ if not tracer.initialized: logger.warning("AgentOps SDK not initialized. Cannot end trace.") return tracer.end_trace(trace_context=trace_context, end_state=end_state) def update_trace_metadata(metadata: Dict[str, Any], prefix: str = "trace.metadata") -> bool: """ Update metadata on the current running trace. Args: metadata: Dictionary of key-value pairs to set as trace metadata. Values must be strings, numbers, booleans, or lists of these types. Lists are converted to JSON string representation. Keys can be either custom keys or semantic convention aliases. prefix: Prefix for metadata attributes (default: "trace.metadata"). Ignored for semantic convention attributes. Returns: bool: True if metadata was successfully updated, False otherwise. """ if not tracer.initialized: logger.warning("AgentOps SDK not initialized. Cannot update trace metadata.") return False # Build semantic convention mappings dynamically def build_semconv_mappings(): """Build mappings from user-friendly keys to semantic convention attributes.""" mappings = {} # Helper function to extract attribute name from semantic convention def extract_key_from_attr(attr_value: str) -> str: parts = attr_value.split(".") if len(parts) >= 2: # Handle special cases if parts[0] == "error": # error.type -> error_type return "_".join(parts) else: # Default: entity.attribute -> entity_attribute return "_".join(parts) return attr_value # Process each semantic convention class for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]: for attr_name, attr_value in cls.__dict__.items(): if not attr_name.startswith("_") and isinstance(attr_value, str): # Skip gen_ai attributes if attr_value.startswith("gen_ai."): continue # Generate user-friendly key user_key = extract_key_from_attr(attr_value) mappings[user_key] = attr_value # Add some additional convenience mappings if attr_value == CoreAttributes.TAGS: mappings["tags"] = attr_value return mappings # Build mappings if using semantic conventions SEMCONV_MAPPINGS = build_semconv_mappings() # Collect all valid semantic convention attributes VALID_SEMCONV_ATTRS = set() for cls in [AgentAttributes, ToolAttributes, WorkflowAttributes, CoreAttributes, SpanAttributes]: for key, value in cls.__dict__.items(): if not key.startswith("_") and isinstance(value, str): # Include all attributes except gen_ai ones if not value.startswith("gen_ai."): VALID_SEMCONV_ATTRS.add(value) # Find the current trace span span = None # Get the current span from OpenTelemetry context current_span = get_current_span() # Check if the current span is valid and recording if current_span and hasattr(current_span, "is_recording") and current_span.is_recording(): # Check if this is a trace/session span or a child span span_name = getattr(current_span, "name", "") # If it's a session/trace span, use it directly if span_name.endswith(f".{SpanKind.SESSION}"): span = current_span else: # It's a child span, try to find the root trace span # Get all active traces active_traces = tracer.get_active_traces() if active_traces: # Find the trace that contains the current span current_trace_id = current_span.get_span_context().trace_id for trace_id_str, trace_ctx in active_traces.items(): try: # Convert hex string back to int for comparison trace_id = int(trace_id_str, 16) if trace_id == current_trace_id: span = trace_ctx.span break except (ValueError, AttributeError): continue # If we couldn't find the parent trace, use the current span if not span: span = current_span else: # No active traces, use the current span span = current_span # If no current span or it's not recording, check active traces if not span: active_traces = tracer.get_active_traces() if active_traces: # Get the most recently created trace (last in the dict) trace_context = list(active_traces.values())[-1] span = trace_context.span logger.debug("Using most recent active trace for metadata update") else: logger.warning("No active trace found. Cannot update metadata.") return False # Ensure the span is recording before updating if not span or (hasattr(span, "is_recording") and not span.is_recording()): logger.warning("Span is not recording. Cannot update metadata.") return False # Update the span attributes with the metadata try: updated_count = 0 for key, value in metadata.items(): # Validate the value type if value is None: continue # Convert lists to JSON string representation for OpenTelemetry compatibility if isinstance(value, list): # Ensure all list items are valid types if all(isinstance(item, (str, int, float, bool)) for item in value): value = json.dumps(value) else: logger.warning(f"Skipping metadata key '{key}': list contains invalid types") continue elif not isinstance(value, (str, int, float, bool)): logger.warning(f"Skipping metadata key '{key}': value type {type(value)} not supported") continue # Determine the attribute key attribute_key = key # Check if key is already a valid semantic convention attribute if key in VALID_SEMCONV_ATTRS: # Key is already a valid semantic convention, use as-is attribute_key = key elif key in SEMCONV_MAPPINGS: # It's a user-friendly key, map it to semantic convention attribute_key = SEMCONV_MAPPINGS[key] logger.debug(f"Mapped '{key}' to semantic convention '{attribute_key}'") else: # Not a semantic convention, use with prefix attribute_key = f"{prefix}.{key}" # Set the attribute span.set_attribute(attribute_key, value) updated_count += 1 if updated_count > 0: logger.debug(f"Successfully updated {updated_count} metadata attributes on trace") return True else: logger.warning("No valid metadata attributes were updated") return False except Exception as e: logger.error(f"Error updating trace metadata: {e}") return False __all__ = [ # Legacy exports "start_session", "end_session", "track_agent", "track_tool", "end_all_sessions", "Session", "ToolEvent", "ErrorEvent", "ActionEvent", "LLMEvent", # Modern exports "init", "start_trace", "end_trace", "update_trace_metadata", "Client", "get_client", # Decorators "trace", "session", "agent", "task", "workflow", "operation", "tool", "guardrail", "track_endpoint", # Enums "TraceState", "SUCCESS", "ERROR", "UNSET", # Validation "validate_trace_spans", "print_validation_summary", "ValidationError", ] ``` ### agentops/client/client.py ```python import atexit from typing import Optional, Any from agentops.client.api import ApiClient from agentops.config import Config from agentops.exceptions import NoApiKeyException from agentops.instrumentation import instrument_all from agentops.logging import logger from agentops.logging.config import configure_logging, intercept_opentelemetry_logging from agentops.sdk.core import TraceContext, tracer from agentops.legacy import Session # Global variables to hold the client's auto-started trace and its legacy session wrapper _client_init_trace_context: Optional[TraceContext] = None _client_legacy_session_for_init_trace: Optional[Session] = None # Single atexit handler registered flag _atexit_registered = False def _end_init_trace_atexit(): """Global atexit handler to end the client's auto-initialized trace during shutdown.""" global _client_init_trace_context, _client_legacy_session_for_init_trace if _client_init_trace_context is not None: logger.debug("Auto-ending client's init trace during shutdown.") try: # Use global tracer to end the trace directly if tracer.initialized and _client_init_trace_context.span.is_recording(): tracer.end_trace(_client_init_trace_context, end_state="Shutdown") except Exception as e: logger.warning(f"Error ending client's init trace during shutdown: {e}") finally: _client_init_trace_context = None _client_legacy_session_for_init_trace = None # Clear its legacy wrapper too class Client: """Singleton client for AgentOps service""" config: Config _initialized: bool _init_trace_context: Optional[TraceContext] = None # Stores the context of the auto-started trace _legacy_session_for_init_trace: Optional[ Session ] = None # Stores the legacy Session wrapper for the auto-started trace __instance = None # Class variable for singleton pattern api: ApiClient def __new__(cls, *args: Any, **kwargs: Any) -> "Client": if cls.__instance is None: cls.__instance = super(Client, cls).__new__(cls) # Initialize instance variables that should only be set once per instance cls.__instance._init_trace_context = None cls.__instance._legacy_session_for_init_trace = None return cls.__instance def __init__(self): # Initialization of attributes like config, _initialized should happen here if they are instance-specific # and not shared via __new__ for a true singleton that can be re-configured. # However, the current pattern re-initializes config in init(). if ( not hasattr(self, "_initialized") or not self._initialized ): # Ensure init logic runs only once per actual initialization intent self.config = Config() # Initialize config here for the instance self._initialized = False # self._init_trace_context = None # Already done in __new__ # self._legacy_session_for_init_trace = None # Already done in __new__ def init(self, **kwargs: Any) -> None: # Return type updated to None # Recreate the Config object to parse environment variables at the time of initialization # This allows re-init with new env vars if needed, though true singletons usually init once. self.config = Config() self.configure(**kwargs) # Only treat as re-initialization if a different non-None API key is explicitly provided provided_api_key = kwargs.get("api_key") if self.initialized and provided_api_key is not None and provided_api_key != self.config.api_key: logger.warning("AgentOps Client being re-initialized with a different API key. This is unusual.") # Reset initialization status to allow re-init with new key/config self._initialized = False if self._init_trace_context and self._init_trace_context.span.is_recording(): logger.warning("Ending previously auto-started trace due to re-initialization.") tracer.end_trace(self._init_trace_context, "Reinitialized") self._init_trace_context = None self._legacy_session_for_init_trace = None if self.initialized: logger.debug("AgentOps Client already initialized.") # If auto_start_session was true, return the existing legacy session wrapper if self.config.auto_start_session: return self._legacy_session_for_init_trace return None # If not auto-starting, and already initialized, return None if not self.config.api_key: raise NoApiKeyException configure_logging(self.config) intercept_opentelemetry_logging() self.api = ApiClient(self.config.endpoint) try: response = self.api.v3.fetch_auth_token(self.config.api_key) if response is None: # If auth fails, we cannot proceed with tracer initialization that depends on project_id logger.error("Failed to fetch auth token. AgentOps SDK will not be initialized.") return None # Explicitly return None if auth fails except Exception as e: # Re-raise authentication exceptions so they can be caught by tests and calling code logger.error(f"Authentication failed: {e}") raise self.api.v4.set_auth_token(response["token"]) tracing_config = self.config.dict() tracing_config["project_id"] = response["project_id"] tracer.initialize_from_config(tracing_config, jwt=response["token"]) if self.config.instrument_llm_calls: instrument_all() # self._initialized = True # Set initialized to True here - MOVED to after trace start attempt global _atexit_registered if not _atexit_registered: atexit.register(_end_init_trace_atexit) # Register new atexit handler _atexit_registered = True # Auto-start trace if configured if self.config.auto_start_session: if self._init_trace_context is None or not self._init_trace_context.span.is_recording(): logger.debug("Auto-starting init trace.") trace_name = self.config.trace_name or "default" self._init_trace_context = tracer.start_trace( trace_name=trace_name, tags=list(self.config.default_tags) if self.config.default_tags else None, is_init_trace=True, ) if self._init_trace_context: self._legacy_session_for_init_trace = Session(self._init_trace_context) # For backward compatibility, also update the global references in legacy and client modules # These globals are what old code might have been using via agentops.legacy.get_session() or similar indirect access. global _client_init_trace_context, _client_legacy_session_for_init_trace _client_init_trace_context = self._init_trace_context _client_legacy_session_for_init_trace = self._legacy_session_for_init_trace # Update legacy module's _current_session and _current_trace_context # This is tricky; direct access to another module's globals is not ideal. # Prefer explicit calls if possible, but for maximum BC: try: import agentops.legacy agentops.legacy._current_session = self._legacy_session_for_init_trace agentops.legacy._current_trace_context = self._init_trace_context except ImportError: pass # Should not happen else: logger.error("Failed to start the auto-init trace.") # Even if auto-start fails, core services up to the tracer might be initialized. # Set self.initialized to True if tracer is up, but return None. self._initialized = tracer.initialized return None # Failed to start trace self._initialized = True # Successfully initialized and auto-trace started (if configured) # For backward compatibility, return the legacy session wrapper when auto_start_session=True return self._legacy_session_for_init_trace else: logger.debug("Auto-start session is disabled. No init trace started by client.") self._initialized = True # Successfully initialized, just no auto-trace return None # No auto-session, so return None def configure(self, **kwargs: Any) -> None: """Update client configuration""" self.config.configure(**kwargs) @property def initialized(self) -> bool: return self._initialized @initialized.setter def initialized(self, value: bool) -> None: if self._initialized and self._initialized != value: # Allow re-setting to False if we are intentionally re-initializing # This logic is now partly in init() to handle re-init cases pass self._initialized = value # ------------------------------------------------------------ # Remove the old __instance = None at the end of the class definition if it's a repeat # __instance = None # This was a class variable, should be defined once # Make _init_trace_context and _legacy_session_for_init_trace accessible # to the atexit handler if it becomes a static/class method or needs access # For now, the atexit handler is global and uses global vars copied from these. # Deprecate and remove the old global _active_session from this module. # Consumers should use agentops.start_trace() or rely on the auto-init trace. # For a transition, the auto-init trace's legacy wrapper is set to legacy module's globals. # Ensure the global _active_session (if needed for some very old compatibility) points to the client's legacy session for init trace. # This specific global _active_session in client.py is problematic and should be phased out. # For now, _client_legacy_session_for_init_trace is the primary global for the auto-init trace's legacy Session. # Remove the old global _active_session defined at the top of this file if it's no longer the primary mechanism. # The new globals _client_init_trace_context and _client_legacy_session_for_init_trace handle the auto-init trace. ``` ### agentops/sdk/decorators/__init__.py ```python """ Decorators for instrumenting code with AgentOps. Provides @trace for creating trace-level spans (sessions) and other decorators for nested spans. """ from agentops.helpers.deprecation import deprecated from agentops.sdk.decorators.factory import create_entity_decorator from agentops.semconv.span_kinds import SpanKind # Create decorators for specific entity types using the factory agent = create_entity_decorator(SpanKind.AGENT) task = create_entity_decorator(SpanKind.TASK) operation_decorator = create_entity_decorator(SpanKind.OPERATION) workflow = create_entity_decorator(SpanKind.WORKFLOW) trace = create_entity_decorator(SpanKind.SESSION) tool = create_entity_decorator(SpanKind.TOOL) operation = task guardrail = create_entity_decorator(SpanKind.GUARDRAIL) track_endpoint = create_entity_decorator(SpanKind.HTTP) # For backward compatibility: @session decorator calls @trace decorator def session(*args, **kwargs): # noqa: F811 """@deprecated Use @agentops.trace instead. Wraps the @trace decorator for backward compatibility.""" # If called as @session or @session(...) if not args or not callable(args[0]): # called with kwargs like @session(name=...) return trace(*args, **kwargs) else: # called as @session directly on a function return trace(args[0], **kwargs) # args[0] is the wrapped function # Apply deprecation decorator to session function session = deprecated("Use @trace decorator instead.")(session) # Note: The original `operation = task` was potentially problematic if `operation` was meant to be distinct. # Using operation_decorator for clarity if a distinct OPERATION kind decorator is needed. # For now, keeping the alias as it was, assuming it was intentional for `operation` to be `task`. operation = task __all__ = [ "agent", "task", "workflow", "trace", "session", "operation", "tool", "guardrail", "track_endpoint", ] ``` ## Documentation ### v2/introduction.mdx --- title: "Introduction" description: "AgentOps is the developer favorite platform for testing, debugging, and deploying AI agents and LLM apps." --- Prefer asking your IDE? Install the Mintlify MCP Docs Server for AgentOps to chat with the docs while you code: `npx mint-mcp add agentops` ## Integrate with developer favorite LLM providers and agent frameworks ### Agent Frameworks } iconType="image" href="/v2/integrations/ag2" /> } iconType="image" href="/v2/integrations/agno" /> } iconType="image" href="/v2/integrations/autogen" /> } iconType="image" href="/v2/integrations/crewai" /> } iconType="image" href="/v2/integrations/google_adk" /> } iconType="image" href="/v2/integrations/langchain" /> } iconType="image" href="/v2/integrations/openai_agents_python" /> } iconType="image" href="/v2/integrations/openai_agents_js" /> } iconType="image" href="/v2/integrations/smolagents" /> ### LLM Providers } iconType="image" href="/v2/integrations/anthropic" /> } iconType="image" href="/v2/integrations/google_generative_ai" /> } iconType="image" href="/v2/integrations/openai" /> } iconType="image" href="/v2/integrations/litellm" /> } iconType="image" href="/v2/integrations/ibm_watsonx_ai" /> } iconType="image" href="/v2/integrations/xai" /> } iconType="image" href="/v2/integrations/mem0" /> Observability and monitoring for your AI agents and LLM apps. And we do it all in just two lines of code... ```python python import agentops agentops.init() ``` ... that logs everything back to your AgentOps Dashboard. AgentOps is also available for TypeScript/JavaScript applications. Check out our [TypeScript SDK guide](https://github.com/AgentOps-AI/agentops/blob/main/v2/usage/typescript-sdk) for Node.js projects. That's it! AgentOps will automatically instrument your code and start tracking traces. Need more control? You can create custom traces using the `@trace` decorator (recommended) or manage traces manually for advanced use cases: ```python python import agentops from agentops.sdk.decorators import trace agentops.init(, auto_start_session=False) @trace(name="my-workflow", tags=["production"]) def my_workflow(): # Your code here return "Workflow completed" ``` You can also set a custom trace name during initialization: ```python python import agentops agentops.init(, trace_name="custom-trace-name") ``` ## The AgentOps Dashboard [Give us a star](https://github.com/AgentOps-AI/agentops) to bookmark on GitHub, save for later ) With just two lines of code, you can free yourself from the chains of the terminal and, instead, visualize your agents' behavior in your AgentOps Dashboard. After setting up AgentOps, each execution of your program is recorded as a session and the above data is automatically recorded for you. The examples below were captured with two lines of code. ### Session Drilldown Here you will find a list of all of your previously recorded sessions and useful data about each such as total execution time. You also get helpful debugging info such as any SDK versions you were on if you're building on a supported agent framework like Crew or AutoGen. LLM calls are presented as a familiar chat history view, and charts give you a breakdown of the types of events that were called and how long they took. Find any past sessions from your Session Drawer. Most powerful of all is the Session Waterfall. On the left, a time visualization of all your LLM calls, Action events, Tool calls, and Errors. On the right, specific details about the event you've selected on the waterfall. For instance the exact prompt and completion for a given LLM call. Most of which has been automatically recorded for you. ### Session Overview View a meta-analysis of all of your sessions in a single view. ### v2/quickstart.mdx --- title: "Quickstart" description: "Get started with AgentOps in minutes with just 2 lines of code for basic monitoring, and explore powerful decorators for custom tracing." --- AgentOps is designed for easy integration into your AI agent projects, providing powerful observability with minimal setup. This guide will get you started quickly. [Give us a star on GitHub!](https://github.com/AgentOps-AI/agentops) Your support helps us grow. Prefer asking your IDE? Install the Mintlify MCP Docs Server for AgentOps to chat with the docs while you code: `npx mint-mcp add agentops` ## Installation First, install the AgentOps SDK. We recommend including `python-dotenv` for easy API key management. ```bash pip pip install agentops python-dotenv ``` ```bash poetry poetry add agentops python-dotenv ``` ```bash uv uv add agentops python-dotenv ``` ## Initial Setup (2 Lines of Code) At its simplest, AgentOps can start monitoring your supported LLM and agent framework calls with just two lines of Python code. 1. **Import AgentOps**: Add `import agentops` to your script. 2. **Initialize AgentOps**: Call `agentops.init()` with your API key. ```python Python import agentops import os from dotenv import load_dotenv # Load environment variables (recommended for API keys) load_dotenv() # Initialize AgentOps # The API key can be passed directly or set as an environment variable AGENTOPS_API_KEY AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") agentops.init(AGENTOPS_API_KEY) # That's it for basic auto-instrumentation! # If you're using a supported library (like OpenAI, LangChain, CrewAI, etc.), # AgentOps will now automatically track LLM calls and agent actions. ``` ### Setting Your AgentOps API Key You need an AgentOps API key to send data to your dashboard. - Get your API key from the [AgentOps Dashboard](https://app.agentops.ai/settings/projects). It's best practice to set your API key as an environment variable. ```bash Export to CLI export AGENTOPS_API_KEY="your_agentops_api_key_here" ``` ```txt Set in .env file AGENTOPS_API_KEY="your_agentops_api_key_here" ``` If you use a `.env` file, make sure `load_dotenv()` is called before `agentops.init()`. ## Running Your Agent & Viewing Traces After adding the two lines and ensuring your API key is set up: 1. Run your agent application as you normally would. 2. AgentOps will automatically instrument supported libraries and send trace data. 3. Visit your [AgentOps Dashboard](https://app.agentops.ai/traces) to observe your agent's operations! ## Beyond Automatic Instrumentation: Decorators While AgentOps automatically instruments many popular libraries, you can gain finer-grained control and track custom parts of your code using our powerful decorators. This allows you to define specific operations, group logic under named agents, track tool usage with costs, and create custom traces. ### Tracking Custom Operations with `@operation` Instrument any function in your code to create spans that track its execution, parameters, and return values. These operations will appear in your session visualization alongside LLM calls. ```python from agentops.sdk.decorators import operation @operation def process_data(data): # Your function logic here processed_result = data.upper() # agentops.record(Events("Processed Data", result=processed_result)) # Optional: record specific events return processed_result # Example usage: # my_data = "example input" # output = process_data(my_data) ``` ### Tracking Agent Logic with `@agent` If you structure your system with specific named agents (e.g., classes), use the `@agent` decorator on the class and `@operation` on its methods to group all downstream operations under that agent's context. ```python from agentops.sdk.decorators import agent, operation @agent(name="MyCustomAgent") # You can provide a name for the agent class MyAgent: def __init__(self, agent_id): self.agent_id = agent_id # agent_id is a reserved parameter for AgentOps @operation def perform_task(self, task_description): # Agent task logic here # This could include LLM calls or calls to other @operation decorated functions return f"Agent {self.agent_id} completed: {task_description}" # Example usage: # research_agent = MyAgent(agent_id="researcher-001") # result = research_agent.perform_task("Analyze market trends") ``` ### Tracking Tools with `@tool` Track the usage of specific tools or functions, and optionally associate costs with them. This data will be aggregated in your dashboard. ```python from agentops.sdk.decorators import tool @tool(name="WebSearchTool", cost=0.05) # Cost is optional def web_search(query: str) -> str: # Tool logic here return f"Search results for: {query}" @tool # No cost specified def calculator(expression: str) -> str: try: return str(eval(expression)) except Exception as e: return f"Error: {e}" # Example usage: # search_result = web_search("AgentOps features") # calculation = calculator("2 + 2") ``` ### Grouping with Traces (`@trace` or manual) Create custom traces to group a sequence of operations or define logical units of work. You can use the `@trace` decorator or manage traces manually for more complex scenarios. If `auto_start_session=False` in `agentops.init()`, you must use `@trace` or `agentops.start_trace()` for any data to be recorded. ```python from agentops.sdk.decorators import trace # Assuming MyAgent and web_search are defined as above # Option 1: Using the @trace decorator @trace(name="MyMainWorkflow", tags=["main-flow"]) def my_workflow_decorated(task_to_perform): # Your workflow code here main_agent = MyAgent(agent_id="workflow-agent") # Assuming MyAgent is defined result = main_agent.perform_task(task_to_perform) # Example of using a tool within the trace tool_result = web_search(f"details for {task_to_perform}") # Assuming web_search is defined return result, tool_result # result_decorated = my_workflow_decorated("complex data processing") # Option 2: Managing traces manually # import agentops # Already imported # custom_trace = agentops.start_trace(name="MyManualWorkflow", tags=["manual-flow"]) # try: # # Your code here # main_agent = MyAgent(agent_id="manual-workflow-agent") # Assuming MyAgent is defined # result = main_agent.perform_task("another complex task") # tool_result = web_search(f"info for {result}") # Assuming web_search is defined # agentops.end_trace(custom_trace, end_state="Success", end_prompt=f"Completed: {result}") # except Exception as e: # if custom_trace: # Ensure trace was started before trying to end it # agentops.end_trace(custom_trace, end_state="Fail", error_message=str(e)) # raise ``` ### Updating Trace Metadata You can also update metadata on running traces to add context or track progress: ```python from agentops import update_trace_metadata # Update metadata during trace execution update_trace_metadata({ "operation_name": "AI Agent Processing", "processing_stage": "data_validation", "records_processed": 1500, "user_id": "user_123", "tags": ["validation", "production"] }) ``` ## Complete Example with Decorators Here's a consolidated example showcasing how these decorators can work together: ```python import agentops from agentops.sdk.decorators import agent, operation, tool, trace from dotenv import load_dotenv import os # Load environment variables load_dotenv() AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") # Initialize AgentOps. # Set auto_start_session=False because @trace will manage the session. agentops.init(AGENTOPS_API_KEY, auto_start_session=False, tags=["quickstart-complete-example"]) # Define a tool @tool(name="AdvancedSearch", cost=0.02) def advanced_web_search(query: str) -> str: # Simulate a more advanced search return f"Advanced search results for '{query}': [Details...]" # Define an agent class @agent(name="ResearchSpecialistAgent") class ResearchAgent: def __init__(self, agent_id: str): self.agent_id = agent_id # This will be used as the agent_id in AgentOps @operation(name="ConductResearch") def conduct_research(self, research_topic: str) -> str: # Use the tool within the agent's operation search_results = advanced_web_search(f"Deep dive into {research_topic}") # Simulate further processing analysis = f"Analysis of '{research_topic}': Based on '{search_results}', the key findings are..." return analysis # Define a workflow using the @trace decorator @trace(name="FullResearchWorkflow", tags=["research", "analysis", "example"]) def run_full_research_workflow(topic: str) -> str: specialist_agent = ResearchAgent(agent_id="researcher-alpha-007") research_findings = specialist_agent.conduct_research(topic) final_report = f"Research Report for '{topic}':\n{research_findings}" # agentops.record(Events("ReportGenerated", details=final_report)) # Optional: record a custom event return final_report # Execute the workflow final_output = run_full_research_workflow("AI in healthcare") print(final_output) ``` ## Next Steps You've seen how to get started with AgentOps! Explore further to leverage its full potential: See how AgentOps automatically instruments popular LLM and agent frameworks. Explore detailed examples for various use cases and integrations. Dive deeper into the AgentOps SDK capabilities and API. Learn how to group operations and create custom traces using the @trace decorator. ### v2/concepts/core-concepts.mdx --- title: 'Core Concepts' description: 'Understanding the fundamental concepts of AgentOps' --- # The AgentOps SDK Architecture AgentOps is designed to provide comprehensive monitoring and analytics for AI agent workflows with minimal implementation effort. The SDK follows these key design principles: ## Automated Instrumentation After calling `agentops.init()`, the SDK automatically identifies installed LLM providers and instruments their API calls. This allows AgentOps to capture interactions between your code and the LLM providers to collect data for your dashboard without requiring manual instrumentation for every call. ## Declarative Tracing with Decorators The [decorators](https://github.com/AgentOps-AI/agentops/blob/main/v2/concepts/decorators) system allows you to add tracing to your existing functions and classes with minimal code changes. Decorators create hierarchical spans that provide a structured view of your agent's operations for monitoring and analysis. ## OpenTelemetry Foundation AgentOps is built on [OpenTelemetry](https://opentelemetry.io/), a widely-adopted standard for observability instrumentation. This provides a robust and standardized approach to collecting, processing, and exporting telemetry data. # Sessions A [Session](https://github.com/AgentOps-AI/agentops/blob/main/v2/concepts/sessions) represents a single user interaction with your agent. When you initialize AgentOps using the `init` function, a session is automatically created for you: ```python import agentops # Initialize AgentOps with automatic session creation agentops.init(api_key="YOUR_API_KEY") ``` By default, all events and API calls will be associated with this session. For more advanced use cases, you can control session creation manually: ```python # Initialize without auto-starting a session agentops.init(api_key="YOUR_API_KEY", auto_start_session=False) # Later, manually start a session when needed agentops.start_session(tags=["customer-query"]) ``` # Span Hierarchy In AgentOps, activities are organized into a hierarchical structure of spans: - **SESSION**: The root container for all activities in a single execution of your workflow - **AGENT**: Represents an autonomous entity with specialized capabilities - **WORKFLOW**: A logical grouping of related operations - **OPERATION/TASK**: A specific task or function performed by an agent - **LLM**: An interaction with a language model - **TOOL**: The use of a tool or API by an agent This hierarchy creates a complete trace of your agent's execution: ``` SESSION AGENT OPERATION/TASK LLM TOOL WORKFLOW OPERATION/TASK LLM (unattributed to a specific agent) ``` # Agents An **Agent** represents a component in your application that performs tasks. You can create and track agents using the `@agent` decorator: ```python from agentops.sdk.decorators import agent, operation @agent(name="customer_service") class CustomerServiceAgent: @operation def answer_query(self, query): # Agent logic here pass ``` # LLM Events AgentOps automatically tracks LLM API calls from supported providers, collecting valuable information like: - **Model**: The specific model used (e.g., "gpt-4", "claude-3-opus") - **Provider**: The LLM provider (e.g., "OpenAI", "Anthropic") - **Prompt Tokens**: Number of tokens in the input - **Completion Tokens**: Number of tokens in the output - **Cost**: The estimated cost of the interaction - **Messages**: The prompt and completion content ```python import agentops from openai import OpenAI # Initialize AgentOps agentops.init(api_key="YOUR_API_KEY") # Initialize the OpenAI client client = OpenAI() # This LLM call is automatically tracked response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": "What's the capital of France?"}] ) ``` # Tags [Tags](https://github.com/AgentOps-AI/agentops/blob/main/v2/concepts/tags) help you organize and filter your sessions. You can add tags when initializing AgentOps or when starting a session: ```python # Add tags when initializing agentops.init(api_key="YOUR_API_KEY", tags=["production", "web-app"]) # Or when manually starting a session agentops.start_session(tags=["customer-service", "tier-1"]) ``` # Host Environment AgentOps automatically collects basic [information](https://github.com/AgentOps-AI/agentops/blob/main/v2/concepts/host-env) about the environment where your agent is running: - **Operating System**: The OS type and version - **Python Version**: The version of Python being used - **Hostname**: The name of the host machine (anonymized) - **SDK Version**: The version of the AgentOps SDK being used # Dashboard Views The AgentOps dashboard provides several ways to visualize and analyze your agent's performance: - **Session List**: Overview of all sessions with filtering options - **Timeline View**: Chronological display of spans showing duration and relationships - **Tree View**: Hierarchical representation of spans showing parent-child relationships - **Message View**: Detailed view of LLM interactions with prompt and completion content - **Analytics**: Aggregated metrics across sessions and operations # Putting It All Together A typical implementation looks like this: ```python import agentops from openai import OpenAI from agentops.sdk.decorators import agent, operation # Initialize AgentOps agentops.init(api_key="YOUR_API_KEY", tags=["production"]) # Define an agent @agent(name="assistant") class AssistantAgent: def __init__(self): self.client = OpenAI() @operation def answer_question(self, question): # This LLM call will be automatically tracked and associated with this agent response = self.client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": question}] ) return response.choices[0].message.content def workflow(): # Use the agent assistant = AssistantAgent() answer = assistant.answer_question("What's the capital of France?") print(answer) workflow() # Session is automatically tracked until application terminates ``` ### v1/quickstart.mdx --- title: "Quickstart" description: "Start using AgentOps with just 2 lines of code" --- import CodeTooltip from '/snippets/add-code-tooltip.mdx' import EnvTooltip from '/snippets/add-env-tooltip.mdx' ```bash pip pip install agentops ``` ```bash poetry poetry add agentops ``` Get an AgentOps API key [here](https://app.agentops.ai/settings/projects) ```python python import agentops agentops.init() ``` Execute your program and visit [app.agentops.ai/drilldown](https://app.agentops.ai/drilldown) to observe your Agent! After your run, AgentOps prints a clickable URL to console linking directly to your session in the Dashboard {/* Intentionally blank div for newline */} [Give us a star](https://github.com/AgentOps-AI/agentops) if you liked AgentOps! (you may be our 3,000th ) ## More basic functionality You can instrument functions inside your code with the `@operation` decorator, which will create spans that track function execution, parameters, and return values. These operations will be displayed in your session visualization alongside LLM calls. ```python python # Instrument a function as an operation from agentops.sdk.decorators import operation @operation def process_data(data): # Your function logic here result = data.upper() return result ``` If you use specific named agents within your system, you can create agent spans that contain all downstream operations using the `@agent` decorator. ```python python # Create an agent class from agentops.sdk.decorators import agent, operation @agent class MyAgent: def __init__(self, name): self.name = name @operation def perform_task(self, task): # Agent task logic here return f"Completed {task}" ``` Create a session to group all your agent operations by using the `@session` decorator. Sessions serve as the root span for all operations. ```python python # Create a session from agentops.sdk.decorators import session @session def my_workflow(): # Your session code here agent = MyAgent("research-agent") result = agent.perform_task("data analysis") return result # Run the session my_workflow() ``` ## Example Code Here is the complete code from the sections above ```python python import agentops from agentops.sdk.decorators import session, agent, operation # Initialize AgentOps agentops.init() # Create an agent class @agent class MyAgent: def __init__(self, name): self.name = name @operation def perform_task(self, task): # Agent task logic here return f"Completed {task}" # Create a session @session def my_workflow(): # Your session code here agent = MyAgent("research-agent") result = agent.perform_task("data analysis") return result # Run the session my_workflow() ``` Jupyter Notebook with sample code that you can run! That's all you need to get started! Check out the documentation below to see how you can record other operations. AgentOps is a lot more powerful this way! ## Explore our more advanced functionality! Record all of your operations the way AgentOps intends. Associate operations with specific named agents. ## Instrumentation Architecture ### agentops/instrumentation/__init__.py ```python """ AgentOps Instrumentation Module This module provides automatic instrumentation for various LLM providers and agentic libraries. It works by monitoring Python imports and automatically instrumenting packages as they are imported. Key Features: - Automatic detection and instrumentation of LLM providers (OpenAI, Anthropic, etc.) - Support for agentic libraries (CrewAI, AutoGen, etc.) - Version-aware instrumentation (only activates for supported versions) - Smart handling of provider vs agentic library conflicts - Non-intrusive monitoring using Python's import system """ from typing import Optional, Set, TypedDict try: from typing import NotRequired except ImportError: from typing_extensions import NotRequired from types import ModuleType from dataclasses import dataclass import importlib import sys from packaging.version import Version, parse import builtins # Add os and site for path checking import os import site from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore from agentops.logging import logger from agentops.sdk.core import tracer from agentops.instrumentation.common import get_library_version # Define the structure for instrumentor configurations class InstrumentorConfig(TypedDict): module_name: str class_name: str min_version: str package_name: NotRequired[str] # Optional: actual pip package name if different from module # Configuration for supported LLM providers PROVIDERS: dict[str, InstrumentorConfig] = { "openai": { "module_name": "agentops.instrumentation.providers.openai", "class_name": "OpenaiInstrumentor", "min_version": "1.0.0", }, "anthropic": { "module_name": "agentops.instrumentation.providers.anthropic", "class_name": "AnthropicInstrumentor", "min_version": "0.32.0", }, "ibm_watsonx_ai": { "module_name": "agentops.instrumentation.providers.ibm_watsonx_ai", "class_name": "WatsonxInstrumentor", "min_version": "0.1.0", }, "google.genai": { "module_name": "agentops.instrumentation.providers.google_genai", "class_name": "GoogleGenaiInstrumentor", "min_version": "0.1.0", "package_name": "google-genai", # Actual pip package name }, "mem0": { "module_name": "agentops.instrumentation.providers.mem0", "class_name": "Mem0Instrumentor", "min_version": "0.1.0", "package_name": "mem0ai", }, } # Configuration for supported agentic libraries AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = { "crewai": { "module_name": "agentops.instrumentation.agentic.crewai", "class_name": "CrewaiInstrumentor", "min_version": "0.56.0", }, "autogen": { "module_name": "agentops.instrumentation.agentic.ag2", "class_name": "AG2Instrumentor", "min_version": "0.3.2", }, "agents": { "module_name": "agentops.instrumentation.agentic.openai_agents", "class_name": "OpenAIAgentsInstrumentor", "min_version": "0.0.1", }, "google.adk": { "module_name": "agentops.instrumentation.agentic.google_adk", "class_name": "GooogleAdkInstrumentor", "min_version": "0.1.0", }, "agno": { "module_name": "agentops.instrumentation.agentic.agno", "class_name": "AgnoInstrumentor", "min_version": "1.5.8", }, "smolagents": { "module_name": "agentops.instrumentation.agentic.smolagents", "class_name": "SmolagentsInstrumentor", "min_version": "1.0.0", }, "langgraph": { "module_name": "agentops.instrumentation.agentic.langgraph", "class_name": "LanggraphInstrumentor", "min_version": "0.2.0", }, } # Combine all target packages for monitoring TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) # Create a single instance of the manager # _manager = InstrumentationManager() # Removed # Module-level state variables _active_instrumentors: list[BaseInstrumentor] = [] _original_builtins_import = builtins.__import__ # Store original import _instrumenting_packages: Set[str] = set() _has_agentic_library: bool = False # New helper function to check module origin def _is_installed_package(module_obj: ModuleType, package_name_key: str) -> bool: """ Determines if the given module object corresponds to an installed site-package rather than a local module, especially when names might collide. `package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk'). """ if not hasattr(module_obj, "__file__") or not module_obj.__file__: logger.debug( f"_is_installed_package: Module '{package_name_key}' has no __file__, assuming it might be an SDK namespace package. Returning True." ) return True module_path = os.path.normcase(os.path.realpath(os.path.abspath(module_obj.__file__))) # Priority 1: Check if it's in any site-packages directory. site_packages_dirs = site.getsitepackages() if isinstance(site_packages_dirs, str): site_packages_dirs = [site_packages_dirs] if hasattr(site, "USER_SITE") and site.USER_SITE and os.path.exists(site.USER_SITE): site_packages_dirs.append(site.USER_SITE) normalized_site_packages_dirs = [ os.path.normcase(os.path.realpath(p)) for p in site_packages_dirs if p and os.path.exists(p) ] for sp_dir in normalized_site_packages_dirs: if module_path.startswith(sp_dir): logger.debug( f"_is_installed_package: Module '{package_name_key}' is a library, instrumenting '{package_name_key}'." ) return True # Priority 2: If not in site-packages, it's highly likely a local module or not an SDK we target. logger.debug(f"_is_installed_package: Module '{package_name_key}' is a local module, skipping instrumentation.") return False def _is_package_instrumented(package_name: str) -> bool: """Check if a package is already instrumented by looking at active instrumentors.""" # Handle package.module names by converting dots to underscores for comparison normalized_target_name = package_name.replace(".", "_").lower() for instrumentor in _active_instrumentors: # Check based on the key it was registered with if ( hasattr(instrumentor, "_agentops_instrumented_package_key") and instrumentor._agentops_instrumented_package_key == package_name ): return True # Fallback to class name check (existing logic, less precise) # We use split('.')[-1] for cases like 'google.genai' to match GenAIInstrumentor instrumentor_class_name_prefix = instrumentor.__class__.__name__.lower().replace("instrumentor", "") target_base_name = package_name.split(".")[-1].lower() normalized_class_name_match = ( normalized_target_name.startswith(instrumentor_class_name_prefix) or target_base_name == instrumentor_class_name_prefix ) if normalized_class_name_match: # This fallback can be noisy, let's make it more specific or rely on the key above more # For now, if the key matches or this broad name match works, consider instrumented. # This helps if _agentops_instrumented_package_key was somehow not set. return True return False def _uninstrument_providers(): """Uninstrument all provider instrumentors while keeping agentic libraries active.""" global _active_instrumentors new_active_instrumentors = [] uninstrumented_any = False for instrumentor in _active_instrumentors: instrumented_key = getattr(instrumentor, "_agentops_instrumented_package_key", None) if instrumented_key and instrumented_key in PROVIDERS: try: instrumentor.uninstrument() logger.debug( f"AgentOps: Uninstrumented provider: {instrumentor.__class__.__name__} (for package '{instrumented_key}') due to agentic library activation." ) uninstrumented_any = True except Exception as e: logger.error(f"Error uninstrumenting provider {instrumentor.__class__.__name__}: {e}") else: # Keep non-provider instrumentors or those without our key (shouldn't happen for managed ones) new_active_instrumentors.append(instrumentor) if uninstrumented_any or not new_active_instrumentors and _active_instrumentors: logger.debug( f"_uninstrument_providers: Processed. Previous active: {len(_active_instrumentors)}, New active after filtering providers: {len(new_active_instrumentors)}" ) _active_instrumentors = new_active_instrumentors def _should_instrument_package(package_name: str) -> bool: """ Determine if a package should be instrumented based on current state. Handles special cases for agentic libraries and providers. """ global _has_agentic_library # If already instrumented by AgentOps (using our refined check), skip. if _is_package_instrumented(package_name): logger.debug(f"_should_instrument_package: '{package_name}' already instrumented by AgentOps. Skipping.") return False is_target_agentic = package_name in AGENTIC_LIBRARIES is_target_provider = package_name in PROVIDERS if not is_target_agentic and not is_target_provider: logger.debug( f"_should_instrument_package: '{package_name}' is not a targeted provider or agentic library. Skipping." ) return False if _has_agentic_library: # An agentic library is already active. if is_target_agentic: logger.debug( f"AgentOps: An agentic library is active. Skipping instrumentation for subsequent agentic library '{package_name}'." ) return False if is_target_provider: logger.debug( f"AgentOps: An agentic library is active. Skipping instrumentation for provider '{package_name}'." ) return False else: # No agentic library is active yet. if is_target_agentic: logger.debug( f"AgentOps: '{package_name}' is the first-targeted agentic library. Will uninstrument providers if any are/become active." ) _uninstrument_providers() return True if is_target_provider: logger.debug( f"_should_instrument_package: '{package_name}' is a provider, no agentic library active. Allowing." ) return True logger.debug( f"_should_instrument_package: Defaulting to False for '{package_name}' (state: _has_agentic_library={_has_agentic_library})" ) return False def _perform_instrumentation(package_name: str): """Helper function to perform instrumentation for a given package.""" global _instrumenting_packages, _active_instrumentors, _has_agentic_library if not _should_instrument_package(package_name): return # Get the appropriate configuration for the package # Ensure package_name is a key in either PROVIDERS or AGENTIC_LIBRARIES if package_name not in PROVIDERS and package_name not in AGENTIC_LIBRARIES: logger.debug( f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS or AGENTIC_LIBRARIES. Skipping." ) return config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) loader = InstrumentorLoader(**config) # instrument_one already checks loader.should_activate instrumentor_instance = instrument_one(loader) if instrumentor_instance is not None: # Check if it was *actually* instrumented by instrument_one by seeing if the instrument method was called successfully. # This relies on instrument_one returning None if its internal .instrument() call failed (if we revert that, this needs adjustment) # For now, assuming instrument_one returns instance only on full success. # User request was to return instrumentor even if .instrument() fails. So, we check if _agentops_instrumented_package_key was set by us. # Let's assume instrument_one might return an instance whose .instrument() failed. # The key is set before _active_instrumentors.append, so if it's already there and matches, it means it's a re-attempt on the same package. # The _is_package_instrumented check at the start of _should_instrument_package should prevent most re-entry for the same package_name. # Store the package key this instrumentor is for, to aid _is_package_instrumented instrumentor_instance._agentops_instrumented_package_key = package_name # Add to active_instrumentors only if it's not a duplicate in terms of package_key being instrumented # This is a safeguard, _is_package_instrumented should catch this earlier. is_newly_added = True for existing_inst in _active_instrumentors: if ( hasattr(existing_inst, "_agentops_instrumented_package_key") and existing_inst._agentops_instrumented_package_key == package_name ): is_newly_added = False logger.debug( f"_perform_instrumentation: Instrumentor for '{package_name}' already in _active_instrumentors. Not adding again." ) break if is_newly_added: _active_instrumentors.append(instrumentor_instance) # If this was an agentic library AND it's newly effectively instrumented. if ( package_name in AGENTIC_LIBRARIES and not _has_agentic_library ): # Check _has_agentic_library to ensure this is the *first* one. # _uninstrument_providers() was already called in _should_instrument_package for the first agentic library. _has_agentic_library = True # Special case: If mem0 is instrumented, also instrument concurrent.futures if package_name == "mem0" and is_newly_added: try: # Check if concurrent.futures module is available # Create config for concurrent.futures instrumentor concurrent_config = InstrumentorConfig( module_name="agentops.instrumentation.utilities.concurrent_futures", class_name="ConcurrentFuturesInstrumentor", min_version="3.7.0", # Python 3.7+ (concurrent.futures is stdlib) package_name="python", # Special case for stdlib modules ) # Create and instrument concurrent.futures concurrent_loader = InstrumentorLoader(**concurrent_config) concurrent_instrumentor = instrument_one(concurrent_loader) if concurrent_instrumentor is not None: concurrent_instrumentor._agentops_instrumented_package_key = "concurrent.futures" _active_instrumentors.append(concurrent_instrumentor) logger.debug("AgentOps: Instrumented concurrent.futures as a dependency of mem0.") except Exception as e: logger.debug(f"Could not instrument concurrent.futures for mem0: {e}") else: logger.debug( f"_perform_instrumentation: instrument_one for '{package_name}' returned None. Not added to active instrumentors." ) def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(), level=0): """ Monitor imports and instrument packages as they are imported. This replaces the built-in import function to intercept package imports. """ global _instrumenting_packages, _has_agentic_library # If an agentic library is already instrumented, skip all further instrumentation if _has_agentic_library: return _original_builtins_import(name, globals_dict, locals_dict, fromlist, level) # First, do the actual import module = _original_builtins_import(name, globals_dict, locals_dict, fromlist, level) # Check for exact matches first (handles package.module like google.adk) packages_to_check = set() # Check the imported module itself if name in TARGET_PACKAGES: packages_to_check.add(name) else: # Check if any target package is a prefix of the import name for target in TARGET_PACKAGES: if name.startswith(target + ".") or name == target: packages_to_check.add(target) # For "from X import Y" style imports, also check submodules if fromlist: for item in fromlist: # Construct potential full name, e.g., "google.adk" from name="google", item="adk" # Or if name="os", item="path", full_name="os.path" # If the original name itself is a multi-part name like "a.b", and item is "c", then "a.b.c" # This logic needs to correctly identify the root package if 'name' is already a sub-package. # The existing TARGET_PACKAGES check is simpler: it checks against pre-defined full names. # Check full name if item forms part of a target package name full_item_name_candidate = f"{name}.{item}" if full_item_name_candidate in TARGET_PACKAGES: packages_to_check.add(full_item_name_candidate) else: # Fallback to checking if 'name' itself is a target for target in TARGET_PACKAGES: if name == target or name.startswith(target + "."): packages_to_check.add(target) # Check the base target if a submodule is imported from it. # Instrument all matching packages for package_to_check in packages_to_check: if package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check): target_module_obj = sys.modules.get(package_to_check) if target_module_obj: is_sdk = _is_installed_package(target_module_obj, package_to_check) if not is_sdk: logger.debug( f"AgentOps: Target '{package_to_check}' appears to be a local module/directory. Skipping AgentOps SDK instrumentation for it." ) continue else: logger.debug( f"_import_monitor: No module object found in sys.modules for '{package_to_check}', proceeding with SDK instrumentation attempt." ) _instrumenting_packages.add(package_to_check) try: _perform_instrumentation(package_to_check) # If we just instrumented an agentic library, stop if _has_agentic_library: break except Exception as e: logger.error(f"Error instrumenting {package_to_check}: {str(e)}") finally: _instrumenting_packages.discard(package_to_check) return module @dataclass class InstrumentorLoader: """ Represents a dynamically-loadable instrumentor. Handles version checking and instantiation of instrumentors. """ module_name: str class_name: str min_version: str package_name: Optional[str] = None # Optional: actual pip package name @property def module(self) -> ModuleType: """Get the instrumentor module.""" return importlib.import_module(self.module_name) @property def should_activate(self) -> bool: """Check if the package is available and meets version requirements.""" try: # Special case for stdlib modules (like concurrent.futures) if self.package_name == "python": import sys python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" return Version(python_version) >= parse(self.min_version) # Use explicit package_name if provided, otherwise derive from module_name if self.package_name: provider_name = self.package_name else: provider_name = self.module_name.split(".")[-1] # Use common version utility module_version = get_library_version(provider_name) return module_version != "unknown" and Version(module_version) >= parse(self.min_version) except Exception: return False def get_instance(self) -> BaseInstrumentor: """Create and return a new instance of the instrumentor.""" return getattr(self.module, self.class_name)() def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]: """ Instrument a single package using the provided loader. Returns the instrumentor instance if successful, None otherwise. """ if not loader.should_activate: # This log is important for users to know why something wasn't instrumented. logger.debug( f"AgentOps: Package '{loader.package_name or loader.module_name}' not found or version is less than minimum required ('{loader.min_version}'). Skipping instrumentation." ) return None instrumentor = loader.get_instance() try: # Use the provider directly from the global tracer instance instrumentor.instrument(tracer_provider=tracer.provider) logger.debug( f"AgentOps: Successfully instrumented '{loader.class_name}' for package '{loader.package_name or loader.module_name}'." ) except Exception as e: logger.error( f"Failed to instrument {loader.class_name} for {loader.package_name or loader.module_name}: {e}", exc_info=True, ) return instrumentor def instrument_all(): """Start monitoring and instrumenting packages if not already started.""" # Check if active_instrumentors is empty, as a proxy for not started. if not _active_instrumentors: builtins.__import__ = _import_monitor global _instrumenting_packages, _has_agentic_library # If an agentic library is already instrumented, don't instrument anything else if _has_agentic_library: return for name in list(sys.modules.keys()): # Stop if an agentic library gets instrumented during the loop if _has_agentic_library: break module = sys.modules.get(name) if not isinstance(module, ModuleType): continue # Check for exact matches first (handles package.module like google.adk) package_to_check = None if name in TARGET_PACKAGES: package_to_check = name else: # Check if any target package is a prefix of the module name for target in TARGET_PACKAGES: if name.startswith(target + ".") or name == target: package_to_check = target break if ( package_to_check and package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check) ): target_module_obj = sys.modules.get(package_to_check) if target_module_obj: is_sdk = _is_installed_package(target_module_obj, package_to_check) if not is_sdk: continue else: logger.debug( f"instrument_all: No module object found for '{package_to_check}' in sys.modules during startup scan. Proceeding cautiously." ) _instrumenting_packages.add(package_to_check) try: _perform_instrumentation(package_to_check) except Exception as e: logger.error(f"Error instrumenting {package_to_check}: {str(e)}") finally: _instrumenting_packages.discard(package_to_check) def uninstrument_all(): """Stop monitoring and uninstrument all packages.""" global _active_instrumentors, _has_agentic_library builtins.__import__ = _original_builtins_import for instrumentor in _active_instrumentors: instrumentor.uninstrument() logger.debug(f"Uninstrumented {instrumentor.__class__.__name__}") _active_instrumentors = [] _has_agentic_library = False def get_active_libraries() -> set[str]: """ Get all actively used libraries in the current execution context. Returns a set of package names that are currently imported and being monitored. """ active_libs = set() for name, module in sys.modules.items(): if not isinstance(module, ModuleType): continue # Check for exact matches first if name in TARGET_PACKAGES: active_libs.add(name) else: # Check if any target package is a prefix of the module name for target in TARGET_PACKAGES: if name.startswith(target + ".") or name == target: active_libs.add(target) break return active_libs ``` ### agentops/instrumentation/README.md # AgentOps Instrumentation This package provides OpenTelemetry instrumentation for various LLM providers and related services. ## Available Instrumentors - **OpenAI** (`v0.27.0+` and `v1.0.0+`) - **Anthropic** (`v0.7.0+`) - **Google GenAI** (`v0.1.0+`) - **IBM WatsonX AI** (`v0.1.0+`) - **CrewAI** (`v0.56.0+`) - **AG2/AutoGen** (`v0.3.2+`) - **Google ADK** (`v0.1.0+`) - **Agno** (`v0.0.1+`) - **Mem0** (`v0.1.0+`) - **smolagents** (`v0.1.0+`) ## Common Module Usage The `agentops.instrumentation.common` module provides shared utilities for creating instrumentations: ### Base Instrumentor Use `CommonInstrumentor` for creating new instrumentations: ```python from agentops.instrumentation.common import CommonInstrumentor, InstrumentorConfig, WrapConfig class MyInstrumentor(CommonInstrumentor): def __init__(self): config = InstrumentorConfig( library_name="my-library", library_version="1.0.0", wrapped_methods=[ WrapConfig( trace_name="my.method", package="my_library.module", class_name="MyClass", method_name="my_method", handler=my_attribute_handler ) ], dependencies=["my-library >= 1.0.0"] ) super().__init__(config) ``` ### Attribute Handlers Create attribute handlers to extract data from method calls: ```python from agentops.instrumentation.common import AttributeMap def my_attribute_handler(args=None, kwargs=None, return_value=None) -> AttributeMap: attributes = {} if kwargs and "model" in kwargs: attributes["llm.request.model"] = kwargs["model"] if return_value and hasattr(return_value, "usage"): attributes["llm.usage.total_tokens"] = return_value.usage.total_tokens return attributes ``` ### Span Management Use the span management utilities for consistent span creation: ```python from agentops.instrumentation.common import create_span, SpanAttributeManager # Create an attribute manager attr_manager = SpanAttributeManager(service_name="my-service") # Use the create_span context manager with create_span( tracer, "my.operation", attributes={"my.attribute": "value"}, attribute_manager=attr_manager ) as span: # Your operation code here pass ``` ### Token Counting Use the token counting utilities for consistent token usage extraction: ```python from agentops.instrumentation.common import TokenUsageExtractor, set_token_usage_attributes # Extract token usage from a response usage = TokenUsageExtractor.extract_from_response(response) # Set token usage attributes on a span set_token_usage_attributes(span, response) ``` ### Streaming Support Use streaming utilities for handling streaming responses: ```python from agentops.instrumentation.common import create_stream_wrapper_factory, StreamingResponseHandler # Create a stream wrapper factory wrapper = create_stream_wrapper_factory( tracer, "my.stream", extract_chunk_content=StreamingResponseHandler.extract_generic_chunk_content, initial_attributes={"stream.type": "text"} ) # Apply to streaming methods wrap_function_wrapper("my_module", "stream_method", wrapper) ``` ### Metrics Use standard metrics for consistency across instrumentations: ```python from agentops.instrumentation.common import StandardMetrics, MetricsRecorder # Create standard metrics metrics = StandardMetrics.create_standard_metrics(meter) # Use the metrics recorder recorder = MetricsRecorder(metrics) recorder.record_token_usage(prompt_tokens=100, completion_tokens=50) recorder.record_duration(1.5) ``` ## Creating a New Instrumentor 1. Create a new directory under `agentops/instrumentation/` for your provider 2. Create an `__init__.py` file with version information 3. Create an `instrumentor.py` file extending `CommonInstrumentor` 4. Create attribute handlers in an `attributes/` subdirectory 5. Add your instrumentor to the main `__init__.py` configuration Example structure: ``` agentops/instrumentation/ my_provider/ __init__.py instrumentor.py attributes/ __init__.py handlers.py ``` ## Best Practices 1. **Use Common Utilities**: Leverage the common module for consistency 2. **Follow Semantic Conventions**: Use attributes from `agentops.semconv` 3. **Handle Errors Gracefully**: Wrap operations in try-except blocks 4. **Support Async**: Provide both sync and async method wrapping 5. **Document Attributes**: Comment on what attributes are captured 6. **Test Thoroughly**: Write unit tests for your instrumentor ## Examples See the `examples/` directory for usage examples of each instrumentor. ### agentops/instrumentation/providers/openai/instrumentor.py ```python """OpenAI API Instrumentation for AgentOps This module provides comprehensive instrumentation for the OpenAI API, including: - Chat completions (streaming and non-streaming) - Regular completions - Embeddings - Image generation - Assistants API (create, runs, messages) - Responses API (Agents SDK) The instrumentation supports both sync and async methods, metrics collection, and distributed tracing. """ from typing import Dict, Any from wrapt import wrap_function_wrapper from opentelemetry.metrics import Meter from agentops.logging import logger from agentops.instrumentation.common import ( CommonInstrumentor, InstrumentorConfig, WrapConfig, StandardMetrics, MetricsRecorder, ) from agentops.instrumentation.providers.openai import LIBRARY_NAME, LIBRARY_VERSION from agentops.instrumentation.providers.openai.config import Config from agentops.instrumentation.providers.openai.utils import is_openai_v1 from agentops.instrumentation.providers.openai.wrappers import ( handle_completion_attributes, handle_embeddings_attributes, handle_image_gen_attributes, handle_assistant_attributes, handle_run_attributes, handle_run_retrieve_attributes, handle_run_stream_attributes, handle_messages_attributes, ) from agentops.instrumentation.providers.openai.stream_wrapper import ( chat_completion_stream_wrapper, async_chat_completion_stream_wrapper, responses_stream_wrapper, async_responses_stream_wrapper, ) from agentops.instrumentation.providers.openai.v0 import OpenAIV0Instrumentor from agentops.semconv import Meters _instruments = ("openai >= 0.27.0",) class OpenaiInstrumentor(CommonInstrumentor): """An instrumentor for OpenAI's client library with comprehensive coverage.""" def __init__( self, enrich_assistant: bool = False, enrich_token_usage: bool = False, exception_logger=None, get_common_metrics_attributes=None, upload_base64_image=None, enable_trace_context_propagation: bool = True, ): # Configure the global config with provided options Config.enrich_assistant = enrich_assistant Config.enrich_token_usage = enrich_token_usage Config.exception_logger = exception_logger Config.get_common_metrics_attributes = get_common_metrics_attributes or (lambda: {}) Config.upload_base64_image = upload_base64_image Config.enable_trace_context_propagation = enable_trace_context_propagation # Create instrumentor config config = InstrumentorConfig( library_name=LIBRARY_NAME, library_version=LIBRARY_VERSION, wrapped_methods=self._get_wrapped_methods(), metrics_enabled=True, dependencies=_instruments, ) super().__init__(config) def _initialize(self, **kwargs): """Handle version-specific initialization.""" if not is_openai_v1(): # For v0, use the legacy instrumentor OpenAIV0Instrumentor().instrument(**kwargs) # Skip normal instrumentation self.config.wrapped_methods = [] def _custom_wrap(self, **kwargs): """Add custom wrappers for streaming functionality.""" if is_openai_v1() and self._tracer: # from wrapt import wrap_function_wrapper # # Add streaming wrappers for v1 try: # Chat completion streaming wrappers wrap_function_wrapper( "openai.resources.chat.completions", "Completions.create", chat_completion_stream_wrapper(self._tracer), ) wrap_function_wrapper( "openai.resources.chat.completions", "AsyncCompletions.create", async_chat_completion_stream_wrapper(self._tracer), ) # Beta chat completion streaming wrappers wrap_function_wrapper( "openai.resources.beta.chat.completions", "Completions.parse", chat_completion_stream_wrapper(self._tracer), ) wrap_function_wrapper( "openai.resources.beta.chat.completions", "AsyncCompletions.parse", async_chat_completion_stream_wrapper(self._tracer), ) # Responses API streaming wrappers wrap_function_wrapper( "openai.resources.responses", "Responses.create", responses_stream_wrapper(self._tracer), ) wrap_function_wrapper( "openai.resources.responses", "AsyncResponses.create", async_responses_stream_wrapper(self._tracer), ) except Exception as e: logger.warning(f"[OPENAI INSTRUMENTOR] Error setting up OpenAI streaming wrappers: {e}") else: if not is_openai_v1(): logger.debug("[OPENAI INSTRUMENTOR] Skipping custom wrapping - not using OpenAI v1") if not self._tracer: logger.debug("[OPENAI INSTRUMENTOR] Skipping custom wrapping - no tracer available") def _create_metrics(self, meter: Meter) -> Dict[str, Any]: """Create metrics for OpenAI instrumentation.""" metrics = StandardMetrics.create_standard_metrics(meter) # Add OpenAI-specific metrics metrics.update( { "chat_choice_counter": meter.create_counter( name=Meters.LLM_GENERATION_CHOICES, unit="choice", description="Number of choices returned by chat completions call", ), "streaming_time_to_first_token": meter.create_histogram( name=Meters.LLM_STREAMING_TIME_TO_FIRST_TOKEN, unit="s", description="Time to first token in streaming chat completions", ), "streaming_time_to_generate": meter.create_histogram( name=Meters.LLM_STREAMING_TIME_TO_GENERATE, unit="s", description="Time between first token and completion in streaming chat completions", ), "embeddings_vector_size_counter": meter.create_counter( name=Meters.LLM_EMBEDDINGS_VECTOR_SIZE, unit="element", description="The size of returned vector", ), "embeddings_exception_counter": meter.create_counter( name=Meters.LLM_EMBEDDINGS_EXCEPTIONS, unit="time", description="Number of exceptions occurred during embeddings operation", ), "image_gen_exception_counter": meter.create_counter( name=Meters.LLM_IMAGE_GENERATIONS_EXCEPTIONS, unit="time", description="Number of exceptions occurred during image generations operation", ), } ) return metrics def _custom_unwrap(self, **kwargs): """Handle version-specific uninstrumentation.""" if not is_openai_v1(): OpenAIV0Instrumentor().uninstrument(**kwargs) def _get_wrapped_methods(self) -> list[WrapConfig]: """Get all methods that should be wrapped. Note: Chat completions and Responses API methods are NOT included here as they are wrapped directly in _custom_wrap to support streaming. """ wrapped_methods = [] # Regular completions wrapped_methods.extend( [ WrapConfig( trace_name="openai.completion", package="openai.resources.completions", class_name="Completions", method_name="create", handler=handle_completion_attributes, ), WrapConfig( trace_name="openai.completion", package="openai.resources.completions", class_name="AsyncCompletions", method_name="create", handler=handle_completion_attributes, is_async=True, ), ] ) # Embeddings wrapped_methods.extend( [ WrapConfig( trace_name="openai.embeddings", package="openai.resources.embeddings", class_name="Embeddings", method_name="create", handler=handle_embeddings_attributes, ), WrapConfig( trace_name="openai.embeddings", package="openai.resources.embeddings", class_name="AsyncEmbeddings", method_name="create", handler=handle_embeddings_attributes, is_async=True, ), ] ) # Image generation wrapped_methods.append( WrapConfig( trace_name="openai.images.generate", package="openai.resources.images", class_name="Images", method_name="generate", handler=handle_image_gen_attributes, ) ) # Beta APIs - these may not be available in all versions beta_methods = [] # Assistants beta_methods.append( WrapConfig( trace_name="openai.assistants.create", package="openai.resources.beta.assistants", class_name="Assistants", method_name="create", handler=handle_assistant_attributes, ) ) # Runs beta_methods.extend( [ WrapConfig( trace_name="openai.runs.create", package="openai.resources.beta.threads.runs", class_name="Runs", method_name="create", handler=handle_run_attributes, ), WrapConfig( trace_name="openai.runs.retrieve", package="openai.resources.beta.threads.runs", class_name="Runs", method_name="retrieve", handler=handle_run_retrieve_attributes, ), WrapConfig( trace_name="openai.runs.create_and_stream", package="openai.resources.beta.threads.runs", class_name="Runs", method_name="create_and_stream", handler=handle_run_stream_attributes, ), ] ) # Messages beta_methods.append( WrapConfig( trace_name="openai.messages.list", package="openai.resources.beta.threads.messages", class_name="Messages", method_name="list", handler=handle_messages_attributes, ) ) # Add beta methods to wrapped methods (they might fail) wrapped_methods.extend(beta_methods) return wrapped_methods def get_metrics_recorder(self) -> MetricsRecorder: """Get a metrics recorder for use in wrappers.""" return MetricsRecorder(self._metrics) ``` ## Examples ### examples/openai/openai_example_sync.py ```python # OpenAI Sync Example # # We are going to create a simple chatbot that creates stories based on a prompt. The chatbot will use the gpt-4o-mini LLM to generate the story using a user prompt. # # We will track the chatbot with AgentOps and see how it performs! # First let's install the required packages # # Install required dependencies # %pip install agentops # %pip install openai # %pip install python-dotenv # Then import them from openai import OpenAI import agentops import os from dotenv import load_dotenv # Next, we'll grab our API keys. You can use dotenv like below or however else you like to load environment variables load_dotenv() os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "your_openai_api_key_here") os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY", "your_api_key_here") # Next we initialize the AgentOps client. agentops.init(auto_start_session=True, trace_name="OpenAI Sync Example", tags=["openai", "sync", "agentops-example"]) tracer = agentops.start_trace( trace_name="OpenAI Sync Example", tags=["openai-sync-example", "openai", "agentops-example"] ) client = OpenAI() # And we are all set! Note the seesion url above. We will use it to track the chatbot. # # Let's create a simple chatbot that generates stories. system_prompt = """ You are a master storyteller, with the ability to create vivid and engaging stories. You have experience in writing for children and adults alike. You are given a prompt and you need to generate a story based on the prompt. """ user_prompt = "Write a very short story about a cyber-warrior trapped in the imperial time period." messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, ) print(response.choices[0].message.content) # The response is a string that contains the story. We can track this with AgentOps by navigating to the trace url and viewing the run. # ## Streaming Version # We will demonstrate the streaming version of the API. stream = client.chat.completions.create( model="gpt-4o-mini", messages=messages, stream=True, ) for chunk in stream: if chunk.choices and len(chunk.choices) > 0: print(chunk.choices[0].delta.content or "", end="") agentops.end_trace(tracer, end_state="Success") # Let's check programmatically that spans were recorded in AgentOps print("\n" + "=" * 50) print("Now let's verify that our LLM calls were tracked properly...") try: result = agentops.validate_trace_spans(trace_context=tracer) agentops.print_validation_summary(result) except agentops.ValidationError as e: print(f"\n❌ Error validating spans: {e}") raise # Note that the response is a generator that yields chunks of the story. We can track this with AgentOps by navigating to the trace url and viewing the run. # All done! ``` ### examples/crewai/job_posting.py ```python # Crew Job Posting # First let's install the required packages # %pip install -U 'crewai[tools]' # Then import them from crewai import Crew, Agent, Task from crewai_tools.tools import WebsiteSearchTool, SerperDevTool, FileReadTool import agentops import os from dotenv import load_dotenv from textwrap import dedent # Next, we'll set our API keys. There are several ways to do this, the code below is just the most foolproof way for the purposes of this notebook. It accounts for both users who use environment variables and those who just want to set the API Key here in this notebook. # [Get an AgentOps API key](https://agentops.ai/settings/projects) # 1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or... # 2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo! load_dotenv() os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY", "your_api_key_here") os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "your_openai_api_key_here") os.environ["SERPER_API_KEY"] = os.getenv("SERPER_API_KEY", "your_serper_api_key_here") # Initialize AgentOps client agentops.init( auto_start_session=False, trace_name="CrewAI Job Posting", tags=["crewai", "job-posting", "agentops-example"] ) web_search_tool = WebsiteSearchTool() serper_dev_tool = SerperDevTool() file_read_tool = FileReadTool( file_path="job_description_example.md", description="A tool to read the job description example file.", ) class Agents: def research_agent(self): return Agent( role="Research Analyst", goal="Analyze the company website and provided description to extract insights on culture, values, and specific needs.", tools=[web_search_tool, serper_dev_tool], backstory="Expert in analyzing company cultures and identifying key values and needs from various sources, including websites and brief descriptions.", verbose=True, ) def writer_agent(self): return Agent( role="Job Description Writer", goal="Use insights from the Research Analyst to create a detailed, engaging, and enticing job posting.", tools=[web_search_tool, serper_dev_tool, file_read_tool], backstory="Skilled in crafting compelling job descriptions that resonate with the company's values and attract the right candidates.", verbose=True, ) def review_agent(self): return Agent( role="Review and Editing Specialist", goal="Review the job posting for clarity, engagement, grammatical accuracy, and alignment with company values and refine it to ensure perfection.", tools=[web_search_tool, serper_dev_tool, file_read_tool], backstory="A meticulous editor with an eye for detail, ensuring every piece of content is clear, engaging, and grammatically perfect.", verbose=True, ) class Tasks: def research_company_culture_task(self, agent, company_description, company_domain): return Task( description=dedent( f"""\ Analyze the provided company website and the hiring manager's company's domain {company_domain}, description: "{company_description}". Focus on understanding the company's culture, values, and mission. Identify unique selling points and specific projects or achievements highlighted on the site. Compile a report summarizing these insights, specifically how they can be leveraged in a job posting to attract the right candidates.""" ), expected_output=dedent( """\ A comprehensive report detailing the company's culture, values, and mission, along with specific selling points relevant to the job role. Suggestions on incorporating these insights into the job posting should be included.""" ), agent=agent, ) def research_role_requirements_task(self, agent, hiring_needs): return Task( description=dedent( f"""\ Based on the hiring manager's needs: "{hiring_needs}", identify the key skills, experiences, and qualities the ideal candidate should possess for the role. Consider the company's current projects, its competitive landscape, and industry trends. Prepare a list of recommended job requirements and qualifications that align with the company's needs and values.""" ), expected_output=dedent( """\ A list of recommended skills, experiences, and qualities for the ideal candidate, aligned with the company's culture, ongoing projects, and the specific role's requirements.""" ), agent=agent, ) def draft_job_posting_task(self, agent, company_description, hiring_needs, specific_benefits): return Task( description=dedent( f"""\ Draft a job posting for the role described by the hiring manager: "{hiring_needs}". Use the insights on "{company_description}" to start with a compelling introduction, followed by a detailed role description, responsibilities, and required skills and qualifications. Ensure the tone aligns with the company's culture and incorporate any unique benefits or opportunities offered by the company. Specfic benefits: "{specific_benefits}""" ), expected_output=dedent( """\ A detailed, engaging job posting that includes an introduction, role description, responsibilities, requirements, and unique company benefits. The tone should resonate with the company's culture and values, aimed at attracting the right candidates.""" ), agent=agent, ) def review_and_edit_job_posting_task(self, agent, hiring_needs): return Task( description=dedent( f"""\ Review the draft job posting for the role: "{hiring_needs}". Check for clarity, engagement, grammatical accuracy, and alignment with the company's culture and values. Edit and refine the content, ensuring it speaks directly to the desired candidates and accurately reflects the role's unique benefits and opportunities. Provide feedback for any necessary revisions.""" ), expected_output=dedent( """\ A polished, error-free job posting that is clear, engaging, and perfectly aligned with the company's culture and values. Feedback on potential improvements and final approval for publishing. Formated in markdown.""" ), agent=agent, output_file="job_posting.md", ) def industry_analysis_task(self, agent, company_domain, company_description): return Task( description=dedent( f"""\ Conduct an in-depth analysis of the industry related to the company's domain: "{company_domain}". Investigate current trends, challenges, and opportunities within the industry, utilizing market reports, recent developments, and expert opinions. Assess how these factors could impact the role being hired for and the overall attractiveness of the position to potential candidates. Consider how the company's position within this industry and its response to these trends could be leveraged to attract top talent. Include in your report how the role contributes to addressing industry challenges or seizing opportunities.""" ), expected_output=dedent( """\ A detailed analysis report that identifies major industry trends, challenges, and opportunities relevant to the company's domain and the specific job role. This report should provide strategic insights on positioning the job role and the company as an attractive choice for potential candidates.""" ), agent=agent, ) tracer = agentops.start_trace(trace_name="CrewAI Job Posting", tags=["crew-job-posting-example", "agentops-example"]) tasks = Tasks() agents = Agents() company_description = "We are a software company that builds AI-powered tools for businesses." company_domain = "https://www.agentops.ai" hiring_needs = "We are looking for a software engineer with 3 years of experience in Python and Django." specific_benefits = "We offer a competitive salary, health insurance, and a 401k plan." # Create Agents researcher_agent = agents.research_agent() writer_agent = agents.writer_agent() review_agent = agents.review_agent() # Define Tasks for each agent research_company_culture_task = tasks.research_company_culture_task( researcher_agent, company_description, company_domain ) industry_analysis_task = tasks.industry_analysis_task(researcher_agent, company_domain, company_description) research_role_requirements_task = tasks.research_role_requirements_task(researcher_agent, hiring_needs) draft_job_posting_task = tasks.draft_job_posting_task( writer_agent, company_description, hiring_needs, specific_benefits ) review_and_edit_job_posting_task = tasks.review_and_edit_job_posting_task(review_agent, hiring_needs) # Instantiate the crew with a sequential process crew = Crew( agents=[researcher_agent, writer_agent, review_agent], tasks=[ research_company_culture_task, industry_analysis_task, research_role_requirements_task, draft_job_posting_task, review_and_edit_job_posting_task, ], ) result = crew.kickoff() print("Job Posting Creation Process Completed.") print("Final Job Posting:") print(result) agentops.end_trace(tracer, end_state="Success") # Let's check programmatically that spans were recorded in AgentOps print("\n" + "=" * 50) print("Now let's verify that our LLM calls were tracked properly...") try: agentops.validate_trace_spans(trace_context=tracer) print("\n✅ Success! All LLM spans were properly recorded in AgentOps.") except agentops.ValidationError as e: print(f"\n❌ Error validating spans: {e}") raise ``` ### examples/langchain/langchain_examples.py ```python # AgentOps Langchain Agent Implementation # # Using AgentOps monitoring with Langchain is simple. We've created a LangchainCallbackHandler that will do all of the heavy lifting! # # First let's install the required packages # %pip install langchain # %pip install langchain_openai # %pip install agentops # %pip install python-dotenv # Then import them import os from langchain_openai import ChatOpenAI from langchain.agents import tool, AgentExecutor, create_openai_tools_agent from dotenv import load_dotenv from langchain_core.prompts import ChatPromptTemplate # The only difference with using AgentOps is that we'll also import this special Callback Handler from agentops.integration.callbacks.langchain import ( LangchainCallbackHandler as AgentOpsLangchainCallbackHandler, ) # Next, we'll set our API keys. There are several ways to do this, the code below is just the most foolproof way for the purposes of this notebook. It accounts for both users who use environment variables and those who just want to set the API Key here in this notebook. # # [Get an AgentOps API key](https://agentops.ai/settings/projects) # # 1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or... # # 2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo! load_dotenv() os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY", "your_api_key_here") os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "your_openai_api_key_here") # This is where AgentOps comes into play. Before creating our LLM instance via Langchain, first we'll create an instance of the AO LangchainCallbackHandler. After the handler is initialized, a session will be recorded automatically. # # Pass in your API key, and optionally any tags to describe this session for easier lookup in the AO dashboard. agentops_handler = AgentOpsLangchainCallbackHandler(tags=["Langchain Example", "agentops-example"]) llm = ChatOpenAI(callbacks=[agentops_handler], model="gpt-3.5-turbo") # You must pass in a callback handler to record your agent llm.callbacks = [agentops_handler] prompt = ChatPromptTemplate.from_messages( [ ("system", "You are a helpful assistant. Respond only in Spanish."), ("human", "{input}"), # Placeholders fill up a **list** of messages ("placeholder", "{agent_scratchpad}"), # ("tool_names", "find_movie") ] ) # Agents generally use tools. Let's define a simple tool here. Tool usage is also recorded. @tool def find_movie(genre: str) -> str: """Find available movies""" if genre == "drama": return "Dune 2" else: return "Pineapple Express" tools = [find_movie] # For each tool, you need to also add the callback handler for t in tools: t.callbacks = [agentops_handler] # Add the tools to our LLM llm_with_tools = llm.bind_tools([find_movie]) # Finally, let's create our agent! Pass in the callback handler to the agent, and all the actions will be recorded in the AO Dashboard agent = create_openai_tools_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools) agent_executor.invoke({"input": "What comedies are playing?"}, config={"callback": [agentops_handler]}) # ## Check your session # Finally, check your run on [AgentOps](https://app.agentops.ai). You will see a session recorded with the LLM calls and tool usage. # Let's check programmatically that spans were recorded in AgentOps print("\n" + "=" * 50) print("Now let's verify that our LLM calls were tracked properly...") try: import agentops agentops.validate_trace_spans(trace_context=None) print("\n✅ Success! All LLM spans were properly recorded in AgentOps.") except ImportError: print("\n❌ Error: agentops library not installed. Please install it to validate spans.") except agentops.ValidationError as e: print(f"\n❌ Error validating spans: {e}") raise ``` ### examples/README.md # AgentOps Examples This directory contains comprehensive examples demonstrating how to integrate AgentOps with various AI/ML frameworks, libraries, and providers. Each example is provided as a Jupyter notebook and a Python script with detailed explanations and code samples. ## Directory Structure - **[`ag2/`](https://github.com/AgentOps-AI/agentops/blob/main/ag2)** - Examples for AG2 (AutoGen 2.0) multi-agent conversations - `agentchat_with_memory` - Agent chat with persistent memory - `async_human_input` - Asynchronous human input handling - `tools_wikipedia_search` - Wikipedia search tool integration - **[`anthropic/`](https://github.com/AgentOps-AI/agentops/blob/main/anthropic)** - Anthropic Claude API integration examples - `agentops-anthropic-understanding-tools` - Deep dive into tool usage - `anthropic-example-async` - Asynchronous API calls - `anthropic-example-sync` - Synchronous API calls - `antrophic-example-tool` - Tool calling examples - `README.md` - Detailed Anthropic integration guide - **[`autogen/`](https://github.com/AgentOps-AI/agentops/blob/main/autogen)** - Microsoft AutoGen framework examples - `AgentChat` - Basic agent chat functionality - `MathAgent` - Mathematical problem-solving agent - **[`crewai/`](https://github.com/AgentOps-AI/agentops/blob/main/crewai)** - CrewAI multi-agent framework examples - `job_posting` - Job posting automation workflow - `markdown_validator` - Markdown validation agent - **[`gemini/`](https://github.com/AgentOps-AI/agentops/blob/main/gemini)** - Google Gemini API integration - `gemini_example` - Basic Gemini API usage with AgentOps - **[`google_adk/`](https://github.com/AgentOps-AI/agentops/blob/main/google_adk)** - Google AI Development Kit examples - `human_approval` - Human-in-the-loop approval workflows - **[`langchain/`](https://github.com/AgentOps-AI/agentops/blob/main/langchain)** - LangChain framework integration - `langchain_examples` - Comprehensive LangChain usage examples - **[`litellm/`](https://github.com/AgentOps-AI/agentops/blob/main/litellm)** - LiteLLM proxy integration - `litellm_example` - Multi-provider LLM access through LiteLLM - **[`openai/`](https://github.com/AgentOps-AI/agentops/blob/main/openai)** - OpenAI API integration examples - `multi_tool_orchestration` - Complex tool orchestration - `openai_example_async` - Asynchronous OpenAI API calls - `openai_example_sync` - Synchronous OpenAI API calls - `web_search` - Web search functionality - **[`openai_agents/`](https://github.com/AgentOps-AI/agentops/blob/main/openai_agents)** - OpenAI Agents SDK examples - `agent_patterns` - Common agent design patterns - `agents_tools` - Agent tool integration - `customer_service_agent` - Customer service automation - **[`smolagents/`](https://github.com/AgentOps-AI/agentops/blob/main/smolagents)** - SmolAgents framework examples - `multi_smolagents_system` - Multi-agent system coordination - `text_to_sql` - Natural language to SQL conversion - **[`watsonx/`](https://github.com/AgentOps-AI/agentops/blob/main/watsonx)** - IBM Watsonx AI integration - `watsonx-streaming` - Streaming text generation - `watsonx-text-chat` - Text generation and chat completion - `watsonx-tokeniation-model` - Tokenization and model details - `README.md` - Detailed Watsonx integration guide - **[`xai/`](https://github.com/AgentOps-AI/agentops/blob/main/xai)** - xAI (Grok) API integration - `grok_examples` - Basic Grok API usage - `grok_vision_examples` - Vision capabilities with Grok ### Utility Scripts - **[`generate_documentation.py`](https://github.com/AgentOps-AI/agentops/blob/main/generate_documentation.py)** - Script to convert Jupyter notebooks to MDX documentation files - Converts notebooks from `examples/` to `docs/v2/examples/` - Handles frontmatter, GitHub links, and installation sections - Transforms `%pip install` commands to CodeGroup format ## Prerequisites 1. **AgentOps Account**: Sign up at [agentops.ai](https://agentops.ai) 2. **Python Environment**: Python 3.10+ recommended 3. **API Keys**: Obtain API keys for the services you want to use ## Documentation Generation The `generate_documentation.py` script automatically converts these Jupyter notebook examples into documentation for the AgentOps website. It: - Extracts notebook content and converts to Markdown - Adds proper frontmatter and metadata - Transforms installation commands into user-friendly format - Generates GitHub links for source notebooks - Creates MDX files in `docs/v2/examples/` ### Usage ```bash python examples/generate_documentation.py examples/langchain/langchain_examples.ipynb ``` ## Contributing When adding new examples: 1. Create a new subdirectory for the framework/provider 2. Include comprehensive Jupyter notebooks with explanations 3. Add a README.md if the integration is complex 4. Ensure examples are self-contained and runnable 5. Follow the existing naming conventions 6. Use the `generate_documentation.py` script to create documentation files 7. Add the example notebook to the main `README.md` for visibility 8. Add the generated documentation to the `docs/v2/examples/` directory for website visibility 9. Submit a pull request with a clear description of your changes ## Additional Resources - [AgentOps Documentation](https://docs.agentops.ai) - [AgentOps Dashboard](https://app.agentops.ai) - [GitHub Repository](https://github.com/AgentOps-AI/agentops) - [Community Discord](https://discord.gg/agentops) ## License These examples are provided under the same license as the AgentOps project. See the main repository for license details.