agentx-python 0.4.10__py3-none-any.whl → 0.4.12__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.
agentx/__init__.py CHANGED
@@ -2,6 +2,15 @@ import logging
2
2
 
3
3
  from agentx.agentx import AgentX
4
4
  from agentx.version import VERSION
5
+ from agentx.exceptions import (
6
+ AgentXError,
7
+ AgentXAuthError,
8
+ AgentXAPIError,
9
+ DatasetNotFound,
10
+ CINotEnabled,
11
+ CIRunExpired,
12
+ CIGateFailure,
13
+ )
5
14
 
6
15
  logging.basicConfig(
7
16
  level=logging.INFO,
@@ -9,5 +18,14 @@ logging.basicConfig(
9
18
  datefmt="%Y-%m-%d %H:%M:%S %Z",
10
19
  )
11
20
 
12
- __all__ = ["AgentX"]
21
+ __all__ = [
22
+ "AgentX",
23
+ "AgentXError",
24
+ "AgentXAuthError",
25
+ "AgentXAPIError",
26
+ "DatasetNotFound",
27
+ "CINotEnabled",
28
+ "CIRunExpired",
29
+ "CIGateFailure",
30
+ ]
13
31
  __version__ = VERSION
agentx/agentx.py CHANGED
@@ -22,6 +22,8 @@ class AgentX:
22
22
 
23
23
  from agentx.evaluations.client import EvaluationsClient
24
24
  from agentx.evaluations.runner import EvaluationsRunner
25
+ from agentx.tracing.ingest_client import IngestClient
26
+ from agentx.tracing.tracer import Tracer
25
27
  from agentx.version import VERSION
26
28
 
27
29
  _eval_client = EvaluationsClient(
@@ -31,6 +33,13 @@ class AgentX:
31
33
  )
32
34
  self.evaluations = EvaluationsRunner(_eval_client)
33
35
 
36
+ _ingest_client = IngestClient(
37
+ api_key=self.api_key,
38
+ sdk_version=VERSION,
39
+ base_url=self.base_url,
40
+ )
41
+ self.tracer = Tracer(_ingest_client)
42
+
34
43
  @classmethod
35
44
  def from_env(cls) -> "AgentX":
36
45
  """Create an AgentX client using AGENTX_API_KEY (and optionally AGENTX_API_BASE_URL) from the environment."""
@@ -14,6 +14,7 @@ from agentx.evaluations.models import (
14
14
  EvaluationResult,
15
15
  EvaluationRun,
16
16
  EvaluationSubject,
17
+ ModelInfo,
17
18
  Report,
18
19
  )
19
20
 
@@ -106,6 +107,19 @@ class EvaluationsClient:
106
107
  return resp.text
107
108
  raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
108
109
 
110
+ # ------------------------------------------------------------------
111
+ # Model registry
112
+ # ------------------------------------------------------------------
113
+
114
+ def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
115
+ """List the LLM models AgentX supports — the same set selectable for
116
+ the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
117
+ to filter."""
118
+ params = {"provider": provider} if provider else None
119
+ data = self._request("GET", "/models", params=params)
120
+ items = data if isinstance(data, list) else data.get("models", [])
121
+ return [ModelInfo(**m) for m in items]
122
+
109
123
  # ------------------------------------------------------------------
110
124
  # Dataset endpoints
111
125
  # ------------------------------------------------------------------
@@ -27,6 +27,10 @@ class DatasetBuilder:
27
27
  acceptance_criteria: Optional[str] = None,
28
28
  rejection_criteria: Optional[str] = None,
29
29
  evaluation_criteria: Optional[str] = None,
30
+ vector_similarity: bool = False,
31
+ jaccard_similarity: bool = False,
32
+ similarity_model: Optional[str] = None,
33
+ sovereignty_models: Optional[List[str]] = None,
30
34
  ):
31
35
  self._client = client
32
36
  self._payload: Dict[str, Any] = {
@@ -38,6 +42,22 @@ class DatasetBuilder:
38
42
  "evaluationCriteria": evaluation_criteria,
39
43
  "questions": [],
40
44
  }
45
+ # Opt-in similarity metrics, surfaced on the report as cosine_similarity /
46
+ # jaccard_similarity (computed against each case's expected_results).
47
+ if vector_similarity:
48
+ vs: Dict[str, Any] = {"enabled": True}
49
+ if similarity_model:
50
+ vs["model"] = similarity_model
51
+ self._payload["vectorSimilarity"] = vs
52
+ if jaccard_similarity:
53
+ self._payload["jaccardSimilarity"] = {"enabled": True}
54
+ # Sovereignty & Portability — the models to compare on this dataset (use
55
+ # client.evaluations.list_models() to discover valid ids).
56
+ if sovereignty_models:
57
+ self._payload["sovereigntyIndex"] = {
58
+ "enabled": True,
59
+ "models": list(sovereignty_models),
60
+ }
41
61
 
42
62
  def add_case(
43
63
  self,
@@ -190,6 +210,10 @@ class DatasetClient:
190
210
  acceptance_criteria: Optional[str] = None,
191
211
  rejection_criteria: Optional[str] = None,
192
212
  evaluation_criteria: Optional[str] = None,
213
+ vector_similarity: bool = False,
214
+ jaccard_similarity: bool = False,
215
+ similarity_model: Optional[str] = None,
216
+ sovereignty_models: Optional[List[str]] = None,
193
217
  ) -> DatasetBuilder:
194
218
  return DatasetBuilder(
195
219
  self._client,
@@ -199,6 +223,10 @@ class DatasetClient:
199
223
  acceptance_criteria=acceptance_criteria,
200
224
  rejection_criteria=rejection_criteria,
201
225
  evaluation_criteria=evaluation_criteria,
226
+ vector_similarity=vector_similarity,
227
+ jaccard_similarity=jaccard_similarity,
228
+ similarity_model=similarity_model,
229
+ sovereignty_models=sovereignty_models,
202
230
  )
203
231
 
204
232
  def from_csv(self, path: str, name: str, **kwargs) -> DatasetBuilder:
@@ -58,6 +58,18 @@ class Dataset(BaseModel):
58
58
  questions: List[DatasetQuestion] = Field(default_factory=list)
59
59
  status: str = "published"
60
60
  version_id: Optional[str] = Field(default=None, alias="versionId")
61
+ # Sovereignty & Portability — models selected to compare on this dataset.
62
+ # Hoisted from the nested ``sovereigntyIndex`` object when enabled.
63
+ sovereignty_models: List[str] = Field(default_factory=list)
64
+
65
+ @model_validator(mode="before")
66
+ @classmethod
67
+ def _extract_sovereignty_models(cls, data: Any) -> Any:
68
+ if isinstance(data, dict):
69
+ sov = data.get("sovereigntyIndex") or data.get("sovereignty_index") or {}
70
+ if isinstance(sov, dict) and sov.get("enabled") and sov.get("models"):
71
+ data = {**data, "sovereignty_models": list(sov.get("models") or [])}
72
+ return data
61
73
 
62
74
  class Config:
63
75
  populate_by_name = True
@@ -99,6 +111,30 @@ class EvaluationSubject(BaseModel):
99
111
  extra = "ignore"
100
112
 
101
113
 
114
+ # ---------------------------------------------------------------------------
115
+ # Supported models (the AgentX model registry / portability set)
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ class ModelInfo(BaseModel):
120
+ """One entry from the AgentX supported-model registry — the set selectable
121
+ for the Sovereignty & Portability Index. Returned by ``list_models()``."""
122
+
123
+ name: str
124
+ display: Optional[str] = None
125
+ provider: Optional[str] = None
126
+ context_window: Optional[int] = Field(default=None, alias="contextWindow")
127
+ max_output_tokens: Optional[int] = Field(default=None, alias="maxOutputTokens")
128
+ input_cost: Optional[float] = Field(default=None, alias="inputCost")
129
+ output_cost: Optional[float] = Field(default=None, alias="outputCost")
130
+ knowledge_cutoff: Optional[str] = Field(default=None, alias="knowledgeCutOff")
131
+ legacy: Optional[bool] = None
132
+
133
+ class Config:
134
+ populate_by_name = True
135
+ extra = "ignore"
136
+
137
+
102
138
  # ---------------------------------------------------------------------------
103
139
  # Run
104
140
  # ---------------------------------------------------------------------------
@@ -144,6 +180,10 @@ class EvaluationCase(BaseModel):
144
180
  expected_capabilities: Optional[List[str]] = None
145
181
  expected_knowledge_base: Optional[List[str]] = None
146
182
  expected_delegations: Optional[List[str]] = None
183
+ # Sovereignty & Portability: the model this case should run on. Set when the
184
+ # dataset selects comparison models; your callable can read it to pick the
185
+ # model. The SDK also tags the submitted result with it.
186
+ model: Optional[str] = None
147
187
 
148
188
  class Config:
149
189
  extra = "ignore"
@@ -299,6 +339,43 @@ class ReportRecommendation(BaseModel):
299
339
  extra = "ignore"
300
340
 
301
341
 
342
+ class SovereigntyModelMetrics(BaseModel):
343
+ """Per-model row of the Sovereignty & Portability matrix."""
344
+
345
+ model: str
346
+ provider: Optional[str] = None
347
+ is_baseline: bool = Field(default=False, alias="isBaseline")
348
+ run_count: int = Field(default=0, alias="runCount")
349
+ average_rating: Optional[float] = Field(default=None, alias="averageRating")
350
+ min_rating: Optional[float] = Field(default=None, alias="minRating")
351
+ max_rating: Optional[float] = Field(default=None, alias="maxRating")
352
+ rating_variance: Optional[float] = Field(default=None, alias="ratingVariance")
353
+ average_vector_similarity: Optional[float] = Field(
354
+ default=None, alias="averageVectorSimilarity"
355
+ )
356
+ average_jaccard_similarity: Optional[float] = Field(
357
+ default=None, alias="averageJaccardSimilarity"
358
+ )
359
+ average_latency_ms: Optional[float] = Field(default=None, alias="averageLatencyMs")
360
+ total_input_tokens: Optional[int] = Field(default=None, alias="totalInputTokens")
361
+ total_output_tokens: Optional[int] = Field(default=None, alias="totalOutputTokens")
362
+
363
+ class Config:
364
+ populate_by_name = True
365
+ extra = "ignore"
366
+
367
+
368
+ class SovereigntyIndex(BaseModel):
369
+ """Sovereignty & Portability matrix — side-by-side per-model performance,
370
+ grouped from the model-tagged results of a run."""
371
+
372
+ enabled: bool = False
373
+ models: List[SovereigntyModelMetrics] = Field(default_factory=list)
374
+
375
+ class Config:
376
+ extra = "ignore"
377
+
378
+
302
379
  class Report(BaseModel):
303
380
  run_id: str = Field(alias="runId")
304
381
  dataset_id: str = Field(alias="datasetId")
@@ -325,6 +402,9 @@ class Report(BaseModel):
325
402
  low_scoring_cases: List[Dict[str, Any]] = Field(
326
403
  default_factory=list, alias="lowScoringCases"
327
404
  )
405
+ sovereignty_index: Optional[SovereigntyIndex] = Field(
406
+ default=None, alias="sovereigntyIndex"
407
+ )
328
408
  dashboard_url: Optional[str] = Field(default=None, alias="dashboardUrl")
329
409
 
330
410
  class Config:
@@ -14,6 +14,7 @@ from agentx.evaluations.models import (
14
14
  EvaluationResult,
15
15
  EvaluationRun,
16
16
  EvaluationSubject,
17
+ ModelInfo,
17
18
  Report,
18
19
  )
19
20
  from agentx.evaluations.redaction import redact_dict
@@ -108,6 +109,12 @@ class EvaluationRunContext:
108
109
 
109
110
  result = normalized(case)
110
111
  result.idempotency_key = idem_key
112
+ # Tag the result with the case's model so the server can group it into
113
+ # the Sovereignty & Portability matrix (the callable may also set it).
114
+ if case.model:
115
+ meta = dict(result.metadata or {})
116
+ meta.setdefault("model", case.model)
117
+ result.metadata = meta
111
118
  result = EvaluationResult(
112
119
  **{**result.model_dump(), "idempotencyKey": idem_key}
113
120
  )
@@ -217,6 +224,13 @@ class EvaluationsRunner:
217
224
  self._client = client
218
225
  self.datasets = client.datasets
219
226
 
227
+ def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
228
+ """List the LLM models AgentX supports — the same set selectable for
229
+ the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
230
+ to filter. Useful for discovering valid model identifiers to compare
231
+ against."""
232
+ return self._client.list_models(provider)
233
+
220
234
  def run(
221
235
  self,
222
236
  dataset_id: str,
@@ -257,21 +271,29 @@ def _wrap_adapter(adapter: AdapterLike) -> Callable[[EvaluationCase], Evaluation
257
271
  def _build_cases(dataset: Dataset) -> List[EvaluationCase]:
258
272
  cases: List[EvaluationCase] = []
259
273
  n_runs = max(dataset.number_of_requests, 1)
274
+ # Sovereignty & Portability: when the dataset selects comparison models, run
275
+ # every question/run once per model in this single run so the report groups
276
+ # results into a per-model portability matrix (mirrors the native route).
277
+ # ``[None]`` keeps legacy single-model behavior (case.model stays unset).
278
+ models: List[Optional[str]] = list(dataset.sovereignty_models) or [None]
260
279
  for q_idx, question in enumerate(dataset.questions):
261
280
  mq = question.main_question
262
281
  for run_num in range(1, n_runs + 1):
263
- cases.append(
264
- EvaluationCase(
265
- case_id=f"case-{q_idx}",
266
- question_index=q_idx,
267
- run_number=run_num,
268
- query=mq.query,
269
- expected_results=mq.expected_results,
270
- expected_capabilities=mq.expected_capabilities,
271
- expected_knowledge_base=mq.expected_knowledge_base,
272
- expected_delegations=mq.expected_delegations,
282
+ for model in models:
283
+ suffix = f"::{model}" if model else ""
284
+ cases.append(
285
+ EvaluationCase(
286
+ case_id=f"case-{q_idx}{suffix}",
287
+ question_index=q_idx,
288
+ run_number=run_num,
289
+ query=mq.query,
290
+ expected_results=mq.expected_results,
291
+ expected_capabilities=mq.expected_capabilities,
292
+ expected_knowledge_base=mq.expected_knowledge_base,
293
+ expected_delegations=mq.expected_delegations,
294
+ model=model,
295
+ )
273
296
  )
274
- )
275
297
  return cases
276
298
 
277
299
 
agentx/exceptions.py ADDED
@@ -0,0 +1,46 @@
1
+ """AgentX SDK exceptions."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from agentx.tracing.ci_types import CIRunResult
8
+
9
+
10
+ class AgentXError(Exception):
11
+ """Base class for all AgentX SDK errors."""
12
+
13
+
14
+ class AgentXAuthError(AgentXError):
15
+ """Invalid or missing API key."""
16
+
17
+
18
+ class AgentXAPIError(AgentXError):
19
+ """Unexpected API error."""
20
+
21
+ def __init__(self, message: str, status_code: int | None = None) -> None:
22
+ super().__init__(message)
23
+ self.status_code = status_code
24
+
25
+
26
+ class DatasetNotFound(AgentXError):
27
+ """Dataset ID does not exist or is not accessible from this API key."""
28
+
29
+
30
+ class CINotEnabled(AgentXError):
31
+ """Dataset exists but ci.enabled is false. Enable CI in the dataset settings."""
32
+
33
+
34
+ class CIRunExpired(AgentXError):
35
+ """CI run was not finalized within the 2-hour window."""
36
+
37
+
38
+ class CIGateFailure(AgentXError):
39
+ """Gate result is 'fail'. Raised when fail_on_gate=True."""
40
+
41
+ def __init__(self, result: "CIRunResult") -> None:
42
+ super().__init__(
43
+ f"CI gate failed: {result.pass_rate:.0%} passed "
44
+ f"({result.passed_questions}/{result.total_questions} questions)"
45
+ )
46
+ self.result = result
@@ -0,0 +1,7 @@
1
+ # Framework-specific integrations for production tracing.
2
+ # Each sub-module is lazily importable so the core SDK has no extra dependencies.
3
+ #
4
+ # from agentx.integrations.langchain import AgentXCallbackHandler
5
+ # from agentx.integrations.crewai import AgentXCrewObserver
6
+ # from agentx.integrations.openai_agents import AgentXTracingProcessor
7
+ # from agentx.integrations.anthropic import patch_anthropic_client
@@ -0,0 +1,152 @@
1
+ """
2
+ Anthropic SDK integration for AgentX production tracing.
3
+
4
+ Usage::
5
+
6
+ from agentx.integrations.anthropic import patch_anthropic_client
7
+ import anthropic
8
+
9
+ client = anthropic.Anthropic()
10
+ patch_anthropic_client(client, agentx.tracer, name="claude-agent")
11
+
12
+ # All subsequent client.messages.create() calls are now traced automatically.
13
+
14
+ Requires: ``pip install agentx[anthropic]``
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import time
19
+ from typing import Any, Dict, Optional
20
+
21
+ from agentx.tracing.tracer import Tracer, _safe_serialize
22
+
23
+
24
+ def patch_anthropic_client(
25
+ client: Any,
26
+ tracer: Tracer,
27
+ name: str = "anthropic-agent",
28
+ metadata: Optional[Dict[str, Any]] = None,
29
+ session_id: Optional[str] = None,
30
+ ) -> None:
31
+ """
32
+ Monkey-patch ``client.messages.create`` and ``client.messages.stream``
33
+ (if present) to automatically send a trace for every call.
34
+
35
+ The original method is still called and its return value is passed through
36
+ unchanged so nothing in the caller needs to change.
37
+ """
38
+ messages = getattr(client, "messages", None)
39
+ if messages is None:
40
+ raise ValueError("Provided client does not have a .messages attribute")
41
+
42
+ _patch_create(messages, tracer, name, metadata, session_id)
43
+
44
+ # stream is optional (not present in all versions)
45
+ if hasattr(messages, "stream"):
46
+ _patch_stream(messages, tracer, name, metadata, session_id)
47
+
48
+
49
+ def _patch_create(
50
+ messages_resource: Any,
51
+ tracer: Tracer,
52
+ name: str,
53
+ metadata: Optional[Dict[str, Any]],
54
+ session_id: Optional[str],
55
+ ) -> None:
56
+ original = messages_resource.create
57
+ if getattr(original, "_agentx_patched", False):
58
+ return # already patched
59
+
60
+ def patched_create(*args, **kwargs):
61
+ start = time.time()
62
+ error: Optional[str] = None
63
+ response = None
64
+ try:
65
+ response = original(*args, **kwargs)
66
+ return response
67
+ except Exception as exc:
68
+ error = str(exc)
69
+ raise
70
+ finally:
71
+ latency_ms = int((time.time() - start) * 1000)
72
+ input_messages = kwargs.get("messages") or (args[0] if args else None)
73
+ model = kwargs.get("model")
74
+ output = None
75
+ if response is not None:
76
+ try:
77
+ output = response.content[0].text if response.content else None
78
+ except Exception:
79
+ output = str(response)[:500]
80
+ tracer._send(
81
+ name=name,
82
+ input=_safe_serialize(input_messages),
83
+ output=output,
84
+ latency_ms=latency_ms,
85
+ error=error,
86
+ framework="anthropic",
87
+ model=model,
88
+ metadata=metadata,
89
+ session_id=session_id,
90
+ )
91
+
92
+ patched_create._agentx_patched = True
93
+ messages_resource.create = patched_create
94
+
95
+
96
+ def _patch_stream(
97
+ messages_resource: Any,
98
+ tracer: Tracer,
99
+ name: str,
100
+ metadata: Optional[Dict[str, Any]],
101
+ session_id: Optional[str],
102
+ ) -> None:
103
+ original_stream = messages_resource.stream
104
+ if getattr(original_stream, "_agentx_patched", False):
105
+ return
106
+
107
+ def patched_stream(*args, **kwargs):
108
+ start = time.time()
109
+ ctx = original_stream(*args, **kwargs)
110
+
111
+ class _TracedStream:
112
+ """Thin wrapper that records timing when the stream context exits."""
113
+
114
+ def __enter__(self_inner):
115
+ return ctx.__enter__()
116
+
117
+ def __exit__(self_inner, exc_type, exc_val, tb):
118
+ result = ctx.__exit__(exc_type, exc_val, tb)
119
+ latency_ms = int((time.time() - start) * 1000)
120
+ error = str(exc_val) if exc_val else None
121
+ output = None
122
+ try:
123
+ final = ctx.get_final_message()
124
+ output = final.content[0].text if final.content else None
125
+ except Exception:
126
+ pass
127
+ tracer._send(
128
+ name=name,
129
+ input=_safe_serialize(kwargs.get("messages")),
130
+ output=output,
131
+ latency_ms=latency_ms,
132
+ error=error,
133
+ framework="anthropic",
134
+ model=kwargs.get("model"),
135
+ metadata=metadata,
136
+ session_id=session_id,
137
+ )
138
+ return result
139
+
140
+ def __iter__(self_inner):
141
+ return iter(ctx)
142
+
143
+ def __aiter__(self_inner):
144
+ return aiter(ctx)
145
+
146
+ def __getattr__(self_inner, item):
147
+ return getattr(ctx, item)
148
+
149
+ return _TracedStream()
150
+
151
+ patched_stream._agentx_patched = True
152
+ messages_resource.stream = patched_stream
@@ -0,0 +1,106 @@
1
+ """
2
+ CrewAI integration for AgentX production tracing.
3
+
4
+ Usage::
5
+
6
+ from agentx.integrations.crewai import AgentXCrewObserver
7
+
8
+ observer = AgentXCrewObserver(agentx.tracer, name="my-crew")
9
+ result = observer.kickoff(crew, inputs={"topic": "AI"})
10
+
11
+ Or as a context manager around your own kickoff::
12
+
13
+ with observer.observe(name="my-crew", input={"topic": "AI"}) as span:
14
+ result = crew.kickoff(inputs={"topic": "AI"})
15
+ span.output = result.raw
16
+
17
+ Requires: ``pip install agentx[crewai]``
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import time
22
+ from typing import Any, Dict, Optional
23
+
24
+ from agentx.tracing.tracer import Tracer, _safe_serialize
25
+
26
+
27
+ class AgentXCrewObserver:
28
+ """
29
+ Wraps a CrewAI ``Crew.kickoff()`` call and sends one trace per execution.
30
+ Does not require any CrewAI version-specific event system.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ tracer: Tracer,
36
+ name: str = "crewai-crew",
37
+ metadata: Optional[Dict[str, Any]] = None,
38
+ session_id: Optional[str] = None,
39
+ ) -> None:
40
+ self._tracer = tracer
41
+ self._name = name
42
+ self._metadata = metadata
43
+ self._session_id = session_id
44
+
45
+ def kickoff(self, crew: Any, inputs: Optional[Dict[str, Any]] = None) -> Any:
46
+ """
47
+ Call ``crew.kickoff(inputs=inputs)``, capture result, and send a trace.
48
+ Returns the raw CrewAI ``CrewOutput`` object unchanged.
49
+ """
50
+ start = time.time()
51
+ error: Optional[str] = None
52
+ result = None
53
+ try:
54
+ result = crew.kickoff(inputs=inputs or {})
55
+ return result
56
+ except Exception as exc:
57
+ error = str(exc)
58
+ raise
59
+ finally:
60
+ latency_ms = int((time.time() - start) * 1000)
61
+ output = None
62
+ if result is not None:
63
+ output = getattr(result, "raw", None) or _safe_serialize(result)
64
+
65
+ # Collect task outputs as tool_calls for observability
66
+ tool_calls = []
67
+ if result is not None:
68
+ task_outputs = getattr(result, "tasks_output", []) or []
69
+ for task_out in task_outputs:
70
+ tool_calls.append(
71
+ {
72
+ "name": getattr(task_out, "description", "task")[:100],
73
+ "output": str(getattr(task_out, "raw", ""))[:500],
74
+ }
75
+ )
76
+
77
+ self._tracer._send(
78
+ name=self._name,
79
+ input=_safe_serialize(inputs) if inputs else None,
80
+ output=output,
81
+ latency_ms=latency_ms,
82
+ error=error,
83
+ framework="crewai",
84
+ tool_calls=tool_calls or None,
85
+ metadata=self._metadata,
86
+ session_id=self._session_id,
87
+ )
88
+
89
+ def observe(
90
+ self,
91
+ name: Optional[str] = None,
92
+ input: Optional[Any] = None,
93
+ metadata: Optional[Dict[str, Any]] = None,
94
+ session_id: Optional[str] = None,
95
+ ):
96
+ """Return a context-manager span for manual kickoff wrapping."""
97
+ from agentx.tracing.tracer import _TraceSpan
98
+
99
+ return _TraceSpan(
100
+ tracer=self._tracer,
101
+ name=name or self._name,
102
+ input=_safe_serialize(input) if input is not None else None,
103
+ metadata=metadata or self._metadata,
104
+ framework="crewai",
105
+ session_id=session_id or self._session_id,
106
+ )