agenta 0.68.0__py3-none-any.whl → 0.70.1__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.
agenta/__init__.py CHANGED
@@ -1,52 +1,50 @@
1
1
  from typing import Any, Callable, Optional
2
2
 
3
- from .sdk.utils.preinit import PreInitObject
4
-
5
- from agenta.client import AgentaApi, AsyncAgentaApi
6
3
  import agenta.client.backend.types as client_types # pylint: disable=wrong-import-order
4
+ from agenta.client import AgentaApi, AsyncAgentaApi
7
5
 
8
- from .sdk.types import (
9
- MCField,
10
- DictInput,
11
- MultipleChoice,
12
- FloatParam,
13
- IntParam,
14
- MultipleChoiceParam,
15
- GroupedMultipleChoiceParam,
16
- MessagesInput,
17
- TextParam,
18
- FileInputURL,
19
- BinaryParam,
20
- Prompt,
21
- PromptTemplate,
22
- )
6
+ from .sdk import assets as assets
23
7
 
24
- from .sdk.agenta_init import Config, AgentaSingleton, init as _init
25
- from .sdk.utils.logging import get_module_logger
26
- from .sdk.utils.costs import calculate_token_usage
27
- from .sdk.tracing import Tracing, get_tracer
28
- from .sdk.tracing.conventions import Reference
29
- from .sdk.decorators.tracing import instrument
8
+ # evaluations
9
+ from .sdk import testsets as testsets
10
+ from .sdk import tracer
11
+ from .sdk.agenta_init import AgentaSingleton, Config
12
+ from .sdk.agenta_init import init as _init
13
+ from .sdk.context.running import workflow_mode_enabled
30
14
  from .sdk.decorators.running import (
31
- workflow,
32
15
  application,
33
16
  evaluator,
17
+ workflow,
34
18
  )
35
- from .sdk.decorators.serving import route, app
36
- from .sdk.context.running import workflow_mode_enabled
19
+ from .sdk.decorators.serving import app, route
20
+ from .sdk.decorators.tracing import instrument
37
21
  from .sdk.litellm import litellm as callbacks
38
22
  from .sdk.managers.apps import AppManager
39
- from .sdk.managers.vault import VaultManager
40
- from .sdk.managers.secrets import SecretsManager
41
23
  from .sdk.managers.config import ConfigManager
42
- from .sdk.managers.variant import VariantManager
43
24
  from .sdk.managers.deployment import DeploymentManager
44
- from .sdk import assets as assets
45
- from .sdk import tracer
46
-
47
- # evaluations
48
- from .sdk import testsets as testsets
49
-
25
+ from .sdk.managers.secrets import SecretsManager
26
+ from .sdk.managers.variant import VariantManager
27
+ from .sdk.managers.vault import VaultManager
28
+ from .sdk.tracing import Tracing, get_tracer
29
+ from .sdk.tracing.conventions import Reference
30
+ from .sdk.types import (
31
+ BinaryParam,
32
+ DictInput,
33
+ FileInputURL,
34
+ FloatParam,
35
+ GroupedMultipleChoiceParam,
36
+ IntParam,
37
+ MCField,
38
+ MessagesInput,
39
+ MultipleChoice,
40
+ MultipleChoiceParam,
41
+ Prompt,
42
+ PromptTemplate,
43
+ TextParam,
44
+ )
45
+ from .sdk.utils.costs import calculate_token_usage
46
+ from .sdk.utils.logging import get_module_logger
47
+ from .sdk.utils.preinit import PreInitObject
50
48
 
51
49
  config = PreInitObject("agenta.config", Config)
52
50
  DEFAULT_AGENTA_SINGLETON_INSTANCE = AgentaSingleton()
@@ -88,3 +86,35 @@ def init(
88
86
 
89
87
  tracing = DEFAULT_AGENTA_SINGLETON_INSTANCE.tracing # type: ignore
90
88
  tracer = get_tracer(tracing)
89
+
90
+
91
+ def get_trace_url(trace_id: Optional[str] = None) -> str:
92
+ """
93
+ Build a URL to view the current trace in the Agenta UI.
94
+
95
+ Automatically extracts the trace ID from the current tracing context.
96
+ Can also accept an explicit trace_id if needed.
97
+
98
+ Args:
99
+ trace_id: Optional trace ID (hex string format). If not provided,
100
+ it will be automatically extracted from the current trace context.
101
+
102
+ Returns:
103
+ The full URL to view the trace in the observability dashboard
104
+
105
+ Raises:
106
+ RuntimeError: If the SDK is not initialized, no active trace context exists,
107
+ or scope info cannot be fetched
108
+
109
+ Example:
110
+ >>> import agenta as ag
111
+ >>> ag.init(api_key="xxx")
112
+ >>>
113
+ >>> @ag.instrument()
114
+ >>> def my_function():
115
+ >>> # Get URL for the current trace
116
+ >>> url = ag.tracing.get_trace_url()
117
+ >>> print(url)
118
+ >>> return "result"
119
+ """
120
+ return DEFAULT_AGENTA_SINGLETON_INSTANCE.tracing.get_trace_url(trace_id)
agenta/sdk/agenta_init.py CHANGED
@@ -1,16 +1,15 @@
1
- import toml
2
- from os import getenv
3
- from typing import Optional, Callable, Any
4
1
  from importlib.metadata import version
2
+ from os import getenv
3
+ from typing import Any, Callable, Optional
5
4
 
6
- from agenta.sdk.utils.helpers import parse_url
7
- from agenta.sdk.utils.globals import set_global
8
- from agenta.sdk.utils.logging import get_module_logger
5
+ import requests
6
+ import toml
9
7
  from agenta.client.client import AgentaApi, AsyncAgentaApi
10
-
11
- from agenta.sdk.tracing import Tracing
12
8
  from agenta.sdk.contexts.routing import RoutingContext
13
-
9
+ from agenta.sdk.tracing import Tracing
10
+ from agenta.sdk.utils.globals import set_global
11
+ from agenta.sdk.utils.helpers import parse_url
12
+ from agenta.sdk.utils.logging import get_module_logger
14
13
 
15
14
  log = get_module_logger(__name__)
16
15
 
@@ -19,6 +18,7 @@ class AgentaSingleton:
19
18
  """Singleton class to save all the "global variables" for the sdk."""
20
19
 
21
20
  _instance = None
21
+ _initialized = False
22
22
  config = None
23
23
  tracing = None
24
24
 
@@ -26,6 +26,11 @@ class AgentaSingleton:
26
26
  async_api = None
27
27
 
28
28
  def __init__(self):
29
+ # Only initialize once
30
+ if AgentaSingleton._initialized:
31
+ return
32
+
33
+ AgentaSingleton._initialized = True
29
34
  self.host = None
30
35
  self.api_url = None
31
36
  self.api_key = None
@@ -33,6 +38,11 @@ class AgentaSingleton:
33
38
  self.scope_type = None
34
39
  self.scope_id = None
35
40
 
41
+ # Cached scope information for URL building
42
+ self.organization_id: Optional[str] = None
43
+ self.workspace_id: Optional[str] = None
44
+ self.project_id: Optional[str] = None
45
+
36
46
  def __new__(cls):
37
47
  if not cls._instance:
38
48
  cls._instance = super(AgentaSingleton, cls).__new__(cls)
@@ -70,6 +80,10 @@ class AgentaSingleton:
70
80
 
71
81
  """
72
82
 
83
+ # Idempotency check: if already initialized, skip re-initialization
84
+ if self.tracing and self.api and self.async_api:
85
+ return
86
+
73
87
  log.info("Agenta - SDK ver: %s", version("agenta"))
74
88
 
75
89
  config = {}
@@ -100,7 +114,7 @@ class AgentaSingleton:
100
114
 
101
115
  try:
102
116
  assert _api_url and isinstance(_api_url, str), (
103
- "API URL is required. Please provide a valid API URL or set AGENTA_API_URL environment variable."
117
+ "API URL is required. Please set AGENTA_API_URL environment variable or pass api_url parameter in ag.init()."
104
118
  )
105
119
  self.host = _host
106
120
  self.api_url = _api_url
@@ -118,6 +132,11 @@ class AgentaSingleton:
118
132
  or None # NO FALLBACK
119
133
  )
120
134
 
135
+ if self.api_key is None:
136
+ log.error(
137
+ "API key is required. Please set AGENTA_API_KEY environment variable or pass api_key parameter in ag.init()."
138
+ )
139
+
121
140
  log.info("Agenta - API URL: %s", self.api_url)
122
141
 
123
142
  self.scope_type = (
@@ -159,6 +178,65 @@ class AgentaSingleton:
159
178
  api_key=self.api_key,
160
179
  )
161
180
 
181
+ # Reset cached scope info on re-init
182
+ self.organization_id = None
183
+ self.workspace_id = None
184
+ self.project_id = None
185
+
186
+ def resolve_scopes(self) -> Optional[tuple[str, str, str]]:
187
+ """Fetch and cache workspace_id and project_id from the API."""
188
+ if (
189
+ self.organization_id is not None
190
+ and self.workspace_id is not None
191
+ and self.project_id is not None
192
+ ):
193
+ return
194
+
195
+ if self.api_url is None or self.api_key is None:
196
+ log.error("API URL or API key is not set. Please call ag.init() first.")
197
+ return
198
+
199
+ try:
200
+ response = requests.get(
201
+ f"{self.api_url}/projects/current",
202
+ headers={"Authorization": f"ApiKey {self.api_key}"},
203
+ timeout=10,
204
+ )
205
+ response.raise_for_status()
206
+
207
+ project_info = response.json()
208
+
209
+ if not project_info:
210
+ log.error(
211
+ "No project context found. Please ensure your API key is valid."
212
+ )
213
+
214
+ self.organization_id = project_info.get("organization_id")
215
+ self.workspace_id = project_info.get("workspace_id")
216
+ self.project_id = project_info.get("project_id")
217
+
218
+ if (
219
+ not self.organization_id
220
+ and not self.workspace_id
221
+ or not self.project_id
222
+ ):
223
+ log.error(
224
+ "Could not determine organization/workspace/project from API response."
225
+ )
226
+
227
+ except Exception as e:
228
+ log.error(f"Failed to fetch scope information: {e}")
229
+ return
230
+
231
+ if self.organization_id and self.workspace_id and self.project_id:
232
+ return (
233
+ self.organization_id,
234
+ self.workspace_id,
235
+ self.project_id,
236
+ )
237
+
238
+ return None
239
+
162
240
 
163
241
  class Config:
164
242
  def __init__(
agenta/sdk/assets.py CHANGED
@@ -28,6 +28,7 @@ supported_llm_models = {
28
28
  ],
29
29
  "gemini": [
30
30
  "gemini/gemini-3-pro-preview",
31
+ "gemini/gemini-3-flash-preview",
31
32
  "gemini/gemini-2.5-pro",
32
33
  "gemini/gemini-2.5-pro-preview-05-06",
33
34
  "gemini/gemini-2.5-flash",
@@ -64,21 +65,26 @@ supported_llm_models = {
64
65
  "mistral/mistral-large-latest",
65
66
  ],
66
67
  "openai": [
67
- "gpt-5",
68
+ "gpt-5.2-pro",
69
+ "gpt-5.2-chat-latest",
70
+ "gpt-5.2",
71
+ "gpt-5.1-chat-latest",
68
72
  "gpt-5.1",
69
- "gpt-5-mini",
73
+ "gpt-5-pro",
70
74
  "gpt-5-nano",
75
+ "gpt-5-mini",
76
+ "gpt-5",
77
+ "o4-mini",
71
78
  "gpt-4.5-preview",
72
- "gpt-3.5-turbo-1106",
73
- "gpt-3.5-turbo",
74
- "gpt-4",
75
- "gpt-4o",
79
+ "gpt-4.1-nano",
80
+ "gpt-4.1-mini",
81
+ "gpt-4.1",
76
82
  "gpt-4o-mini",
83
+ "gpt-4o",
77
84
  "gpt-4-1106-preview",
78
- "gpt-4.1",
79
- "gpt-4.1-mini",
80
- "gpt-4.1-nano",
81
- "o4-mini",
85
+ "gpt-4",
86
+ "gpt-3.5-turbo-1106",
87
+ "gpt-3.5-turbo",
82
88
  ],
83
89
  "openrouter": [
84
90
  "openrouter/qwen/qwen3-235b-a22b",
@@ -166,24 +166,27 @@ def litellm_handler():
166
166
  namespace="metrics.unit.costs",
167
167
  )
168
168
 
169
+ # Handle both dict and object attribute access for usage, and safely handle None
170
+ usage = getattr(response_obj, "usage", None)
171
+ if isinstance(usage, dict):
172
+ prompt_tokens = usage.get("prompt_tokens")
173
+ completion_tokens = usage.get("completion_tokens")
174
+ total_tokens = usage.get("total_tokens")
175
+ elif usage is not None:
176
+ prompt_tokens = getattr(usage, "prompt_tokens", None)
177
+ completion_tokens = getattr(usage, "completion_tokens", None)
178
+ total_tokens = getattr(usage, "total_tokens", None)
179
+ else:
180
+ prompt_tokens = completion_tokens = total_tokens = None
181
+
169
182
  span.set_attributes(
170
183
  attributes=(
171
184
  {
172
- "prompt": (
173
- float(response_obj.usage.prompt_tokens)
174
- if response_obj.usage.prompt_tokens
175
- else None
176
- ),
177
- "completion": (
178
- float(response_obj.usage.completion_tokens)
179
- if response_obj.usage.completion_tokens
180
- else None
181
- ),
182
- "total": (
183
- float(response_obj.usage.total_tokens)
184
- if response_obj.usage.total_tokens
185
- else None
186
- ),
185
+ "prompt": float(prompt_tokens) if prompt_tokens else None,
186
+ "completion": float(completion_tokens)
187
+ if completion_tokens
188
+ else None,
189
+ "total": float(total_tokens) if total_tokens else None,
187
190
  }
188
191
  ),
189
192
  namespace="metrics.unit.tokens",
@@ -300,24 +303,29 @@ def litellm_handler():
300
303
  namespace="metrics.unit.costs",
301
304
  )
302
305
 
306
+ # Handle both dict and object attribute access for usage
307
+ usage = getattr(response_obj, "usage", None)
308
+ if usage is None:
309
+ prompt_tokens = None
310
+ completion_tokens = None
311
+ total_tokens = None
312
+ elif isinstance(usage, dict):
313
+ prompt_tokens = usage.get("prompt_tokens")
314
+ completion_tokens = usage.get("completion_tokens")
315
+ total_tokens = usage.get("total_tokens")
316
+ else:
317
+ prompt_tokens = getattr(usage, "prompt_tokens", None)
318
+ completion_tokens = getattr(usage, "completion_tokens", None)
319
+ total_tokens = getattr(usage, "total_tokens", None)
320
+
303
321
  span.set_attributes(
304
322
  attributes=(
305
323
  {
306
- "prompt": (
307
- float(response_obj.usage.prompt_tokens)
308
- if response_obj.usage.prompt_tokens
309
- else None
310
- ),
311
- "completion": (
312
- float(response_obj.usage.completion_tokens)
313
- if response_obj.usage.completion_tokens
314
- else None
315
- ),
316
- "total": (
317
- float(response_obj.usage.total_tokens)
318
- if response_obj.usage.total_tokens
319
- else None
320
- ),
324
+ "prompt": float(prompt_tokens) if prompt_tokens else None,
325
+ "completion": float(completion_tokens)
326
+ if completion_tokens
327
+ else None,
328
+ "total": float(total_tokens) if total_tokens else None,
321
329
  }
322
330
  ),
323
331
  namespace="metrics.unit.tokens",
@@ -17,14 +17,12 @@ import agenta as ag
17
17
 
18
18
  log = get_module_logger(__name__)
19
19
 
20
- AGENTA_RUNTIME_PREFIX = getenv("AGENTA_RUNTIME_PREFIX", "")
21
-
22
20
 
23
21
  _CACHE_ENABLED = (
24
22
  getenv("AGENTA_SERVICE_MIDDLEWARE_CACHE_ENABLED", "true").lower() in TRUTHY
25
23
  )
26
24
 
27
- _ALWAYS_ALLOW_LIST = [f"{AGENTA_RUNTIME_PREFIX}/health"]
25
+ _ALWAYS_ALLOW_LIST = ["/health"]
28
26
 
29
27
  _cache = TTLLRUCache()
30
28
 
@@ -64,7 +62,7 @@ class AuthHTTPMiddleware(BaseHTTPMiddleware):
64
62
 
65
63
  async def dispatch(self, request: Request, call_next: Callable):
66
64
  try:
67
- if request.url.path in _ALWAYS_ALLOW_LIST:
65
+ if _strip_service_prefix(request.url.path) in _ALWAYS_ALLOW_LIST:
68
66
  request.state.auth = {}
69
67
 
70
68
  else:
@@ -253,3 +251,20 @@ class AuthHTTPMiddleware(BaseHTTPMiddleware):
253
251
  status_code=500,
254
252
  content=f"Could not verify credentials: unexpected error - {str(exc)}. Please try again later or contact support if the issue persists.",
255
253
  ) from exc
254
+
255
+
256
+ def _strip_service_prefix(path: str) -> str:
257
+ if not path.startswith("/services/"):
258
+ return path
259
+
260
+ parts = path.split("/", 3)
261
+ if len(parts) < 4:
262
+ return "/"
263
+
264
+ service_name = parts[2]
265
+ remainder = parts[3]
266
+
267
+ if not service_name or not remainder or remainder.startswith("/"):
268
+ return path
269
+
270
+ return f"/{remainder}"
@@ -3,9 +3,6 @@ from typing import Callable
3
3
  from starlette.middleware.base import BaseHTTPMiddleware
4
4
  from fastapi import Request, FastAPI
5
5
 
6
- from opentelemetry.baggage.propagation import W3CBaggagePropagator
7
- from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
8
-
9
6
  from agenta.sdk.utils.exceptions import suppress
10
7
  from agenta.sdk.tracing.propagation import extract
11
8
 
@@ -21,11 +21,9 @@ import agenta as ag
21
21
  log = get_module_logger(__name__)
22
22
 
23
23
 
24
- AGENTA_RUNTIME_PREFIX = getenv("AGENTA_RUNTIME_PREFIX", "")
25
-
26
24
  _ALWAYS_ALLOW_LIST = [
27
- f"{AGENTA_RUNTIME_PREFIX}/health",
28
- f"{AGENTA_RUNTIME_PREFIX}/openapi.json",
25
+ "/health",
26
+ "/openapi.json",
29
27
  ]
30
28
 
31
29
  _PROVIDER_KINDS = [
@@ -116,7 +114,7 @@ class VaultMiddleware(BaseHTTPMiddleware):
116
114
  allow_secrets = True
117
115
 
118
116
  try:
119
- if not request.url.path in _ALWAYS_ALLOW_LIST:
117
+ if _strip_service_prefix(request.url.path) not in _ALWAYS_ALLOW_LIST:
120
118
  await self._allow_local_secrets(credentials)
121
119
 
122
120
  for provider_kind in _PROVIDER_KINDS:
@@ -331,3 +329,20 @@ class VaultMiddleware(BaseHTTPMiddleware):
331
329
  status_code=500,
332
330
  content=f"Could not verify credentials: unexpected error - {str(exc)}. Please try again later or contact support if the issue persists.",
333
331
  ) from exc
332
+
333
+
334
+ def _strip_service_prefix(path: str) -> str:
335
+ if not path.startswith("/services/"):
336
+ return path
337
+
338
+ parts = path.split("/", 3)
339
+ if len(parts) < 4:
340
+ return "/"
341
+
342
+ service_name = parts[2]
343
+ remainder = parts[3]
344
+
345
+ if not service_name or not remainder or remainder.startswith("/"):
346
+ return path
347
+
348
+ return f"/{remainder}"
@@ -127,7 +127,7 @@ class VaultMiddleware:
127
127
  request: WorkflowServiceRequest,
128
128
  call_next: Callable[[WorkflowServiceRequest], Any],
129
129
  ):
130
- api_url = f"{ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.host}/api"
130
+ api_url = ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.api_url
131
131
 
132
132
  with suppress():
133
133
  ctx = RunningContext.get()
@@ -88,7 +88,7 @@ class Slug(BaseModel):
88
88
  def check_url_safety(cls, v):
89
89
  if v is not None:
90
90
  if not match(r"^[a-zA-Z0-9_-]+$", v):
91
- raise ValueError("slug must be URL-safe.")
91
+ raise ValueError("'slug' must be URL-safe.")
92
92
  return v
93
93
 
94
94
 
@@ -122,6 +122,7 @@ class OTLPExporter(OTLPSpanExporter):
122
122
  # "[SPAN] [EXPORT]",
123
123
  # trace_id=UUID(int=trace_id).hex,
124
124
  # span_id=UUID(int=span_id).hex[-16:],
125
+ # span_attributes=_span.attributes,
125
126
  # )
126
127
 
127
128
  serialized_spans.append(super().export(_spans))
@@ -1,7 +1,5 @@
1
1
  from typing import Optional, Dict, List
2
2
  from threading import Lock
3
- from json import dumps
4
- from uuid import UUID
5
3
 
6
4
  from opentelemetry.baggage import get_all as get_baggage
7
5
  from opentelemetry.context import Context
@@ -14,8 +12,7 @@ from opentelemetry.sdk.trace.export import (
14
12
  from opentelemetry.trace import SpanContext
15
13
 
16
14
  from agenta.sdk.utils.logging import get_module_logger
17
- from agenta.sdk.tracing.conventions import Reference
18
-
15
+ from agenta.sdk.models.tracing import BaseModel
19
16
  from agenta.sdk.contexts.tracing import TracingContext
20
17
 
21
18
  log = get_module_logger(__name__)
@@ -65,15 +62,36 @@ class TraceProcessor(SpanProcessor):
65
62
  # )
66
63
 
67
64
  for key in self.references.keys():
68
- span.set_attribute(f"ag.refs.{key}", self.references[key])
65
+ ref = self.references[key]
66
+ if isinstance(ref, BaseModel):
67
+ try:
68
+ ref = ref.model_dump(mode="json", exclude_none=True)
69
+ except Exception: # pylint: disable=bare-except
70
+ pass
71
+ if isinstance(ref, dict):
72
+ for field, value in ref.items():
73
+ span.set_attribute(f"ag.refs.{key}.{field}", str(value))
69
74
 
70
75
  baggage = get_baggage(parent_context)
71
76
 
72
77
  for key in baggage.keys():
73
- if key.startswith("ag.refs."):
74
- _key = key.replace("ag.refs.", "")
75
- if _key in [_.value for _ in Reference.__members__.values()]:
76
- span.set_attribute(key, baggage[key])
78
+ if key.startswith("ag."):
79
+ value = baggage[key]
80
+
81
+ if key.startswith("ag.refs."):
82
+ ref = value
83
+ if isinstance(value, BaseModel):
84
+ try:
85
+ ref = value.model_dump(mode="json", exclude_none=True) # type: ignore
86
+ except Exception: # pylint: disable=bare-except
87
+ pass
88
+ if isinstance(ref, dict):
89
+ for field, val in ref.items():
90
+ span.set_attribute(f"{key}.{field}", str(val))
91
+ else:
92
+ # Not a reference - only set if it's a valid attribute type
93
+ if isinstance(value, (str, bool, int, float, bytes)):
94
+ span.set_attribute(key, value)
77
95
 
78
96
  context = TracingContext.get()
79
97
 
@@ -105,10 +123,11 @@ class TraceProcessor(SpanProcessor):
105
123
  if not self.inline:
106
124
  if context.links:
107
125
  for key, link in context.links.items():
108
- try:
109
- link = link.model_dump(mode="json", exclude_none=True)
110
- except: # pylint: disable=bare-except
111
- pass
126
+ if isinstance(link, BaseModel):
127
+ try:
128
+ link = link.model_dump(mode="json", exclude_none=True)
129
+ except Exception:
130
+ pass
112
131
  if not isinstance(link, dict):
113
132
  continue
114
133
  if not link.get("trace_id") or not link.get("span_id"):
@@ -127,30 +146,14 @@ class TraceProcessor(SpanProcessor):
127
146
 
128
147
  if context.references:
129
148
  for key, ref in context.references.items():
130
- try:
131
- ref = ref.model_dump(mode="json", exclude_none=True)
132
- except: # pylint: disable=bare-except
133
- pass
134
- if not isinstance(ref, dict):
135
- continue
136
- if not ref.get("id") and not ref.get("slug") and not ref.get("version"):
137
- continue
138
-
139
- if ref.get("id"):
140
- span.set_attribute(
141
- f"ag.refs.{key}.id",
142
- str(ref.get("id")),
143
- )
144
- if ref.get("slug"):
145
- span.set_attribute(
146
- f"ag.refs.{key}.slug",
147
- str(ref.get("slug")),
148
- )
149
- if ref.get("version"):
150
- span.set_attribute(
151
- f"ag.refs.{key}.version",
152
- str(ref.get("version")),
153
- )
149
+ if isinstance(ref, BaseModel):
150
+ try:
151
+ ref = ref.model_dump(mode="json", exclude_none=True)
152
+ except Exception:
153
+ pass
154
+ if isinstance(ref, dict):
155
+ for field, value in ref.items():
156
+ span.set_attribute(f"ag.refs.{key}.{field}", str(value))
154
157
 
155
158
  trace_id = span.context.trace_id
156
159
  span_id = span.context.span_id
@@ -31,6 +31,7 @@ from agenta.sdk.tracing.conventions import Reference, is_valid_attribute_key
31
31
  from agenta.sdk.tracing.propagation import extract, inject
32
32
  from agenta.sdk.utils.cache import TTLLRUCache
33
33
 
34
+ import agenta as ag
34
35
 
35
36
  log = get_module_logger(__name__)
36
37
 
@@ -215,6 +216,42 @@ class Tracing(metaclass=Singleton):
215
216
  namespace="metrics",
216
217
  )
217
218
 
219
+ def store_session(
220
+ self,
221
+ session_id: Optional[str] = None,
222
+ span: Optional[Span] = None,
223
+ ):
224
+ """Set session attributes on the current span.
225
+
226
+ Args:
227
+ session_id: Unique identifier for the session
228
+ span: Optional span to set attributes on (defaults to current span)
229
+ """
230
+ with suppress():
231
+ if span is None:
232
+ span = self.get_current_span()
233
+
234
+ if session_id:
235
+ span.set_attribute("id", session_id, namespace="session")
236
+
237
+ def store_user(
238
+ self,
239
+ user_id: Optional[str] = None,
240
+ span: Optional[Span] = None,
241
+ ):
242
+ """Set user attributes on the current span.
243
+
244
+ Args:
245
+ user_id: Unique identifier for the user
246
+ span: Optional span to set attributes on (defaults to current span)
247
+ """
248
+ with suppress():
249
+ if span is None:
250
+ span = self.get_current_span()
251
+
252
+ if user_id:
253
+ span.set_attribute("id", user_id, namespace="user")
254
+
218
255
  def is_inline_trace_ready(
219
256
  self,
220
257
  trace_id: Optional[int] = None,
@@ -314,6 +351,58 @@ class Tracing(metaclass=Singleton):
314
351
 
315
352
  return None
316
353
 
354
+ def get_trace_url(
355
+ self,
356
+ trace_id: Optional[str] = None,
357
+ ) -> str:
358
+ """
359
+ Build a URL to view a trace in the Agenta UI.
360
+
361
+ Automatically extracts the trace ID from the current tracing context
362
+ if not explicitly provided.
363
+
364
+ Args:
365
+ trace_id: Optional trace ID (hex string format). If not provided,
366
+ it will be automatically extracted from the current trace context.
367
+
368
+ Returns:
369
+ The full URL to view the trace in the observability dashboard
370
+
371
+ Raises:
372
+ RuntimeError: If the SDK is not initialized, no active trace context exists,
373
+ or scope info cannot be fetched
374
+ """
375
+ if trace_id is None:
376
+ span_ctx = self.get_span_context()
377
+ if span_ctx is None or not span_ctx.is_valid:
378
+ raise RuntimeError(
379
+ "No active trace context found. "
380
+ "Make sure you call this within an instrumented function or span."
381
+ )
382
+
383
+ trace_id = f"{span_ctx.trace_id:032x}"
384
+
385
+ if not ag or not ag.DEFAULT_AGENTA_SINGLETON_INSTANCE:
386
+ raise RuntimeError(
387
+ "Agenta SDK is not initialized. Please call ag.init() first."
388
+ )
389
+
390
+ api_url = ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.api_url
391
+ web_url = api_url.replace("/api", "") if api_url else None
392
+
393
+ (organization_id, workspace_id, project_id) = (
394
+ ag.DEFAULT_AGENTA_SINGLETON_INSTANCE.resolve_scopes()
395
+ )
396
+
397
+ if not web_url or not workspace_id or not project_id:
398
+ raise RuntimeError(
399
+ "Could not determine workspace/project context. Please call ag.init() first."
400
+ )
401
+
402
+ return (
403
+ f"{web_url}/w/{workspace_id}/p/{project_id}/observability?trace={trace_id}"
404
+ )
405
+
317
406
 
318
407
  def get_tracer(
319
408
  tracing: Tracing,
@@ -88,7 +88,6 @@ class DaytonaRunner(CodeRunner):
88
88
  target=target,
89
89
  )
90
90
  self.daytona = Daytona(config)
91
- # log.debug("Daytona client initialized")
92
91
 
93
92
  except Exception as e:
94
93
  raise RuntimeError(f"Failed to initialize Daytona client: {e}")
@@ -107,8 +106,6 @@ class DaytonaRunner(CodeRunner):
107
106
  "Set it to the Daytona sandbox ID or snapshot name you want to use."
108
107
  )
109
108
 
110
- # log.debug(f"Creating sandbox from snapshot: {snapshot_id}")
111
-
112
109
  from daytona import CreateSandboxFromSnapshotParams
113
110
 
114
111
  sandbox = self.daytona.create(
@@ -118,9 +115,6 @@ class DaytonaRunner(CodeRunner):
118
115
  )
119
116
  )
120
117
 
121
- # log.debug(
122
- # f"Sandbox created: {sandbox.id if hasattr(sandbox, 'id') else sandbox}"
123
- # )
124
118
  return sandbox
125
119
 
126
120
  except Exception as e:
@@ -1,7 +1,17 @@
1
1
  import os
2
+ from typing import TYPE_CHECKING
3
+
2
4
  from agenta.sdk.workflows.runners.base import CodeRunner
3
5
  from agenta.sdk.workflows.runners.local import LocalRunner
4
- from agenta.sdk.workflows.runners.daytona import DaytonaRunner
6
+
7
+ if TYPE_CHECKING:
8
+ from agenta.sdk.workflows.runners.daytona import DaytonaRunner
9
+
10
+
11
+ def _get_daytona_runner() -> "DaytonaRunner":
12
+ from agenta.sdk.workflows.runners.daytona import DaytonaRunner
13
+
14
+ return DaytonaRunner()
5
15
 
6
16
 
7
17
  def get_runner() -> CodeRunner:
@@ -21,7 +31,14 @@ def get_runner() -> CodeRunner:
21
31
  runner_type = os.getenv("AGENTA_SERVICES_SANDBOX_RUNNER", "local").lower()
22
32
 
23
33
  if runner_type == "daytona":
24
- return DaytonaRunner()
34
+ try:
35
+ return _get_daytona_runner()
36
+ except ImportError as exc:
37
+ raise ValueError(
38
+ "Daytona runner requires the 'daytona' package. "
39
+ "Install optional dependencies or set "
40
+ "AGENTA_SERVICES_SANDBOX_RUNNER=local."
41
+ ) from exc
25
42
  elif runner_type == "local":
26
43
  return LocalRunner()
27
44
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenta
3
- Version: 0.68.0
3
+ Version: 0.70.1
4
4
  Summary: The SDK for agenta is an open-source LLMOps platform.
5
5
  Keywords: LLMOps,LLM,evaluation,prompt engineering
6
6
  Author: Mahmoud Mabrouk
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Topic :: Software Development :: Libraries
18
18
  Requires-Dist: daytona (>=0.121.0,<0.122.0)
19
- Requires-Dist: fastapi (>=0.122,<0.123)
19
+ Requires-Dist: fastapi (>=0.125)
20
20
  Requires-Dist: httpx (>=0.28,<0.29)
21
21
  Requires-Dist: importlib-metadata (>=8,<9)
22
22
  Requires-Dist: jinja2 (>=3,<4)
@@ -26,6 +26,7 @@ Requires-Dist: opentelemetry-api (>=1,<2)
26
26
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1,<2)
27
27
  Requires-Dist: opentelemetry-instrumentation (>=0.59b0,<0.60)
28
28
  Requires-Dist: opentelemetry-sdk (>=1,<2)
29
+ Requires-Dist: orjson (>=3,<4)
29
30
  Requires-Dist: pydantic (>=2,<3)
30
31
  Requires-Dist: python-dotenv (>=1,<2)
31
32
  Requires-Dist: python-jsonpath (>=2,<3)
@@ -1,4 +1,4 @@
1
- agenta/__init__.py,sha256=ltMd-tuCDxglMfUT51-XlvSqwoE9QsoYi5JSQ4WKO9Q,2597
1
+ agenta/__init__.py,sha256=OtejSy4-gUHKpLOLgQ_yHYROKaJ57u7DHuBMDTDrmJM,3677
2
2
  agenta/client/Readme.md,sha256=ZQJ_nBVYfVJizfMaD_3WPdXPBfi8okrV7i8LAUAfdu0,7604
3
3
  agenta/client/__init__.py,sha256=dXPiqGSFUGv0XIZtKXcmoMea1oRFOiTi5247VVk56Ig,10832
4
4
  agenta/client/backend/__init__.py,sha256=KmQImSlBeXCum1aWUOKN0415fAs0-CGoWgWcKhr0wmI,10559
@@ -306,8 +306,8 @@ agenta/client/types.py,sha256=wBGDVktTL2EblEKW23Y-VrFp7V_JHLPMHltt2jEkF0Q,129
306
306
  agenta/config.py,sha256=0VrTqduB4g8Mt_Ll7ffFcEjKF5qjTUIxmUtTPW2ygWw,653
307
307
  agenta/config.toml,sha256=sIORbhnyct2R9lJrquxhNL4pHul3O0R7iaipCoja5MY,193
308
308
  agenta/sdk/__init__.py,sha256=7QUpZ409HcLB22A80qaZydzhs6afPnCvG0Tfq6PE4fk,3011
309
- agenta/sdk/agenta_init.py,sha256=Vm14_nzyObAmwQFQvAANLsOjqy5AwYIFa6MrdnJ0aqY,7286
310
- agenta/sdk/assets.py,sha256=51uSUp-qlFLB-nLSrDDTDXOQhM-2yGIuODgALYt1i9Y,8699
309
+ agenta/sdk/agenta_init.py,sha256=KiZpk0cfzvKNUtO5S0I_FQghcy8AKRkSxDtVjlZaNkw,9950
310
+ agenta/sdk/assets.py,sha256=c7fbv-ZJdoY8xcHwTJfgT9UeSSamfZuhimfPNYyQZ_k,8865
311
311
  agenta/sdk/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
312
312
  agenta/sdk/context/running.py,sha256=3gEuUdQrJwcuN93MlXFZ6aHXNxUW6dUk_EudgaxOkCU,907
313
313
  agenta/sdk/context/serving.py,sha256=jGRd6v4wWNNoDFM7PJ4ct0MXcMDAVY6puPuseSeIL60,960
@@ -343,7 +343,7 @@ agenta/sdk/evaluations/results.py,sha256=3pe2c0oI1Wc1wFCKFeQnbe_iwtNn9U0MO_c4U3H
343
343
  agenta/sdk/evaluations/runs.py,sha256=8euS2Zfzcw9a7SDOnhK4DymuYNIjIXQ1iWlRuuf0q78,3398
344
344
  agenta/sdk/evaluations/scenarios.py,sha256=XlsVa_M8FmnSvVniF_FEZUhZDLYIotx4V0SRmPEzWS8,1024
345
345
  agenta/sdk/litellm/__init__.py,sha256=Bpz1gfHQc0MN1yolWcjifLWznv6GjHggvRGQSpxpihM,37
346
- agenta/sdk/litellm/litellm.py,sha256=E7omr9kz0yn8CUK5O0g0QUlDA4bD5fllYtHK9RL2bXE,10646
346
+ agenta/sdk/litellm/litellm.py,sha256=5UJ9PcxwocgskHjrwKDW1XCz5y4OxUHq2dL2EwTXkq0,11288
347
347
  agenta/sdk/litellm/mockllm.py,sha256=R32eoGvXokxNk0WihgP8rIdd5mTwCPXF--U5NDisnJE,2255
348
348
  agenta/sdk/litellm/mocks/__init__.py,sha256=dqTLo5sFH6IkjyunWPuBPpahfZ-cbUHOkRloTWTq7BY,5262
349
349
  agenta/sdk/managers/__init__.py,sha256=SN-LRwG0pRRDV3u2Q4JiiSTigN3-mYpzGNM35RzT4mc,238
@@ -359,13 +359,13 @@ agenta/sdk/managers/testsets.py,sha256=m8U6LA-ZuS_8sm_sIFR2rUXFScNncMZ52Ps1hSPt-
359
359
  agenta/sdk/managers/variant.py,sha256=A5ga3mq3b0weUTXa9HO72MGaspthGcu1uK9K5OnP738,4172
360
360
  agenta/sdk/managers/vault.py,sha256=wqDVFPuUi-Zida9zVhuHeW6y63K2kez1-4Kk5U0dY48,334
361
361
  agenta/sdk/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
362
- agenta/sdk/middleware/auth.py,sha256=z-FEr9UbcxM0snf_2KlW1O7GvQFMf2ZET-wxSs4CDkI,10233
362
+ agenta/sdk/middleware/auth.py,sha256=UUwqEfsrNt49U1hs-Qfjsjkl5t7-I25VWhS7dsrHqJs,10528
363
363
  agenta/sdk/middleware/config.py,sha256=0eKRkARI0fkXwmq3NNL4x7iOue0_xN3RQCgoHk8guTc,8723
364
364
  agenta/sdk/middleware/cors.py,sha256=q3r7lGkrIdMcT_vuhsburMcjG7pyl7w0ycxrIrGJ2e8,921
365
365
  agenta/sdk/middleware/inline.py,sha256=ee8E4XBGcRSrHTvblqX1yRXuTN_sxLm7lY1jnywrBG8,901
366
366
  agenta/sdk/middleware/mock.py,sha256=bCUN9iJBxePyN9MBwBpJs-_iCNkUQeUjIIu3WElS1oQ,759
367
- agenta/sdk/middleware/otel.py,sha256=lHzhGUv4fq2RPuTPH2keJ16v-_cBUrLrTqWzHUmEVdI,1041
368
- agenta/sdk/middleware/vault.py,sha256=0fMsNY0HOxm4tmkgchxGe6EfaOMdvS-gzs3E7zoAhQI,12200
367
+ agenta/sdk/middleware/otel.py,sha256=M0cIFdRXVx17g3Nh2bvPjjVBBjoKwyI0MrGtJ4aGjjA,886
368
+ agenta/sdk/middleware/vault.py,sha256=xPrR8XUAMG7pgJ4SKMJxAlrfQGgeeIHFoJ-gYfqVWj0,12471
369
369
  agenta/sdk/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
370
370
  agenta/sdk/middlewares/routing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
371
371
  agenta/sdk/middlewares/routing/auth.py,sha256=WmZLxSu4lj9ltI-SpF31MAfAp7uJh3-E9T-U2L4Y58g,10476
@@ -374,12 +374,12 @@ agenta/sdk/middlewares/routing/otel.py,sha256=w-J7PpoBSyWYAFeoqAHxywjMuYMaW_eJ-t
374
374
  agenta/sdk/middlewares/running/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
375
375
  agenta/sdk/middlewares/running/normalizer.py,sha256=8D7iJkYDbqWiQEGISZOInZCyEjqyOlXETTq3dvDWFac,11384
376
376
  agenta/sdk/middlewares/running/resolver.py,sha256=I4nX7jpsq7oKSwQnlsoLm9kh94G2qxBdablJob1l_fs,5013
377
- agenta/sdk/middlewares/running/vault.py,sha256=DqeWyViDDRJycXtRKEO1S7ihlBfCnPFtXSfj_6trW98,3845
377
+ agenta/sdk/middlewares/running/vault.py,sha256=C4bKrQOdSc1L16peAm9adOw-hOlEnbN4CQSLtp_C4L4,3839
378
378
  agenta/sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
379
379
  agenta/sdk/models/blobs.py,sha256=g8zV6V3UcuskEV6tK_tvt8GDWqx1okfE3Oy4n59mgU0,576
380
380
  agenta/sdk/models/evaluations.py,sha256=twHFZQlAT2xYSqefJdpwtDLSu5GpsDmswV_o04ACyFs,3003
381
381
  agenta/sdk/models/git.py,sha256=ol5H3lu6s3Lx2l7K3glbCqcvAnPXsDsYa-OsSR0CupM,2415
382
- agenta/sdk/models/shared.py,sha256=ynHFXsOgkpvzHJuBcmPp2lMj7ia6x0e9LvNAiKVpPSo,4020
382
+ agenta/sdk/models/shared.py,sha256=zZz0dqSjOhtfGdXq7_zMe35AuUdb2moIoyZafQxUhyM,4022
383
383
  agenta/sdk/models/testsets.py,sha256=cHCZEWqnDMGqKKH9yVuM5FroG10YFKJ4nF5xZniE-Ds,3415
384
384
  agenta/sdk/models/tracing.py,sha256=adxOguxuU_uFRL1sGWPr31xCHs9qnfkD92xYcgHEDdA,5079
385
385
  agenta/sdk/models/workflows.py,sha256=VhPPK4V15pkrbI3otA9RxmoXvTeVd_KHVmYK86iw_MU,18997
@@ -387,12 +387,12 @@ agenta/sdk/router.py,sha256=mOguvtOwl2wmyAgOuWTsf98pQwpNiUILKIo67W_hR3A,119
387
387
  agenta/sdk/tracing/__init__.py,sha256=rQNe5-zT5Kt7_CDhq-lnUIi1EYTBVzVf_MbfcIxVD98,41
388
388
  agenta/sdk/tracing/attributes.py,sha256=Brqle9hGV3DaEJjeYCBq7MDlbvHMAIwmkUj2Lni8dF4,5563
389
389
  agenta/sdk/tracing/conventions.py,sha256=JBtznBXZ3aRkGKkLl7cPwdMNh3w1G-H2Ta2YrAxbr38,950
390
- agenta/sdk/tracing/exporters.py,sha256=NYqw80liPrs91ywkkQC13530pPmpUZguPgTEDO0JKFs,5118
390
+ agenta/sdk/tracing/exporters.py,sha256=6KEI3ESEJaMqktnq0DyTljWRXSVNBv6qcHezfVI9apA,5178
391
391
  agenta/sdk/tracing/inline.py,sha256=UKt10JGKdS6gVDIpExng3UC8vegAcuA2KxlzyvSdUZ0,31886
392
- agenta/sdk/tracing/processors.py,sha256=A7rsaicpFq9xZgyhU3hV5ZQoz6X33gB81G9IhB-x3Xg,8597
392
+ agenta/sdk/tracing/processors.py,sha256=AXCVPLgvUXRHuJwaHvG6K0TkP3pUnSeVk3en6wu3n6s,9019
393
393
  agenta/sdk/tracing/propagation.py,sha256=Zu_z5In8eOhy0tkYzQOI09T4OwdjGMP74nhzvElvyFE,2593
394
394
  agenta/sdk/tracing/spans.py,sha256=r-R68d12BjvilHgbqN-1xp26qxdVRzxRcFUO-IB_u94,3780
395
- agenta/sdk/tracing/tracing.py,sha256=mogsWlTwz-pYvzpst4xb4kjuGRtywzrU9GO9T9stvZw,9236
395
+ agenta/sdk/tracing/tracing.py,sha256=dP2LShcNBtWqGQKQBAGA52dDqNN15IcKcvXz_EwHigI,12200
396
396
  agenta/sdk/types.py,sha256=41yIQagl5L_7WFInjiACHwuNfCQqDrrDOusD17kJGWs,28469
397
397
  agenta/sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
398
398
  agenta/sdk/utils/cache.py,sha256=Er1Hvu1QVLGl99HkUHZ2lKBg3f6PnpkD1uZRvK9r3u4,1429
@@ -416,11 +416,11 @@ agenta/sdk/workflows/handlers.py,sha256=z_DtfgiejsToO4kqXvXBnO36hd0rk97Y2J2hYlIf
416
416
  agenta/sdk/workflows/interfaces.py,sha256=I5Bfil0awdL1TAb_vHqW5n5BHxSBOTuDMhOi4RnUt8A,36315
417
417
  agenta/sdk/workflows/runners/__init__.py,sha256=HoYaKf9G03WEUbY7B1uX4O_6xE5dfliNCG1nEuWp1ks,87
418
418
  agenta/sdk/workflows/runners/base.py,sha256=WgX0OgbLL5PHeGqLNAvrV7NC3FHDWVfU7v9EBj8MIW0,857
419
- agenta/sdk/workflows/runners/daytona.py,sha256=g09014ocerh7X-sLOJ01i5ko6YysdXCPfnrOfn9MWQQ,9791
419
+ agenta/sdk/workflows/runners/daytona.py,sha256=PqpbXh3HD_SZoS5lMYBW2lE7i0pepTzTDfHir5dNhv0,9531
420
420
  agenta/sdk/workflows/runners/local.py,sha256=SJ1msO35mQ4XzlqZi9fE25QJu-PDnYru8a66pMo-5vs,3636
421
- agenta/sdk/workflows/runners/registry.py,sha256=bHM7hTiFawdOM30RwCHeAwFN8C0zGF0at1zsI-57tlw,1067
421
+ agenta/sdk/workflows/runners/registry.py,sha256=QKU_6IXMcbdq_kxoQlOhjVBJ9nBq07QqHIBxFqSI7Uk,1556
422
422
  agenta/sdk/workflows/sandbox.py,sha256=O1Opeg4hc9jygAzyF5cCsStmMjYgrahA_aF0JdGbBO0,1734
423
423
  agenta/sdk/workflows/utils.py,sha256=UDG5or8qqiSCpqi0Fphjxkkhu4MdbiCkHn_yIQcTd0c,11664
424
- agenta-0.68.0.dist-info/METADATA,sha256=B8evMuWFY51eFJX03VPfmeyJ-ns-4wFnBSxVtcmWj1A,31572
425
- agenta-0.68.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
426
- agenta-0.68.0.dist-info/RECORD,,
424
+ agenta-0.70.1.dist-info/METADATA,sha256=-w_Gfj3Sz_2mDtULdrTUoviZQoL2botY1pg-pYjrk9c,31596
425
+ agenta-0.70.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
426
+ agenta-0.70.1.dist-info/RECORD,,