agentx-python 0.8.20__tar.gz → 0.8.21__tar.gz
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_python-0.8.20/agentx_python.egg-info → agentx_python-0.8.21}/PKG-INFO +1 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/client.py +25 -8
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/datasets.py +2 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/evaluation_settings.py +2 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/models.py +4 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/prompts.py +3 -3
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/runner.py +5 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/crewai.py +3 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/google_adk.py +6 -2
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/langchain.py +4 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/openai_agents.py +3 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/client.py +7 -2
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/models.py +7 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/profile.py +1 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/tracer.py +42 -28
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/version.py +2 -2
- {agentx_python-0.8.20 → agentx_python-0.8.21/agentx_python.egg-info}/PKG-INFO +1 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_runner_features.py +16 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_selfhost_analysis_fallback.py +25 -1
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_span_tree.py +52 -5
- {agentx_python-0.8.20 → agentx_python-0.8.21}/LICENSE +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/README.md +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/agentx.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/cli.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/export.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/feedback.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/improvement_groups.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/judge_scorers.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/rules.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/scorer_groups.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/scorers.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/projects.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/py.typed +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/testing.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/traces.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx/util.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/SOURCES.txt +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/setup.cfg +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/setup.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_integration.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_judge_scorers.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.20 → agentx_python-0.8.21}/tests/test_testing.py +0 -0
|
@@ -207,6 +207,12 @@ class EvaluationsClient:
|
|
|
207
207
|
return resp.json()
|
|
208
208
|
except Exception:
|
|
209
209
|
return resp.text
|
|
210
|
+
# A timeout keeps its type: runner._flush_batch catches requests.Timeout specifically
|
|
211
|
+
# (the engine may still be scoring the batch - a blind retry double-bills every judge
|
|
212
|
+
# call), and wrapping it in AgentXEvaluationsError here made that guard unreachable.
|
|
213
|
+
# Applies to retry=False calls too, where the single attempt lands straight here.
|
|
214
|
+
if isinstance(last_exc, requests.Timeout):
|
|
215
|
+
raise last_exc
|
|
210
216
|
raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
|
|
211
217
|
|
|
212
218
|
# ------------------------------------------------------------------
|
|
@@ -227,7 +233,9 @@ class EvaluationsClient:
|
|
|
227
233
|
# ------------------------------------------------------------------
|
|
228
234
|
|
|
229
235
|
def create_dataset(self, payload: dict) -> Dataset:
|
|
230
|
-
|
|
236
|
+
# Server-side write: a timeout after the dataset row was created would be
|
|
237
|
+
# retried into a duplicate dataset, so no transport retry.
|
|
238
|
+
data = self._request("POST", "/datasets", json=self._with_workspace(payload), retry=False)
|
|
231
239
|
return Dataset(**data)
|
|
232
240
|
|
|
233
241
|
def delete_dataset(self, dataset_id: str) -> None:
|
|
@@ -255,8 +263,9 @@ class EvaluationsClient:
|
|
|
255
263
|
# ------------------------------------------------------------------
|
|
256
264
|
|
|
257
265
|
def create_evaluation_settings(self, payload: dict) -> EvaluationSettings:
|
|
266
|
+
# Server-side write - no transport retry (see init_run's comment).
|
|
258
267
|
data = self._request(
|
|
259
|
-
"POST", "/evaluation-settings", json=self._with_workspace(payload)
|
|
268
|
+
"POST", "/evaluation-settings", json=self._with_workspace(payload), retry=False
|
|
260
269
|
)
|
|
261
270
|
return EvaluationSettings(**data)
|
|
262
271
|
|
|
@@ -281,12 +290,14 @@ class EvaluationsClient:
|
|
|
281
290
|
|
|
282
291
|
# ------------------------------------------------------------------
|
|
283
292
|
# Prompt registry endpoints - see agentx.evaluations.prompts.PromptClient for the concept
|
|
284
|
-
# (the external-agent analog to native autotune).
|
|
285
|
-
#
|
|
293
|
+
# (the external-agent analog to native autotune). propose_prompt never publishes;
|
|
294
|
+
# publish_prompt_version below IS the explicit approval step - call it only after a human
|
|
295
|
+
# reviewed the proposal.
|
|
286
296
|
# ------------------------------------------------------------------
|
|
287
297
|
|
|
288
298
|
def create_prompt(self, payload: dict) -> Prompt:
|
|
289
|
-
|
|
299
|
+
# Server-side write - no transport retry (see init_run's comment).
|
|
300
|
+
data = self._request("POST", "/prompts", json=self._with_workspace(payload), retry=False)
|
|
290
301
|
return Prompt(**data)
|
|
291
302
|
|
|
292
303
|
def list_prompts(self) -> List[Prompt]:
|
|
@@ -322,7 +333,9 @@ class EvaluationsClient:
|
|
|
322
333
|
alias and keeps working. ``split`` records the named case subset this run covers.
|
|
323
334
|
``additional_scorer_ids`` (self-host): extra judge scorers that each pass their own
|
|
324
335
|
verdict on every result from the same single agent execution - verdicts land in each
|
|
325
|
-
result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``.
|
|
336
|
+
result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``. When
|
|
337
|
+
``scorer_group_id`` is set, the engine nulls ``additionalScorerIds`` on the run too -
|
|
338
|
+
the group is the whole grading story, not a layer on top of extra scorers."""
|
|
326
339
|
from agentx.version import VERSION
|
|
327
340
|
|
|
328
341
|
grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
|
|
@@ -343,7 +356,8 @@ class EvaluationsClient:
|
|
|
343
356
|
if additional_scorer_ids:
|
|
344
357
|
payload["additionalScorerIds"] = additional_scorer_ids
|
|
345
358
|
# Scorer group grading (self-host): the group's weighted 0-10 aggregate fills the rating
|
|
346
|
-
# column and member verdicts land per row. Mutually exclusive with scorer_id (group
|
|
359
|
+
# column and member verdicts land per row. Mutually exclusive with scorer_id (group
|
|
360
|
+
# wins), and the engine also nulls additionalScorerIds when a group grades the run.
|
|
347
361
|
if scorer_group_id:
|
|
348
362
|
payload["scorerGroupId"] = scorer_group_id
|
|
349
363
|
if split:
|
|
@@ -408,7 +422,10 @@ class EvaluationsClient:
|
|
|
408
422
|
# scorer's own per-result verdicts. Unknown names are a hard 400 from the engine.
|
|
409
423
|
if scorer:
|
|
410
424
|
params["scorer"] = scorer
|
|
411
|
-
|
|
425
|
+
# record=True is a server-side write despite the GET verb (it persists a gate-history
|
|
426
|
+
# row): a timeout after the row was stored would be retried into a duplicate verdict,
|
|
427
|
+
# so no transport retry - same precedent as init_run.
|
|
428
|
+
return self._request("GET", f"/runs/{run_id}/gate", params=params, retry=not record)
|
|
412
429
|
|
|
413
430
|
def analyze_run(
|
|
414
431
|
self,
|
|
@@ -81,7 +81,8 @@ class DatasetBuilder:
|
|
|
81
81
|
self._payload["codeScorers"] = [
|
|
82
82
|
{
|
|
83
83
|
"id": scorer.get("id") or _uuid.uuid4().hex[:12],
|
|
84
|
-
|
|
84
|
+
# Name may be omitted - the engine defaults it, so don't KeyError here.
|
|
85
|
+
"name": scorer.get("name"),
|
|
85
86
|
"code": scorer["code"],
|
|
86
87
|
"enabled": scorer.get("enabled", True),
|
|
87
88
|
}
|
|
@@ -64,7 +64,8 @@ class EvaluationSettingsBuilder:
|
|
|
64
64
|
if rouge_score:
|
|
65
65
|
self._payload["rougeScore"] = {"enabled": True}
|
|
66
66
|
# Sovereignty & Portability - the models to compare when this config runs
|
|
67
|
-
# (use client.evaluations.list_models() to discover valid ids).
|
|
67
|
+
# (use client.evaluations.list_models() to discover valid ids). Self-host: accepted
|
|
68
|
+
# on the wire but not acted on by the engine (same caveat as DatasetBuilder's).
|
|
68
69
|
if sovereignty_models:
|
|
69
70
|
self._payload["sovereigntyIndex"] = {
|
|
70
71
|
"enabled": True,
|
|
@@ -248,6 +248,10 @@ class LiveStatistics(BaseModel):
|
|
|
248
248
|
min_rating: Optional[float] = Field(default=None, alias="minRating")
|
|
249
249
|
max_rating: Optional[float] = Field(default=None, alias="maxRating")
|
|
250
250
|
rated_count: int = Field(default=0, alias="ratedCount")
|
|
251
|
+
# Rows the judge could not score / rows submitted with an error - the difference between
|
|
252
|
+
# "everything rated 8" and "half the run never got a verdict".
|
|
253
|
+
skipped_count: int = Field(default=0, alias="skippedCount")
|
|
254
|
+
failed_count: int = Field(default=0, alias="failedCount")
|
|
251
255
|
|
|
252
256
|
class Config:
|
|
253
257
|
populate_by_name = True
|
|
@@ -17,9 +17,9 @@ class PromptClient:
|
|
|
17
17
|
existing version-comparison view (``client.evaluations`` run comparisons on a dataset) can
|
|
18
18
|
tell you which published version actually scored higher.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
``propose()`` never publishes anything; ``publish_version()`` IS the explicit approval
|
|
21
|
+
step - call it only after a human reviewed the proposal, since a published version is
|
|
22
|
+
what your running agent pulls as its live prompt.
|
|
23
23
|
|
|
24
24
|
Example::
|
|
25
25
|
|
|
@@ -342,6 +342,10 @@ class EvaluationRunContext:
|
|
|
342
342
|
# ------------------------------------------------------------------
|
|
343
343
|
|
|
344
344
|
def finalize(self) -> "EvaluationRunContext":
|
|
345
|
+
"""Mark the run completed server-side. A failed finalize is raised, not swallowed
|
|
346
|
+
(same fail-loud posture as _flush_batch): it leaves the run in_progress - a state a
|
|
347
|
+
CI pipeline MUST treat as a failure, since gates and baselines only consider
|
|
348
|
+
completed runs."""
|
|
345
349
|
_say()
|
|
346
350
|
with Spinner("Finalizing - submitting results"):
|
|
347
351
|
try:
|
|
@@ -353,6 +357,7 @@ class EvaluationRunContext:
|
|
|
353
357
|
except Exception as exc:
|
|
354
358
|
_say(f" {red('✗')} Finalize failed: {dim(str(exc))}")
|
|
355
359
|
logger.error("Finalize failed: %s", exc)
|
|
360
|
+
raise
|
|
356
361
|
return self
|
|
357
362
|
|
|
358
363
|
def gate(
|
|
@@ -212,6 +212,9 @@ class AgentXCrewObserver:
|
|
|
212
212
|
"end_time": end,
|
|
213
213
|
"input": description,
|
|
214
214
|
"output": output_text,
|
|
215
|
+
# A CrewAI task is an agent turn, not a model call - without this,
|
|
216
|
+
# _merge_child_run's default stamped every task span "llm".
|
|
217
|
+
"kind": "agent",
|
|
215
218
|
})
|
|
216
219
|
if description is not None or task_output_text is not None:
|
|
217
220
|
tool_calls.append({"name": name, "input": description, "output": task_output_text})
|
|
@@ -231,6 +231,7 @@ class AgentXADKPlugin(BasePlugin):
|
|
|
231
231
|
model=call_start.get("model") if call_start else None,
|
|
232
232
|
input_tokens=call_input_tokens,
|
|
233
233
|
output_tokens=call_output_tokens,
|
|
234
|
+
span_kind="llm",
|
|
234
235
|
)
|
|
235
236
|
|
|
236
237
|
async def on_model_error_callback(
|
|
@@ -261,6 +262,7 @@ class AgentXADKPlugin(BasePlugin):
|
|
|
261
262
|
output=f"ERROR: {error}",
|
|
262
263
|
model=call_start.get("model") if call_start else None,
|
|
263
264
|
error=str(error),
|
|
265
|
+
span_kind="llm",
|
|
264
266
|
)
|
|
265
267
|
|
|
266
268
|
# ------------------------------------------------------------------
|
|
@@ -290,7 +292,8 @@ class AgentXADKPlugin(BasePlugin):
|
|
|
290
292
|
tool_input = _safe_serialize(tool_args)
|
|
291
293
|
tool_output = str(result) if result is not None else None
|
|
292
294
|
state["root_span"].child_span(
|
|
293
|
-
tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output
|
|
295
|
+
tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output,
|
|
296
|
+
span_kind="tool",
|
|
294
297
|
)
|
|
295
298
|
|
|
296
299
|
async def on_tool_error_callback(
|
|
@@ -311,5 +314,6 @@ class AgentXADKPlugin(BasePlugin):
|
|
|
311
314
|
tool_input = _safe_serialize(tool_args)
|
|
312
315
|
tool_output = f"ERROR: {error}"
|
|
313
316
|
state["root_span"].child_span(
|
|
314
|
-
tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error)
|
|
317
|
+
tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error),
|
|
318
|
+
span_kind="tool",
|
|
315
319
|
)
|
|
@@ -277,6 +277,10 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
277
277
|
become child spans, and each LLM call / tool call / retrieval becomes a span parented under
|
|
278
278
|
the node that ran it - so the engine's Execution Timeline shows the actual graph trajectory
|
|
279
279
|
(which nodes ran, in what order, and what each did), not a flat step list.
|
|
280
|
+
|
|
281
|
+
Retriever runs are stamped ``retrieval`` - LangChain cannot distinguish memory-backed
|
|
282
|
+
retrievers, so a Mem0/Zep-style store exposed as a retriever classifies as retrieval too.
|
|
283
|
+
Use ``tracer.trace_memory`` for lookups that should classify as memory.
|
|
280
284
|
"""
|
|
281
285
|
|
|
282
286
|
def __init__(
|
|
@@ -251,6 +251,7 @@ class AgentXTracingProcessor:
|
|
|
251
251
|
model=call_model,
|
|
252
252
|
input_tokens=call_input_tokens,
|
|
253
253
|
output_tokens=call_output_tokens,
|
|
254
|
+
span_kind="llm",
|
|
254
255
|
)
|
|
255
256
|
|
|
256
257
|
elif span_type == "response":
|
|
@@ -300,6 +301,7 @@ class AgentXTracingProcessor:
|
|
|
300
301
|
model=call_model,
|
|
301
302
|
input_tokens=call_input_tokens,
|
|
302
303
|
output_tokens=call_output_tokens,
|
|
304
|
+
span_kind="llm",
|
|
303
305
|
)
|
|
304
306
|
|
|
305
307
|
elif span_type == "function":
|
|
@@ -313,6 +315,7 @@ class AgentXTracingProcessor:
|
|
|
313
315
|
duration_ms=latency if t0 is None or t1 is None else None,
|
|
314
316
|
input=span_data.input,
|
|
315
317
|
output=tool_output,
|
|
318
|
+
span_kind="tool",
|
|
316
319
|
)
|
|
317
320
|
|
|
318
321
|
def force_flush(self) -> None:
|
|
@@ -235,7 +235,9 @@ class MonitorClient:
|
|
|
235
235
|
# ------------------------------------------------------------------
|
|
236
236
|
|
|
237
237
|
def create_pattern(self, payload: dict) -> MonitorPattern:
|
|
238
|
-
|
|
238
|
+
# Server-side write: a timeout after the pattern row was created would be
|
|
239
|
+
# retried into a duplicate pattern, so no transport retry.
|
|
240
|
+
data = self._request("POST", "/patterns", json=self._with_workspace(payload), retry=False)
|
|
239
241
|
return MonitorPattern(**data["pattern"])
|
|
240
242
|
|
|
241
243
|
def list_patterns(self) -> List[MonitorPattern]:
|
|
@@ -253,7 +255,10 @@ class MonitorClient:
|
|
|
253
255
|
# ------------------------------------------------------------------
|
|
254
256
|
|
|
255
257
|
def create_online_evaluator(self, payload: dict) -> MonitorOnlineEvaluator:
|
|
256
|
-
|
|
258
|
+
# Server-side write - no transport retry (see create_pattern's comment).
|
|
259
|
+
data = self._request(
|
|
260
|
+
"POST", "/online-evaluators", json=self._with_workspace(payload), retry=False
|
|
261
|
+
)
|
|
257
262
|
return MonitorOnlineEvaluator(**data["evaluator"])
|
|
258
263
|
|
|
259
264
|
@property
|
|
@@ -12,6 +12,11 @@ class MonitorPattern(BaseModel):
|
|
|
12
12
|
A "failure" pattern (the default) raises a signal to triage; a "proper" pattern logs a
|
|
13
13
|
healthy tally instead. Only one of ``include_terms``/``regex``/``semantic_prompt`` is
|
|
14
14
|
meaningful at a time, selected by ``detector_kind``.
|
|
15
|
+
|
|
16
|
+
On self-host the engine stores a pattern as a list of ``conditions`` (each with its own
|
|
17
|
+
detector kind and match settings) - the flat ``include_terms``/``regex``/
|
|
18
|
+
``semantic_prompt`` fields are display-only projections derived from the first condition;
|
|
19
|
+
``conditions`` is the truth.
|
|
15
20
|
"""
|
|
16
21
|
|
|
17
22
|
id: str = Field(alias="_id")
|
|
@@ -26,6 +31,8 @@ class MonitorPattern(BaseModel):
|
|
|
26
31
|
exclude_terms: List[str] = Field(default_factory=list, alias="excludeTerms")
|
|
27
32
|
regex: Optional[str] = None
|
|
28
33
|
semantic_prompt: Optional[str] = Field(default=None, alias="semanticPrompt")
|
|
34
|
+
# The engine's stored detection rules (self-host) - see the class docstring.
|
|
35
|
+
conditions: List[Dict[str, Any]] = Field(default_factory=list)
|
|
29
36
|
severity: str = "medium"
|
|
30
37
|
polarity: str = "failure"
|
|
31
38
|
enabled: bool = True
|
|
@@ -46,7 +46,7 @@ class MonitorProfileClient:
|
|
|
46
46
|
"""Update (and enable, if not already) this agent's Monitor profile. Only fields passed
|
|
47
47
|
here are changed; everything else on the existing profile is left as is.
|
|
48
48
|
|
|
49
|
-
Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``,
|
|
49
|
+
Self-host only: ``coverage_mode``/``sample_rate``/``retention_days``, ``dataset_id``,
|
|
50
50
|
and ``threshold_overrides["latencyMs"]`` are project-level defaults now (see
|
|
51
51
|
``MonitorProfile``'s docstring) - set them via the dashboard's Platform Settings screen
|
|
52
52
|
instead, passing them here is accepted but has no effect. ``enabled``/
|
|
@@ -5,6 +5,7 @@ import concurrent.futures
|
|
|
5
5
|
import functools
|
|
6
6
|
import inspect
|
|
7
7
|
import contextvars
|
|
8
|
+
import logging
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
from contextlib import contextmanager
|
|
@@ -17,6 +18,8 @@ from agentx.tracing.ci_types import CIRun, CIRunResult, CIRunStatus, CIQuestionS
|
|
|
17
18
|
from agentx.tracing.eval_scope import EVAL_RUN_SOURCE, current_eval_run_id
|
|
18
19
|
from agentx.tracing.framework_detect import detect_framework
|
|
19
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
20
23
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
24
|
|
|
22
25
|
|
|
@@ -313,7 +316,7 @@ class _TraceSpan:
|
|
|
313
316
|
their own real per-step identity and timing (LangChain's run_id/parent_run_id,
|
|
314
317
|
LlamaIndex's parent_id, the OpenAI Agents SDK's own span objects) and want to parent a new
|
|
315
318
|
child under a specific span they're holding a reference to - not just whatever's on top of
|
|
316
|
-
the tracer's
|
|
319
|
+
the tracer's context-local (ContextVar) active-span stack.
|
|
317
320
|
|
|
318
321
|
Returns the child span (its ``.span_id`` can parent a further-nested grandchild via
|
|
319
322
|
another ``child_span()`` call on it). The returned span is not pushed onto the
|
|
@@ -441,8 +444,10 @@ class _TraceSpan:
|
|
|
441
444
|
cache_read_tokens=step.get("cacheReadTokenSize"),
|
|
442
445
|
cache_write_tokens=step.get("cacheWriteTokenSize"),
|
|
443
446
|
# Stated, so a step named anything other than "LLM Call N" still classifies -
|
|
444
|
-
# the backend's name regex was the only thing holding this together.
|
|
445
|
-
|
|
447
|
+
# the backend's name regex was the only thing holding this together. Steps
|
|
448
|
+
# may state their own kind (crewai.py's task steps carry "agent"); the
|
|
449
|
+
# default stays "llm" for callers whose steps are model calls.
|
|
450
|
+
span_kind=step.get("kind") or "llm",
|
|
446
451
|
)
|
|
447
452
|
for tc in tool_calls or []:
|
|
448
453
|
# Some callers' tool_calls dicts (e.g. langchain.py's, which sets these on the
|
|
@@ -477,6 +482,7 @@ class _TraceSpan:
|
|
|
477
482
|
})
|
|
478
483
|
for step in [] if not emit_steps else (retrieval_steps or []):
|
|
479
484
|
self._child_span_count += 1
|
|
485
|
+
doc_count = step.get("doc_count")
|
|
480
486
|
self.child_span(
|
|
481
487
|
step.get("name") or f"Retrieval {self._child_span_count}",
|
|
482
488
|
start_time=step.get("start_time"),
|
|
@@ -484,7 +490,7 @@ class _TraceSpan:
|
|
|
484
490
|
duration_ms=step.get("duration_ms"),
|
|
485
491
|
input=step.get("query"),
|
|
486
492
|
output=step.get("output"),
|
|
487
|
-
metadata={"kind": "retrieval"},
|
|
493
|
+
metadata={"kind": "retrieval", **({"doc_count": doc_count} if doc_count is not None else {})},
|
|
488
494
|
span_kind="retrieval",
|
|
489
495
|
)
|
|
490
496
|
|
|
@@ -680,9 +686,11 @@ class Tracer:
|
|
|
680
686
|
"""
|
|
681
687
|
Make ``span`` (created on another thread) the active span for the
|
|
682
688
|
duration of this block, on *this* thread. The active-span stack is
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
689
|
+
context-local (a ContextVar): bare threads start with an empty stack,
|
|
690
|
+
while asyncio tasks inherit a copy of their creator's. Work submitted
|
|
691
|
+
to a ``ThreadPoolExecutor`` or run on any other thread therefore
|
|
692
|
+
doesn't automatically see a span opened on the calling thread - wrap
|
|
693
|
+
the worker function body in this to attach it::
|
|
686
694
|
|
|
687
695
|
with tracer.trace("orchestrator") as span:
|
|
688
696
|
def worker():
|
|
@@ -719,7 +727,10 @@ class Tracer:
|
|
|
719
727
|
loop where the tool executes in plain Python between two
|
|
720
728
|
``messages.create()`` calls. Sent as a real child-span row of the active span (see
|
|
721
729
|
``current_span``) immediately; queued onto the next trace's plain ``tool_calls`` list if
|
|
722
|
-
there's no active span to attach a child to.
|
|
730
|
+
there's no active span to attach a child to. That queue is tracer-wide, not
|
|
731
|
+
per-context: the pending record attaches to the next trace THIS TRACER sends from ANY
|
|
732
|
+
thread or context, so concurrent no-span use can attach it to an unrelated trace -
|
|
733
|
+
wrap the call in ``tracer.trace()`` when that matters.
|
|
723
734
|
|
|
724
735
|
``success``/``error`` mark a failed call. ``success=False`` is what the engine's built-in
|
|
725
736
|
"Tool failure" Monitor check and the dashboard's Tool quality column both read; leaving
|
|
@@ -824,6 +835,11 @@ class Tracer:
|
|
|
824
835
|
(same behavior as ``record_tool_call``, covering the patched-client flow where the
|
|
825
836
|
retrieval runs just before a standalone ``messages.create()`` /
|
|
826
837
|
``chat.completions.create()`` call).
|
|
838
|
+
|
|
839
|
+
The pending queue is tracer-wide, not per-context: a record queued with no active
|
|
840
|
+
span attaches to the next trace THIS TRACER sends from ANY thread or context, so
|
|
841
|
+
concurrent no-span use can attach it to an unrelated trace. Wrap the call in
|
|
842
|
+
``tracer.trace()`` when that matters.
|
|
827
843
|
"""
|
|
828
844
|
active_span = self.current_span
|
|
829
845
|
if active_span is None:
|
|
@@ -839,6 +855,7 @@ class Tracer:
|
|
|
839
855
|
"query": _safe_serialize(query) if query is not None else None,
|
|
840
856
|
"output": _safe_serialize(output) if output is not None else None,
|
|
841
857
|
"duration_ms": latency_ms,
|
|
858
|
+
**({"doc_count": doc_count} if doc_count is not None else {}),
|
|
842
859
|
})
|
|
843
860
|
return
|
|
844
861
|
# The kind marker is what tells the engine (retrieval-context extraction for RAG
|
|
@@ -852,7 +869,7 @@ class Tracer:
|
|
|
852
869
|
duration_ms=duration_ms,
|
|
853
870
|
input=query,
|
|
854
871
|
output=output,
|
|
855
|
-
metadata={"kind": "retrieval"},
|
|
872
|
+
metadata={"kind": "retrieval", **({"doc_count": doc_count} if doc_count is not None else {})},
|
|
856
873
|
span_kind="retrieval",
|
|
857
874
|
)
|
|
858
875
|
|
|
@@ -874,27 +891,23 @@ class Tracer:
|
|
|
874
891
|
itself stays one value so dashboards and scorers can select all memory activity at once.
|
|
875
892
|
Deliberately NOT a retrieval: retrieval spans feed the RAG judges' ``{context}``
|
|
876
893
|
(knowledge grounding), while memory is recalled state - see the engine's spanKind.ts.
|
|
894
|
+
|
|
895
|
+
With no active span the record is DROPPED (with a debug log), not queued: the only
|
|
896
|
+
pending queue rides the next trace's ``retrieval_steps``, and memory content must
|
|
897
|
+
never reach the engine's retrieval-context extraction for RAG judges. Wrap the call
|
|
898
|
+
in ``tracer.trace()`` to keep it. (``record_tool_call``/``record_retrieval`` queue
|
|
899
|
+
instead - see their docstrings.)
|
|
877
900
|
"""
|
|
878
901
|
active_span = self.current_span
|
|
879
902
|
if active_span is None:
|
|
880
|
-
#
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if start_time is not None and end_time is not None
|
|
888
|
-
else None
|
|
903
|
+
# NOT the record_retrieval queue posture: _pending_retrievals rides the next
|
|
904
|
+
# trace's performance_summary.retrieval_steps, which the engine's
|
|
905
|
+
# retrieval-context extraction feeds to RAG judges - recalled memory must never
|
|
906
|
+
# classify as knowledge grounding. Drop, and say so.
|
|
907
|
+
logger.debug(
|
|
908
|
+
"record_memory(%r) called with no active span - wrap the call in tracer.trace(); dropped",
|
|
909
|
+
name,
|
|
889
910
|
)
|
|
890
|
-
self._pending_retrievals.append({
|
|
891
|
-
"name": name,
|
|
892
|
-
"query": _safe_serialize(query) if query is not None else None,
|
|
893
|
-
"output": _safe_serialize(output) if output is not None else None,
|
|
894
|
-
"duration_ms": latency_ms,
|
|
895
|
-
"kind": "memory",
|
|
896
|
-
**({"operation": operation} if operation else {}),
|
|
897
|
-
})
|
|
898
911
|
return
|
|
899
912
|
active_span.child_span(
|
|
900
913
|
name,
|
|
@@ -923,9 +936,10 @@ class Tracer:
|
|
|
923
936
|
error: Optional[str] = None
|
|
924
937
|
try:
|
|
925
938
|
yield recorder
|
|
926
|
-
except
|
|
939
|
+
except BaseException as exc:
|
|
927
940
|
# A memory op that raised must not be recorded as a clean span (trace_tool_call
|
|
928
|
-
# precedent) - fold the error into the output
|
|
941
|
+
# precedent, which also catches BaseException) - fold the error into the output
|
|
942
|
+
# and re-raise.
|
|
929
943
|
error = str(exc)
|
|
930
944
|
raise
|
|
931
945
|
finally:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
VERSION = "0.8.
|
|
1
|
+
VERSION = "0.8.21"
|
|
2
2
|
|
|
3
3
|
# The AgentX-trace-eval release this SDK version is tested against - what `agentx-trace-eval`
|
|
4
4
|
# installs and converges to (see agentx/cli.py). Bump together with VERSION when releasing, so
|
|
5
5
|
# every published SDK names a known-good engine+dashboard pair. Users can override with
|
|
6
6
|
# AGENTX_TRACE_EVAL_VERSION=<tag|latest>.
|
|
7
|
-
ENGINE_VERSION = "v0.3.
|
|
7
|
+
ENGINE_VERSION = "v0.3.22"
|
|
@@ -144,3 +144,19 @@ def test_flush_batch_failure_raises_after_one_retry(monkeypatch):
|
|
|
144
144
|
|
|
145
145
|
with pytest.raises(EvaluationSubmissionError):
|
|
146
146
|
ctx.execute(lambda case: "x")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_finalize_failure_raises(monkeypatch):
|
|
150
|
+
"""A failed finalize leaves the run in_progress - swallowing it let CI pipelines pass on
|
|
151
|
+
a run that gates and baselines would never see (same fail-loud posture as _flush_batch)."""
|
|
152
|
+
monkeypatch.setenv("AGENTX_EVAL_QUIET", "1")
|
|
153
|
+
client = FakeClient()
|
|
154
|
+
|
|
155
|
+
def failing_finalize(run_id):
|
|
156
|
+
raise RuntimeError("engine down")
|
|
157
|
+
|
|
158
|
+
client.finalize_run = failing_finalize
|
|
159
|
+
ctx = make_context(client)
|
|
160
|
+
|
|
161
|
+
with pytest.raises(RuntimeError, match="engine down"):
|
|
162
|
+
ctx.finalize()
|
|
@@ -198,13 +198,37 @@ def test_the_synchronous_fallback_analyze_is_never_retried():
|
|
|
198
198
|
|
|
199
199
|
session.request = always_times_out
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
# The timeout keeps its type (requests.Timeout, not a generic wrapper) so callers can
|
|
202
|
+
# branch on "the engine may still be doing the billable work" - see _request.
|
|
203
|
+
with pytest.raises(requests.Timeout):
|
|
202
204
|
client.analyze_run(RUN)
|
|
203
205
|
|
|
204
206
|
dashboard_posts = [u for u in session.urls("POST") if "/evaluate/analyze/" in u]
|
|
205
207
|
assert len(dashboard_posts) == 1, f"retried a billable request: {dashboard_posts}"
|
|
206
208
|
|
|
207
209
|
|
|
210
|
+
def test_append_results_timeout_propagates_as_timeout_with_one_post():
|
|
211
|
+
"""A read timeout on the batch-scoring POST must surface as requests.Timeout (the type
|
|
212
|
+
runner._flush_batch's double-billing guard catches - wrapping it in
|
|
213
|
+
AgentXEvaluationsError made that guard dead code) and must not be re-POSTed by the
|
|
214
|
+
transport while the engine may still be scoring the first submission."""
|
|
215
|
+
import requests
|
|
216
|
+
|
|
217
|
+
client, session = make_client({})
|
|
218
|
+
|
|
219
|
+
def always_times_out(method, url, **kwargs):
|
|
220
|
+
session.calls.append((method, url, kwargs))
|
|
221
|
+
raise requests.exceptions.ReadTimeout("still scoring")
|
|
222
|
+
|
|
223
|
+
session.request = always_times_out
|
|
224
|
+
|
|
225
|
+
with pytest.raises(requests.Timeout):
|
|
226
|
+
client.append_results(RUN, "batch-1", [])
|
|
227
|
+
|
|
228
|
+
result_posts = [u for u in session.urls("POST") if u.endswith(f"/runs/{RUN}/results")]
|
|
229
|
+
assert len(result_posts) == 1, f"re-POSTed a batch mid-scoring: {result_posts}"
|
|
230
|
+
|
|
231
|
+
|
|
208
232
|
def test_the_fallback_request_gets_the_long_analysis_timeout():
|
|
209
233
|
client, session = make_client(
|
|
210
234
|
{("POST", f"{API_ROOT}/evaluate/analyze/{RUN}"): FakeResponse(200, {"status": "completed"})}
|
|
@@ -666,17 +666,64 @@ def test_trace_retrieval_emits_real_child_span():
|
|
|
666
666
|
assert child["output"] == "3 matching docs"
|
|
667
667
|
|
|
668
668
|
|
|
669
|
-
def
|
|
670
|
-
"""No enclosing `with tracer.trace()`
|
|
671
|
-
|
|
672
|
-
|
|
669
|
+
def test_record_retrieval_with_no_active_span_queues_onto_next_trace():
|
|
670
|
+
"""No enclosing `with tracer.trace()` - record_retrieval queues onto the tracer-level
|
|
671
|
+
pending list and rides the next trace's performance_summary.retrieval_steps (the shape
|
|
672
|
+
the engine's retrieval-context extraction reads), carrying doc_count along."""
|
|
673
673
|
tracer = make_tracer()
|
|
674
|
-
tracer.record_retrieval("orphan_search", query="x", output="y")
|
|
674
|
+
tracer.record_retrieval("orphan_search", query="x", output="y", doc_count=4)
|
|
675
675
|
with tracer.trace("agent"):
|
|
676
676
|
pass
|
|
677
677
|
wires = enqueued_wires(tracer)
|
|
678
678
|
assert len(wires) == 1
|
|
679
679
|
assert wires[0]["name"] == "agent"
|
|
680
|
+
steps = wires[0]["performance_summary"]["retrieval_steps"]
|
|
681
|
+
assert [s["name"] for s in steps] == ["orphan_search"]
|
|
682
|
+
assert steps[0]["doc_count"] == 4
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def test_record_retrieval_carries_doc_count_into_child_span_metadata():
|
|
686
|
+
tracer = make_tracer()
|
|
687
|
+
with tracer.trace("agent"):
|
|
688
|
+
tracer.record_retrieval("kb_search", query="q", output="docs", doc_count=3)
|
|
689
|
+
wires = enqueued_wires(tracer)
|
|
690
|
+
child = wires[0]
|
|
691
|
+
assert child["name"] == "kb_search"
|
|
692
|
+
assert child["span_kind"] == "retrieval"
|
|
693
|
+
assert child["metadata"] == {"kind": "retrieval", "doc_count": 3}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_record_memory_with_no_active_span_drops_instead_of_queueing():
|
|
697
|
+
"""Memory must never ride the pending retrieval queue: it lands in
|
|
698
|
+
performance_summary.retrieval_steps, which feeds the engine's retrieval-context
|
|
699
|
+
extraction for RAG judges - recalled state is not knowledge grounding. With no active
|
|
700
|
+
span the record is dropped (debug-logged), not attached to the next trace."""
|
|
701
|
+
tracer = make_tracer()
|
|
702
|
+
tracer.record_memory("orphan prefs", operation="read", query="u-1", output="secret")
|
|
703
|
+
with tracer.trace("agent"):
|
|
704
|
+
pass
|
|
705
|
+
wires = enqueued_wires(tracer)
|
|
706
|
+
assert len(wires) == 1
|
|
707
|
+
assert wires[0]["name"] == "agent"
|
|
708
|
+
assert_no_performance_summary(wires)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def test_merge_child_run_execution_steps_honor_their_stated_kind():
|
|
712
|
+
"""crewai.py stamps its task steps "kind": "agent" - the merge loop must forward that
|
|
713
|
+
instead of unconditionally stamping every step "llm" (autogen/llamaindex steps carry no
|
|
714
|
+
kind and keep classifying as llm)."""
|
|
715
|
+
tracer = make_tracer()
|
|
716
|
+
with tracer.trace("crew") as span:
|
|
717
|
+
span._merge_child_run(
|
|
718
|
+
execution_steps=[
|
|
719
|
+
{"name": "Research task", "duration_ms": 5, "kind": "agent"},
|
|
720
|
+
{"name": "LLM Call 1", "duration_ms": 5},
|
|
721
|
+
],
|
|
722
|
+
framework="crewai",
|
|
723
|
+
)
|
|
724
|
+
wires = enqueued_wires(tracer)
|
|
725
|
+
kinds = {w["name"]: w.get("span_kind") for w in wires if w["name"] != "crew"}
|
|
726
|
+
assert kinds == {"Research task": "agent", "LLM Call 1": "llm"}
|
|
680
727
|
|
|
681
728
|
|
|
682
729
|
def test_record_tool_call_with_no_active_span_still_queues():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|