agentops/llms.txt

4093 lines
146 KiB
Plaintext

# 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 `<your_agentops_key>` 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 `<your_agentops_key>` 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.