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 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)
@@ -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
+ ]