agenta 0.65.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)
@@ -13,6 +13,7 @@ class TestsetOutputResponse(UniversalBaseModel):
13
13
  name: str
14
14
  created_at: str
15
15
  updated_at: str
16
+ columns: typing.List[str]
16
17
 
17
18
  if IS_PYDANTIC_V2:
18
19
  model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
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,7 +80,11 @@ class AgentaSingleton:
70
80
 
71
81
  """
72
82
 
73
- log.info("Agenta - SDK ver: %s", version("agenta"))
83
+ # Idempotency check: if already initialized, skip re-initialization
84
+ if self.tracing and self.api and self.async_api:
85
+ return
86
+
87
+ log.info("Agenta - SDK ver: %s", version("agenta"))
74
88
 
75
89
  config = {}
76
90
  if config_fname:
@@ -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,7 +132,12 @@ class AgentaSingleton:
118
132
  or None # NO FALLBACK
119
133
  )
120
134
 
121
- log.info("Agenta - API URL: %s", self.api_url)
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
+
140
+ log.info("Agenta - API URL: %s", self.api_url)
122
141
 
123
142
  self.scope_type = (
124
143
  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",
@@ -114,7 +114,7 @@ class Tracing(metaclass=Singleton):
114
114
 
115
115
  # TRACE PROCESSORS -- OTLP
116
116
  try:
117
- log.info("Agenta - OLTP URL: %s", self.otlp_url)
117
+ log.info("Agenta - OTLP URL: %s", self.otlp_url)
118
118
 
119
119
  _otlp = TraceProcessor(
120
120
  OTLPExporter(
@@ -127,7 +127,7 @@ class Tracing(metaclass=Singleton):
127
127
 
128
128
  self.tracer_provider.add_span_processor(_otlp)
129
129
  except: # pylint: disable=bare-except
130
- log.warning("Agenta - OLTP unreachable, skipping exports.")
130
+ log.warning("Agenta - OTLP unreachable, skipping exports.")
131
131
 
132
132
  # GLOBAL TRACER PROVIDER -- INSTRUMENTATION LIBRARIES
133
133
  set_tracer_provider(self.tracer_provider)
@@ -13,15 +13,15 @@ async def arefresh(
13
13
  # timestamp: Optional[str] = None,
14
14
  # interval: Optional[float] = None,
15
15
  ) -> EvaluationMetrics:
16
- payload = dict(
16
+ metrics = dict(
17
17
  run_id=str(run_id),
18
18
  scenario_id=str(scenario_id) if scenario_id else None,
19
19
  )
20
20
 
21
21
  response = authed_api()(
22
22
  method="POST",
23
- endpoint=f"/preview/evaluations/metrics/refresh",
24
- params=payload,
23
+ endpoint="/preview/evaluations/metrics/refresh",
24
+ json=dict(metrics=metrics),
25
25
  )
26
26
 
27
27
  try:
@@ -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()
@@ -53,9 +53,17 @@ class EvaluationStatus(str, Enum):
53
53
 
54
54
 
55
55
  class EvaluationRunFlags(BaseModel):
56
- is_closed: Optional[bool] = None # Indicates if the run is immutable
57
- is_live: Optional[bool] = None # Indicates if the run is updated periodically
58
- is_active: Optional[bool] = None # Indicates if the run is currently active
56
+ is_live: bool = False # Indicates if the run has live queries
57
+ is_active: bool = False # Indicates if the run is currently active
58
+ is_closed: bool = False # Indicates if the run is modifiable
59
+ #
60
+ has_queries: bool = False # Indicates if the run has queries
61
+ has_testsets: bool = False # Indicates if the run has testsets
62
+ has_evaluators: bool = False # Indicates if the run has evaluators
63
+ #
64
+ has_custom: bool = False # Indicates if the run has custom evaluators
65
+ has_human: bool = False # Indicates if the run has human evaluators
66
+ has_auto: bool = False # Indicates if the run has auto evaluators
59
67
 
60
68
 
61
69
  class SimpleEvaluationFlags(EvaluationRunFlags):
@@ -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))