agentbasis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentbasis/__init__.py +87 -0
- agentbasis/client.py +134 -0
- agentbasis/config.py +33 -0
- agentbasis/context.py +259 -0
- agentbasis/decorators.py +80 -0
- agentbasis/frameworks/langchain/__init__.py +109 -0
- agentbasis/frameworks/langchain/callback.py +373 -0
- agentbasis/frameworks/pydanticai/__init__.py +32 -0
- agentbasis/frameworks/pydanticai/instrumentation.py +233 -0
- agentbasis/llms/anthropic/__init__.py +18 -0
- agentbasis/llms/anthropic/messages.py +298 -0
- agentbasis/llms/gemini/__init__.py +18 -0
- agentbasis/llms/gemini/chat.py +326 -0
- agentbasis/llms/openai/__init__.py +18 -0
- agentbasis/llms/openai/chat.py +235 -0
- agentbasis-0.1.0.dist-info/METADATA +220 -0
- agentbasis-0.1.0.dist-info/RECORD +19 -0
- agentbasis-0.1.0.dist-info/WHEEL +5 -0
- agentbasis-0.1.0.dist-info/top_level.txt +1 -0
agentbasis/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from .client import AgentBasis
|
|
3
|
+
from .decorators import trace
|
|
4
|
+
from .context import (
|
|
5
|
+
context,
|
|
6
|
+
set_user,
|
|
7
|
+
set_session,
|
|
8
|
+
set_conversation,
|
|
9
|
+
set_metadata,
|
|
10
|
+
with_context,
|
|
11
|
+
AgentBasisContext,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init(api_key: Optional[str] = None, agent_id: Optional[str] = None) -> AgentBasis:
|
|
16
|
+
"""
|
|
17
|
+
Initialize the AgentBasis SDK.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
api_key: Your AgentBasis API Key. If not provided, reads from AGENTBASIS_API_KEY env var.
|
|
21
|
+
agent_id: The ID of the agent to track. If not provided, reads from AGENTBASIS_AGENT_ID env var.
|
|
22
|
+
Returns:
|
|
23
|
+
The initialized AgentBasis client instance.
|
|
24
|
+
"""
|
|
25
|
+
return AgentBasis.initialize(api_key=api_key, agent_id=agent_id)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def flush(timeout_millis: int = 30000) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Force flush all pending telemetry data.
|
|
31
|
+
|
|
32
|
+
This is useful when you want to ensure all traces are sent before
|
|
33
|
+
a critical operation, at specific checkpoints, or before exiting.
|
|
34
|
+
|
|
35
|
+
Note: The SDK automatically flushes on normal Python exit via atexit.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
timeout_millis: Maximum time to wait for flush (default 30 seconds).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if flush completed successfully, False if timed out or not initialized.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> agentbasis.init(api_key="...", agent_id="...")
|
|
45
|
+
>>> # ... your agent code ...
|
|
46
|
+
>>> agentbasis.flush() # Ensure all data is sent
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
client = AgentBasis.get_instance()
|
|
50
|
+
return client.flush(timeout_millis)
|
|
51
|
+
except RuntimeError:
|
|
52
|
+
# SDK not initialized
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def shutdown():
|
|
57
|
+
"""
|
|
58
|
+
Manually shut down the SDK and flush all pending data.
|
|
59
|
+
|
|
60
|
+
This is automatically called on Python exit, but can be called
|
|
61
|
+
manually if you need to shut down the SDK before the process ends.
|
|
62
|
+
|
|
63
|
+
This method is idempotent - calling it multiple times is safe.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
client = AgentBasis.get_instance()
|
|
67
|
+
client.shutdown()
|
|
68
|
+
except RuntimeError:
|
|
69
|
+
# SDK not initialized
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"init",
|
|
75
|
+
"AgentBasis",
|
|
76
|
+
"trace",
|
|
77
|
+
"flush",
|
|
78
|
+
"shutdown",
|
|
79
|
+
# Context management
|
|
80
|
+
"context",
|
|
81
|
+
"set_user",
|
|
82
|
+
"set_session",
|
|
83
|
+
"set_conversation",
|
|
84
|
+
"set_metadata",
|
|
85
|
+
"with_context",
|
|
86
|
+
"AgentBasisContext",
|
|
87
|
+
]
|
agentbasis/client.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import atexit
|
|
3
|
+
from opentelemetry import trace
|
|
4
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
5
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
7
|
+
from opentelemetry.sdk.resources import Resource
|
|
8
|
+
|
|
9
|
+
from .config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentBasis:
|
|
13
|
+
"""
|
|
14
|
+
The main AgentBasis client.
|
|
15
|
+
Manages OpenTelemetry configuration and data transmission.
|
|
16
|
+
"""
|
|
17
|
+
_instance: Optional['AgentBasis'] = None
|
|
18
|
+
_shutdown_registered: bool = False
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: Config):
|
|
21
|
+
self.config = config
|
|
22
|
+
self._is_shutdown = False
|
|
23
|
+
|
|
24
|
+
# 1. Create Resource (Metadata about who is sending data)
|
|
25
|
+
attributes = {
|
|
26
|
+
"service.name": "agentbasis-python-sdk",
|
|
27
|
+
# We can add more metadata here like environment
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Add agent_id if present (it should be, as Config validates it)
|
|
31
|
+
if config.agent_id:
|
|
32
|
+
attributes["service.instance.id"] = config.agent_id
|
|
33
|
+
# Also adding a custom attribute just in case we want to query by it explicitly later
|
|
34
|
+
attributes["agentbasis.agent.id"] = config.agent_id
|
|
35
|
+
|
|
36
|
+
resource = Resource.create(attributes=attributes)
|
|
37
|
+
|
|
38
|
+
# 2. Initialize Tracer Provider
|
|
39
|
+
self.tracer_provider = TracerProvider(resource=resource)
|
|
40
|
+
|
|
41
|
+
# 3. Configure Exporter
|
|
42
|
+
endpoint = f"{config.api_url}/api/v1/traces"
|
|
43
|
+
exporter = OTLPSpanExporter(
|
|
44
|
+
endpoint=endpoint,
|
|
45
|
+
headers={"x-api-key": config.api_key}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 4. Add Batch Processor (Background thread for sending)
|
|
49
|
+
self._processor = BatchSpanProcessor(exporter)
|
|
50
|
+
self.tracer_provider.add_span_processor(self._processor)
|
|
51
|
+
|
|
52
|
+
# 5. Register as Global Tracer
|
|
53
|
+
# This allows trace.get_tracer(__name__) to work anywhere in the user's code
|
|
54
|
+
trace.set_tracer_provider(self.tracer_provider)
|
|
55
|
+
|
|
56
|
+
# 6. Register atexit handler for graceful shutdown
|
|
57
|
+
self._register_atexit()
|
|
58
|
+
|
|
59
|
+
def _register_atexit(self):
|
|
60
|
+
"""
|
|
61
|
+
Register the shutdown handler to run when Python exits.
|
|
62
|
+
Only registers once to avoid duplicate handlers.
|
|
63
|
+
"""
|
|
64
|
+
if not AgentBasis._shutdown_registered:
|
|
65
|
+
atexit.register(self._atexit_handler)
|
|
66
|
+
AgentBasis._shutdown_registered = True
|
|
67
|
+
|
|
68
|
+
def _atexit_handler(self):
|
|
69
|
+
"""
|
|
70
|
+
Handler called when Python exits. Flushes and shuts down gracefully.
|
|
71
|
+
"""
|
|
72
|
+
self.shutdown()
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def initialize(cls, api_key: Optional[str] = None, agent_id: Optional[str] = None) -> 'AgentBasis':
|
|
76
|
+
"""
|
|
77
|
+
Initializes the global AgentBasis client.
|
|
78
|
+
"""
|
|
79
|
+
config = Config(api_key=api_key, agent_id=agent_id)
|
|
80
|
+
config.validate()
|
|
81
|
+
|
|
82
|
+
cls._instance = cls(config)
|
|
83
|
+
return cls._instance
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def get_instance(cls) -> 'AgentBasis':
|
|
87
|
+
"""
|
|
88
|
+
Returns the global AgentBasis client instance.
|
|
89
|
+
"""
|
|
90
|
+
if cls._instance is None:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
"AgentBasis is not initialized. "
|
|
93
|
+
"Please call `agentbasis.init(api_key='...', agent_id='...')` first."
|
|
94
|
+
)
|
|
95
|
+
return cls._instance
|
|
96
|
+
|
|
97
|
+
def flush(self, timeout_millis: int = 30000) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Forces a flush of all pending spans.
|
|
100
|
+
|
|
101
|
+
This is useful when you want to ensure all telemetry is sent before
|
|
102
|
+
a critical operation, or at specific checkpoints in your application.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
timeout_millis: Maximum time to wait for flush to complete (default 30 seconds).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if flush completed successfully, False if timed out.
|
|
109
|
+
"""
|
|
110
|
+
if self._is_shutdown:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if self.tracer_provider and hasattr(self.tracer_provider, 'force_flush'):
|
|
114
|
+
return self.tracer_provider.force_flush(timeout_millis)
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
def shutdown(self):
|
|
118
|
+
"""
|
|
119
|
+
Flushes remaining spans and shuts down the provider.
|
|
120
|
+
|
|
121
|
+
This method is idempotent - calling it multiple times is safe.
|
|
122
|
+
It's automatically called when Python exits via atexit.
|
|
123
|
+
"""
|
|
124
|
+
if self._is_shutdown:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
self._is_shutdown = True
|
|
128
|
+
|
|
129
|
+
if self.tracer_provider:
|
|
130
|
+
try:
|
|
131
|
+
self.tracer_provider.shutdown()
|
|
132
|
+
except Exception:
|
|
133
|
+
# Silently ignore shutdown errors to avoid noise during exit
|
|
134
|
+
pass
|
agentbasis/config.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
"""
|
|
6
|
+
Configuration settings for the AgentBasis SDK.
|
|
7
|
+
Handles API keys and Agent IDs.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, api_key: Optional[str] = None, agent_id: Optional[str] = None):
|
|
10
|
+
self.api_key = api_key or os.environ.get("AGENTBASIS_API_KEY")
|
|
11
|
+
self.agent_id = agent_id or os.environ.get("AGENTBASIS_AGENT_ID")
|
|
12
|
+
|
|
13
|
+
# Backend endpoint - not user-configurable, but can be overridden via env var for testing
|
|
14
|
+
self.api_url = os.environ.get("AGENTBASIS_API_URL", "https://api.agentbasis.co")
|
|
15
|
+
|
|
16
|
+
def validate(self):
|
|
17
|
+
"""
|
|
18
|
+
Checks if the configuration is valid (i.e., has an API key and Agent ID).
|
|
19
|
+
Raises a ValueError if the key is missing.
|
|
20
|
+
"""
|
|
21
|
+
if not self.api_key:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"AgentBasis API Key is missing. "
|
|
24
|
+
"Please provide it via `agentbasis.init(api_key='...')` "
|
|
25
|
+
"or set the `AGENTBASIS_API_KEY` environment variable."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if not self.agent_id:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"AgentBasis Agent ID is missing. "
|
|
31
|
+
"Please provide it via `agentbasis.init(agent_id='...')` "
|
|
32
|
+
"or set the `AGENTBASIS_AGENT_ID` environment variable."
|
|
33
|
+
)
|
agentbasis/context.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context management for AgentBasis.
|
|
3
|
+
|
|
4
|
+
This module provides context tracking for user sessions, allowing developers
|
|
5
|
+
to associate traces with specific users, sessions, and conversations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
import functools
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Context variables for storing user/session info
|
|
16
|
+
# These are thread-safe and async-safe
|
|
17
|
+
_user_id: ContextVar[Optional[str]] = ContextVar('user_id', default=None)
|
|
18
|
+
_session_id: ContextVar[Optional[str]] = ContextVar('session_id', default=None)
|
|
19
|
+
_conversation_id: ContextVar[Optional[str]] = ContextVar('conversation_id', default=None)
|
|
20
|
+
_metadata: ContextVar[Optional[Dict[str, Any]]] = ContextVar('metadata', default=None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentBasisContext:
|
|
24
|
+
"""
|
|
25
|
+
Context manager for setting user/session context.
|
|
26
|
+
|
|
27
|
+
All spans created within this context will automatically include
|
|
28
|
+
the user_id, session_id, and other context attributes.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
with AgentBasisContext(user_id="user-123", session_id="sess-456"):
|
|
32
|
+
response = client.chat.completions.create(...)
|
|
33
|
+
# This span will have user_id and session_id attributes
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
user_id: Optional[str] = None,
|
|
39
|
+
session_id: Optional[str] = None,
|
|
40
|
+
conversation_id: Optional[str] = None,
|
|
41
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
42
|
+
):
|
|
43
|
+
self.user_id = user_id
|
|
44
|
+
self.session_id = session_id
|
|
45
|
+
self.conversation_id = conversation_id
|
|
46
|
+
self.metadata = metadata
|
|
47
|
+
|
|
48
|
+
# Store tokens to reset on exit
|
|
49
|
+
self._tokens = []
|
|
50
|
+
|
|
51
|
+
def __enter__(self):
|
|
52
|
+
# Set context variables and store tokens for cleanup
|
|
53
|
+
if self.user_id is not None:
|
|
54
|
+
self._tokens.append(('user_id', _user_id.set(self.user_id)))
|
|
55
|
+
if self.session_id is not None:
|
|
56
|
+
self._tokens.append(('session_id', _session_id.set(self.session_id)))
|
|
57
|
+
if self.conversation_id is not None:
|
|
58
|
+
self._tokens.append(('conversation_id', _conversation_id.set(self.conversation_id)))
|
|
59
|
+
if self.metadata is not None:
|
|
60
|
+
self._tokens.append(('metadata', _metadata.set(self.metadata)))
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
64
|
+
# Reset context variables to their previous values
|
|
65
|
+
for name, token in self._tokens:
|
|
66
|
+
if name == 'user_id':
|
|
67
|
+
_user_id.reset(token)
|
|
68
|
+
elif name == 'session_id':
|
|
69
|
+
_session_id.reset(token)
|
|
70
|
+
elif name == 'conversation_id':
|
|
71
|
+
_conversation_id.reset(token)
|
|
72
|
+
elif name == 'metadata':
|
|
73
|
+
_metadata.reset(token)
|
|
74
|
+
return False # Don't suppress exceptions
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Convenience function (alias for AgentBasisContext)
|
|
78
|
+
@contextmanager
|
|
79
|
+
def context(
|
|
80
|
+
user_id: Optional[str] = None,
|
|
81
|
+
session_id: Optional[str] = None,
|
|
82
|
+
conversation_id: Optional[str] = None,
|
|
83
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
84
|
+
):
|
|
85
|
+
"""
|
|
86
|
+
Context manager for setting user/session context.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
with agentbasis.context(user_id="user-123"):
|
|
90
|
+
response = client.chat.completions.create(...)
|
|
91
|
+
"""
|
|
92
|
+
with AgentBasisContext(
|
|
93
|
+
user_id=user_id,
|
|
94
|
+
session_id=session_id,
|
|
95
|
+
conversation_id=conversation_id,
|
|
96
|
+
metadata=metadata
|
|
97
|
+
):
|
|
98
|
+
yield
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Global setters for simpler usage
|
|
102
|
+
def set_user(user_id: Optional[str]):
|
|
103
|
+
"""
|
|
104
|
+
Set the current user ID globally.
|
|
105
|
+
|
|
106
|
+
This affects all subsequent spans until changed or cleared.
|
|
107
|
+
For scoped context, use the context() context manager instead.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
agentbasis.set_user("user-123")
|
|
111
|
+
response = client.chat.completions.create(...) # Has user_id
|
|
112
|
+
agentbasis.set_user(None) # Clear
|
|
113
|
+
"""
|
|
114
|
+
_user_id.set(user_id)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def set_session(session_id: Optional[str]):
|
|
118
|
+
"""
|
|
119
|
+
Set the current session ID globally.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
agentbasis.set_session("sess-456")
|
|
123
|
+
"""
|
|
124
|
+
_session_id.set(session_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def set_conversation(conversation_id: Optional[str]):
|
|
128
|
+
"""
|
|
129
|
+
Set the current conversation ID globally.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
agentbasis.set_conversation("conv-789")
|
|
133
|
+
"""
|
|
134
|
+
_conversation_id.set(conversation_id)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_metadata(metadata: Optional[Dict[str, Any]]):
|
|
138
|
+
"""
|
|
139
|
+
Set custom metadata globally.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
agentbasis.set_metadata({"plan": "pro", "feature_flag": "new_ui"})
|
|
143
|
+
"""
|
|
144
|
+
_metadata.set(metadata)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Getters for internal use by instrumentations
|
|
148
|
+
def get_user() -> Optional[str]:
|
|
149
|
+
"""Get the current user ID from context."""
|
|
150
|
+
return _user_id.get()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_session() -> Optional[str]:
|
|
154
|
+
"""Get the current session ID from context."""
|
|
155
|
+
return _session_id.get()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_conversation() -> Optional[str]:
|
|
159
|
+
"""Get the current conversation ID from context."""
|
|
160
|
+
return _conversation_id.get()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_metadata() -> Optional[Dict[str, Any]]:
|
|
164
|
+
"""Get the current metadata from context."""
|
|
165
|
+
return _metadata.get()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_context_attributes() -> Dict[str, Any]:
|
|
169
|
+
"""
|
|
170
|
+
Get all context attributes as a dictionary.
|
|
171
|
+
|
|
172
|
+
This is used internally by instrumentations to inject context into spans.
|
|
173
|
+
Only returns attributes that are set (not None).
|
|
174
|
+
"""
|
|
175
|
+
attributes = {}
|
|
176
|
+
|
|
177
|
+
user_id = get_user()
|
|
178
|
+
if user_id:
|
|
179
|
+
attributes["agentbasis.user.id"] = user_id
|
|
180
|
+
|
|
181
|
+
session_id = get_session()
|
|
182
|
+
if session_id:
|
|
183
|
+
attributes["agentbasis.session.id"] = session_id
|
|
184
|
+
|
|
185
|
+
conversation_id = get_conversation()
|
|
186
|
+
if conversation_id:
|
|
187
|
+
attributes["agentbasis.conversation.id"] = conversation_id
|
|
188
|
+
|
|
189
|
+
metadata = get_metadata()
|
|
190
|
+
if metadata:
|
|
191
|
+
attributes["agentbasis.metadata"] = json.dumps(metadata)
|
|
192
|
+
|
|
193
|
+
return attributes
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Decorator for function-level context
|
|
197
|
+
def with_context(
|
|
198
|
+
user_id: Optional[str] = None,
|
|
199
|
+
session_id: Optional[str] = None,
|
|
200
|
+
conversation_id: Optional[str] = None,
|
|
201
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
202
|
+
):
|
|
203
|
+
"""
|
|
204
|
+
Decorator to set context for a function.
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
@agentbasis.with_context(user_id="user-123")
|
|
208
|
+
def my_agent_function():
|
|
209
|
+
response = client.chat.completions.create(...)
|
|
210
|
+
return response
|
|
211
|
+
"""
|
|
212
|
+
def decorator(func):
|
|
213
|
+
@functools.wraps(func)
|
|
214
|
+
def wrapper(*args, **kwargs):
|
|
215
|
+
with AgentBasisContext(
|
|
216
|
+
user_id=user_id,
|
|
217
|
+
session_id=session_id,
|
|
218
|
+
conversation_id=conversation_id,
|
|
219
|
+
metadata=metadata
|
|
220
|
+
):
|
|
221
|
+
return func(*args, **kwargs)
|
|
222
|
+
|
|
223
|
+
@functools.wraps(func)
|
|
224
|
+
async def async_wrapper(*args, **kwargs):
|
|
225
|
+
with AgentBasisContext(
|
|
226
|
+
user_id=user_id,
|
|
227
|
+
session_id=session_id,
|
|
228
|
+
conversation_id=conversation_id,
|
|
229
|
+
metadata=metadata
|
|
230
|
+
):
|
|
231
|
+
return await func(*args, **kwargs)
|
|
232
|
+
|
|
233
|
+
# Return appropriate wrapper based on function type
|
|
234
|
+
if _is_async_function(func):
|
|
235
|
+
return async_wrapper
|
|
236
|
+
return wrapper
|
|
237
|
+
|
|
238
|
+
return decorator
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _is_async_function(func) -> bool:
|
|
242
|
+
"""Check if a function is async."""
|
|
243
|
+
import asyncio
|
|
244
|
+
return asyncio.iscoroutinefunction(func)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def inject_context_to_span(span) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Inject current context attributes into a span.
|
|
250
|
+
|
|
251
|
+
This is called by LLM instrumentations to automatically add
|
|
252
|
+
user/session context to every span.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
span: An OpenTelemetry Span object
|
|
256
|
+
"""
|
|
257
|
+
attributes = get_context_attributes()
|
|
258
|
+
for key, value in attributes.items():
|
|
259
|
+
span.set_attribute(key, value)
|
agentbasis/decorators.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from opentelemetry import trace as otel_trace
|
|
5
|
+
from opentelemetry.trace import Status, StatusCode
|
|
6
|
+
|
|
7
|
+
from agentbasis.context import inject_context_to_span
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def trace(func: Callable) -> Callable:
|
|
11
|
+
"""
|
|
12
|
+
Decorator to track the execution of a function as an OTel Span.
|
|
13
|
+
|
|
14
|
+
Works with both sync and async functions. Automatically injects
|
|
15
|
+
user/session context from agentbasis.context.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
@agentbasis.trace
|
|
19
|
+
def my_function():
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@agentbasis.trace
|
|
23
|
+
async def my_async_function():
|
|
24
|
+
...
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
def sync_wrapper(*args, **kwargs):
|
|
29
|
+
# Get the tracer at runtime, so it uses the configured provider
|
|
30
|
+
tracer = otel_trace.get_tracer("agentbasis")
|
|
31
|
+
|
|
32
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
33
|
+
# Inject user/session context
|
|
34
|
+
inject_context_to_span(span)
|
|
35
|
+
|
|
36
|
+
# Record Inputs
|
|
37
|
+
span.set_attribute("code.function", func.__name__)
|
|
38
|
+
span.set_attribute("input.args", str(args))
|
|
39
|
+
span.set_attribute("input.kwargs", str(kwargs))
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
result = func(*args, **kwargs)
|
|
43
|
+
span.set_attribute("output", str(result))
|
|
44
|
+
span.set_status(Status(StatusCode.OK))
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
span.record_exception(e)
|
|
49
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
@functools.wraps(func)
|
|
53
|
+
async def async_wrapper(*args, **kwargs):
|
|
54
|
+
# Get the tracer at runtime
|
|
55
|
+
tracer = otel_trace.get_tracer("agentbasis")
|
|
56
|
+
|
|
57
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
58
|
+
# Inject user/session context
|
|
59
|
+
inject_context_to_span(span)
|
|
60
|
+
|
|
61
|
+
# Record Inputs
|
|
62
|
+
span.set_attribute("code.function", func.__name__)
|
|
63
|
+
span.set_attribute("input.args", str(args))
|
|
64
|
+
span.set_attribute("input.kwargs", str(kwargs))
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
result = await func(*args, **kwargs)
|
|
68
|
+
span.set_attribute("output", str(result))
|
|
69
|
+
span.set_status(Status(StatusCode.OK))
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
span.record_exception(e)
|
|
74
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
# Return appropriate wrapper based on function type
|
|
78
|
+
if asyncio.iscoroutinefunction(func):
|
|
79
|
+
return async_wrapper
|
|
80
|
+
return sync_wrapper
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from .callback import AgentBasisCallbackHandler
|
|
2
|
+
|
|
3
|
+
# Module-level handler instance for convenience
|
|
4
|
+
_handler_instance = None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_callback_handler() -> AgentBasisCallbackHandler:
|
|
8
|
+
"""
|
|
9
|
+
Get a new AgentBasis callback handler for LangChain.
|
|
10
|
+
|
|
11
|
+
Use this to get a handler instance that you can pass to your
|
|
12
|
+
LangChain chains, agents, or LLMs.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A new AgentBasisCallbackHandler instance
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
from agentbasis.frameworks.langchain import get_callback_handler
|
|
19
|
+
|
|
20
|
+
handler = get_callback_handler()
|
|
21
|
+
|
|
22
|
+
# Pass to chain
|
|
23
|
+
chain.invoke({"query": "..."}, config={"callbacks": [handler]})
|
|
24
|
+
|
|
25
|
+
# Or pass to LLM
|
|
26
|
+
llm = ChatOpenAI(callbacks=[handler])
|
|
27
|
+
|
|
28
|
+
# Or pass to agent
|
|
29
|
+
agent_executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
|
|
30
|
+
"""
|
|
31
|
+
return AgentBasisCallbackHandler()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def instrument() -> AgentBasisCallbackHandler:
|
|
35
|
+
"""
|
|
36
|
+
Get the global AgentBasis callback handler for LangChain.
|
|
37
|
+
|
|
38
|
+
This returns a singleton handler instance that can be reused across
|
|
39
|
+
your application. For most use cases, you should pass this handler
|
|
40
|
+
explicitly to your LangChain components.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The global AgentBasisCallbackHandler instance
|
|
44
|
+
|
|
45
|
+
Example - Explicit callbacks (recommended):
|
|
46
|
+
from agentbasis.frameworks.langchain import instrument
|
|
47
|
+
|
|
48
|
+
handler = instrument()
|
|
49
|
+
|
|
50
|
+
# Option 1: Pass to invoke/run
|
|
51
|
+
chain.invoke({"query": "..."}, config={"callbacks": [handler]})
|
|
52
|
+
|
|
53
|
+
# Option 2: Pass to constructor
|
|
54
|
+
llm = ChatOpenAI(model="gpt-4", callbacks=[handler])
|
|
55
|
+
|
|
56
|
+
# Option 3: Pass to agent executor
|
|
57
|
+
agent = AgentExecutor(
|
|
58
|
+
agent=agent,
|
|
59
|
+
tools=tools,
|
|
60
|
+
callbacks=[handler]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
Example - With RunnableConfig:
|
|
64
|
+
from langchain_core.runnables import RunnableConfig
|
|
65
|
+
|
|
66
|
+
handler = instrument()
|
|
67
|
+
config = RunnableConfig(callbacks=[handler])
|
|
68
|
+
|
|
69
|
+
result = chain.invoke({"query": "..."}, config=config)
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
Unlike OpenAI/Anthropic instrumentation which patches the SDK globally,
|
|
73
|
+
LangChain requires explicit callback passing. This is because LangChain
|
|
74
|
+
has its own callback system that doesn't support global monkey-patching.
|
|
75
|
+
"""
|
|
76
|
+
global _handler_instance
|
|
77
|
+
|
|
78
|
+
if _handler_instance is None:
|
|
79
|
+
_handler_instance = AgentBasisCallbackHandler()
|
|
80
|
+
|
|
81
|
+
return _handler_instance
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_callback_config():
|
|
85
|
+
"""
|
|
86
|
+
Get a LangChain RunnableConfig with AgentBasis callbacks pre-configured.
|
|
87
|
+
|
|
88
|
+
This is a convenience function for users who prefer working with
|
|
89
|
+
RunnableConfig objects.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A dict that can be passed as the `config` parameter to invoke/batch/stream
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
from agentbasis.frameworks.langchain import get_callback_config
|
|
96
|
+
|
|
97
|
+
config = get_callback_config()
|
|
98
|
+
result = chain.invoke({"query": "..."}, config=config)
|
|
99
|
+
"""
|
|
100
|
+
handler = instrument()
|
|
101
|
+
return {"callbacks": [handler]}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"AgentBasisCallbackHandler",
|
|
106
|
+
"instrument",
|
|
107
|
+
"get_callback_handler",
|
|
108
|
+
"get_callback_config",
|
|
109
|
+
]
|