atif-sql 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/step.py
|
|
3
|
+
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
|
|
4
|
+
# from these header lines and the import path, which reads
|
|
5
|
+
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
|
|
6
|
+
"""Step model for ATIF trajectories."""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
12
|
+
|
|
13
|
+
from atif_converter.domain.atif.content import ContentPart
|
|
14
|
+
from atif_converter.domain.atif.metrics import Metrics
|
|
15
|
+
from atif_converter.domain.atif.observation import Observation
|
|
16
|
+
from atif_converter.domain.atif.tool_call import ToolCall
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Step(BaseModel):
|
|
20
|
+
"""A single step in the trajectory."""
|
|
21
|
+
|
|
22
|
+
step_id: int = Field(
|
|
23
|
+
default=...,
|
|
24
|
+
ge=1,
|
|
25
|
+
description="Ordinal index of the turn (starting from 1)",
|
|
26
|
+
)
|
|
27
|
+
timestamp: str | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
description="ISO 8601 timestamp indicating when this step occurred",
|
|
30
|
+
)
|
|
31
|
+
source: Literal["system", "user", "agent"] = Field(
|
|
32
|
+
default=...,
|
|
33
|
+
description="The originator of this step",
|
|
34
|
+
)
|
|
35
|
+
model_name: str | None = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description=(
|
|
38
|
+
"The specific LLM model used for this turn. Omission implies the model "
|
|
39
|
+
"defined in the root-level agent config."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
reasoning_effort: str | float | None = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="Qualitative or quantitative measure of effort",
|
|
45
|
+
)
|
|
46
|
+
message: str | list[ContentPart] = Field(
|
|
47
|
+
default=...,
|
|
48
|
+
description=(
|
|
49
|
+
"The dialogue message. String for text-only content, or array of "
|
|
50
|
+
"ContentPart for multimodal content (added in ATIF-v1.6)."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
reasoning_content: str | None = Field(
|
|
54
|
+
default=None,
|
|
55
|
+
description="The agent's explicit internal reasoning",
|
|
56
|
+
)
|
|
57
|
+
tool_calls: list[ToolCall] | None = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Array of structured objects for the agent's actions",
|
|
60
|
+
)
|
|
61
|
+
observation: Observation | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Environment feedback/result after actions or system events",
|
|
64
|
+
)
|
|
65
|
+
metrics: Metrics | None = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="LLM operational and confidence data for this step",
|
|
68
|
+
)
|
|
69
|
+
is_copied_context: bool | None = Field(
|
|
70
|
+
default=None,
|
|
71
|
+
description=(
|
|
72
|
+
"Indicates whether this step was copied from a previous trajectory "
|
|
73
|
+
"for context (e.g., during continuation after summarization). "
|
|
74
|
+
"Steps marked as copied context should not be included in training data "
|
|
75
|
+
"as they represent previously-trained interactions. "
|
|
76
|
+
"Added in ATIF-v1.5."
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
llm_call_count: int | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
ge=0,
|
|
82
|
+
description=(
|
|
83
|
+
"Number of LLM inferences this step represents. When >1, metrics are "
|
|
84
|
+
"aggregated across multiple LLM calls. When 1, the step represents exactly "
|
|
85
|
+
"one inference. When 0 on a `source: 'agent'` step, the step represents a "
|
|
86
|
+
"deterministic (non-LLM) dispatch; `metrics` and `reasoning_content` MUST "
|
|
87
|
+
"be absent in that case. When null, the producer did not track this "
|
|
88
|
+
"(backward-compatible default). Added in ATIF-v1.7."
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
extra: dict[str, Any] | None = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Custom step-level metadata",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
model_config = {"extra": "forbid"}
|
|
97
|
+
|
|
98
|
+
@field_validator("timestamp")
|
|
99
|
+
@classmethod
|
|
100
|
+
def validate_timestamp(cls, v: str | None) -> str | None:
|
|
101
|
+
"""Validate that timestamp is a valid ISO 8601 string."""
|
|
102
|
+
if v is not None:
|
|
103
|
+
try:
|
|
104
|
+
datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
raise ValueError(f"Invalid ISO 8601 timestamp: {e}")
|
|
107
|
+
return v
|
|
108
|
+
|
|
109
|
+
@model_validator(mode="after")
|
|
110
|
+
def validate_agent_only_fields(self) -> "Step":
|
|
111
|
+
"""Validate that certain fields are only present for agent steps."""
|
|
112
|
+
if self.source != "agent":
|
|
113
|
+
agent_only_fields = [
|
|
114
|
+
"model_name",
|
|
115
|
+
"reasoning_effort",
|
|
116
|
+
"reasoning_content",
|
|
117
|
+
"tool_calls",
|
|
118
|
+
"metrics",
|
|
119
|
+
]
|
|
120
|
+
for field in agent_only_fields:
|
|
121
|
+
if getattr(self, field) is not None:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Field '{field}' is only applicable when source is 'agent', "
|
|
124
|
+
f"but source is '{self.source}'"
|
|
125
|
+
)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
@model_validator(mode="after")
|
|
129
|
+
def validate_llm_call_count_zero_fields(self) -> "Step":
|
|
130
|
+
"""Enforce ATIF v1.7 no-LLM orchestration rule.
|
|
131
|
+
|
|
132
|
+
When ``llm_call_count == 0`` on a ``source: "agent"`` step, the step
|
|
133
|
+
represents a deterministic (non-LLM) dispatch. LLM-specific fields
|
|
134
|
+
(``metrics``, ``reasoning_content``) MUST be absent on such steps.
|
|
135
|
+
"""
|
|
136
|
+
if self.llm_call_count == 0 and self.source == "agent":
|
|
137
|
+
llm_only_fields = ["metrics", "reasoning_content"]
|
|
138
|
+
for field in llm_only_fields:
|
|
139
|
+
if getattr(self, field) is not None:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Field '{field}' must be absent when llm_call_count is 0 "
|
|
142
|
+
f"(deterministic dispatch on a 'source: agent' step)"
|
|
143
|
+
)
|
|
144
|
+
return self
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/subagent_trajectory_ref.py
|
|
3
|
+
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
|
|
4
|
+
# from these header lines and the import path, which reads
|
|
5
|
+
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
|
|
6
|
+
"""Subagent trajectory reference model for ATIF trajectories."""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, model_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubagentTrajectoryRef(BaseModel):
|
|
14
|
+
"""Reference to a delegated subagent trajectory.
|
|
15
|
+
|
|
16
|
+
A subagent reference is resolved by one of two mechanisms:
|
|
17
|
+
|
|
18
|
+
1. **Embedded form** — set `trajectory_id` to match the
|
|
19
|
+
`Trajectory.trajectory_id` of an entry in the parent's
|
|
20
|
+
`subagent_trajectories` array.
|
|
21
|
+
2. **File-ref form** — set `trajectory_path` to the location
|
|
22
|
+
(file path, S3 URL, etc.) of an external trajectory file.
|
|
23
|
+
|
|
24
|
+
These two mechanisms are the only resolution keys. `session_id`, when
|
|
25
|
+
present on the ref, is **informational only**: it records the run
|
|
26
|
+
identity of the delegated subagent for debug / correlation / cross-
|
|
27
|
+
trajectory search purposes, and MUST NOT be used as a matching key
|
|
28
|
+
(it is run-scoped and MAY collide across siblings — see
|
|
29
|
+
`Trajectory.session_id`). A ref therefore MUST set at least one of
|
|
30
|
+
`trajectory_id` or `trajectory_path`; `session_id` alone is not a
|
|
31
|
+
resolvable reference.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
trajectory_id: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description=(
|
|
37
|
+
"Canonical identifier of the delegated subagent trajectory. "
|
|
38
|
+
"Matches `Trajectory.trajectory_id` of an entry in the parent's "
|
|
39
|
+
"`subagent_trajectories` array and is the resolution key for "
|
|
40
|
+
"embedded references. Added in ATIF-v1.7 to provide a document-"
|
|
41
|
+
"unique matching key without overloading `session_id`."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
session_id: str | None = Field(
|
|
45
|
+
default=None,
|
|
46
|
+
description=(
|
|
47
|
+
"Run identity of the delegated subagent trajectory. Informational "
|
|
48
|
+
"only: recorded so consumers can correlate this ref back to the "
|
|
49
|
+
"subagent's run for debug / search / display purposes. Run-scoped "
|
|
50
|
+
"(see `Trajectory.session_id`) and therefore NOT a valid "
|
|
51
|
+
"resolution key — consumers MUST resolve via `trajectory_id` "
|
|
52
|
+
"(embedded) or `trajectory_path` (external file)."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
trajectory_path: str | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description=(
|
|
58
|
+
"Location of the complete subagent trajectory as an external "
|
|
59
|
+
"file (file path, S3 URL, database reference, etc.). Resolution "
|
|
60
|
+
"key for file-ref references. When both `trajectory_id` and "
|
|
61
|
+
"`trajectory_path` are set, consumers MAY choose either; "
|
|
62
|
+
"typically `trajectory_id` is preferred when the embedded "
|
|
63
|
+
"trajectory is available in-memory."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
extra: dict[str, Any] | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Custom metadata about the subagent execution",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
model_config = {"extra": "forbid"}
|
|
72
|
+
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def validate_is_resolvable(self) -> "SubagentTrajectoryRef":
|
|
75
|
+
"""A ref must be resolvable: set `trajectory_id` or `trajectory_path`.
|
|
76
|
+
|
|
77
|
+
`session_id` alone is not sufficient because it is run-scoped and
|
|
78
|
+
MAY collide across siblings (see `Trajectory.session_id`), so it
|
|
79
|
+
cannot unambiguously identify which subagent trajectory a ref
|
|
80
|
+
points at.
|
|
81
|
+
"""
|
|
82
|
+
if self.trajectory_id is None and self.trajectory_path is None:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"SubagentTrajectoryRef must be resolvable: set either "
|
|
85
|
+
"`trajectory_id` (for embedded references) or "
|
|
86
|
+
"`trajectory_path` (for external-file references). "
|
|
87
|
+
"`session_id` alone is not a resolution key — it is "
|
|
88
|
+
"run-scoped and may collide across siblings."
|
|
89
|
+
)
|
|
90
|
+
return self
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/tool_call.py
|
|
3
|
+
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
|
|
4
|
+
# from these header lines and the import path, which reads
|
|
5
|
+
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
|
|
6
|
+
"""Tool call model for ATIF trajectories."""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolCall(BaseModel):
|
|
14
|
+
"""A tool call within a step."""
|
|
15
|
+
|
|
16
|
+
tool_call_id: str = Field(
|
|
17
|
+
default=...,
|
|
18
|
+
description="Unique identifier for this specific tool call",
|
|
19
|
+
)
|
|
20
|
+
function_name: str = Field(
|
|
21
|
+
default=...,
|
|
22
|
+
description="The name of the function or tool being invoked",
|
|
23
|
+
)
|
|
24
|
+
arguments: dict[str, Any] = Field(
|
|
25
|
+
default=...,
|
|
26
|
+
description="Arguments passed to the function (can be empty dict)",
|
|
27
|
+
)
|
|
28
|
+
extra: dict[str, Any] | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description=(
|
|
31
|
+
"Custom tool-call-level metadata (e.g., timeout, retry count, tool version). "
|
|
32
|
+
"Added in ATIF-v1.7."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
model_config = {"extra": "forbid"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/trajectory.py
|
|
3
|
+
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
|
|
4
|
+
# from these header lines and the import path, which reads
|
|
5
|
+
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
|
|
6
|
+
"""Trajectory model for ATIF (Agent Trajectory Interchange Format)."""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, model_validator
|
|
11
|
+
|
|
12
|
+
from atif_converter.domain.atif.agent import Agent
|
|
13
|
+
from atif_converter.domain.atif.final_metrics import FinalMetrics
|
|
14
|
+
from atif_converter.domain.atif.step import Step
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Trajectory(BaseModel):
|
|
18
|
+
"""Agent Trajectory in ATIF (Agent Trajectory Interchange Format)."""
|
|
19
|
+
|
|
20
|
+
schema_version: Literal[
|
|
21
|
+
"ATIF-v1.0",
|
|
22
|
+
"ATIF-v1.1",
|
|
23
|
+
"ATIF-v1.2",
|
|
24
|
+
"ATIF-v1.3",
|
|
25
|
+
"ATIF-v1.4",
|
|
26
|
+
"ATIF-v1.5",
|
|
27
|
+
"ATIF-v1.6",
|
|
28
|
+
"ATIF-v1.7",
|
|
29
|
+
"ATIF-v1.8",
|
|
30
|
+
] = Field(
|
|
31
|
+
default="ATIF-v1.8",
|
|
32
|
+
description="String defining ATIF compatibility",
|
|
33
|
+
)
|
|
34
|
+
session_id: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description=(
|
|
37
|
+
"Identifier for the agent run this trajectory belongs to. Scoped "
|
|
38
|
+
"to the run, NOT to an individual trajectory document: multiple "
|
|
39
|
+
"Trajectory objects MAY share the same `session_id` when they "
|
|
40
|
+
"represent the same logical run (e.g., a parent trajectory and "
|
|
41
|
+
"its embedded subagents, or a trajectory and its continuation "
|
|
42
|
+
"segments linked via `continued_trajectory_ref`). Therefore "
|
|
43
|
+
"`session_id`s within a parent's `subagent_trajectories` array "
|
|
44
|
+
"are NOT required to be unique. Use `trajectory_id` when a "
|
|
45
|
+
"per-trajectory-document unique identifier is required (e.g., "
|
|
46
|
+
"for `SubagentTrajectoryRef` resolution). Optional since "
|
|
47
|
+
"ATIF-v1.7; producers SHOULD set this on root trajectories for "
|
|
48
|
+
"run-level traceability, and MAY omit it on embedded subagents "
|
|
49
|
+
"that inherit the parent's run identity."
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
trajectory_id: str | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description=(
|
|
55
|
+
"Canonical per-trajectory-document identifier, distinct from "
|
|
56
|
+
"`session_id`. Unlike `session_id` (which is run-scoped and MAY "
|
|
57
|
+
"be shared), `trajectory_id` uniquely identifies THIS trajectory "
|
|
58
|
+
"object. Used to resolve `SubagentTrajectoryRef` entries against "
|
|
59
|
+
"the root's `subagent_trajectories` array without overloading "
|
|
60
|
+
"`session_id`'s run-scoped semantics. Optional on standalone "
|
|
61
|
+
"trajectories, but REQUIRED on any trajectory embedded in a "
|
|
62
|
+
"parent's `subagent_trajectories` array. `trajectory_id`s within "
|
|
63
|
+
"a single parent's `subagent_trajectories` array MUST be unique. "
|
|
64
|
+
"Added in ATIF-v1.7."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
agent: Agent = Field(
|
|
68
|
+
default=...,
|
|
69
|
+
description="Object specifying the agent configuration",
|
|
70
|
+
)
|
|
71
|
+
steps: list[Step] = Field(
|
|
72
|
+
default=...,
|
|
73
|
+
min_length=1,
|
|
74
|
+
description="Array of step objects representing the complete interaction history",
|
|
75
|
+
)
|
|
76
|
+
notes: str | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
description="Custom information, design notes, or explanations",
|
|
79
|
+
)
|
|
80
|
+
final_metrics: FinalMetrics | None = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
description="Summary metrics for the entire trajectory",
|
|
83
|
+
)
|
|
84
|
+
continued_trajectory_ref: str | None = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="Reference to the continuation trajectory file if this trajectory is continued in another file",
|
|
87
|
+
)
|
|
88
|
+
extra: dict[str, Any] | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="Custom root-level metadata",
|
|
91
|
+
)
|
|
92
|
+
subagent_trajectories: list["Trajectory"] | None = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description=(
|
|
95
|
+
"Array of embedded subagent trajectories. Each element is a complete, "
|
|
96
|
+
"independently-valid ATIF Trajectory with its own schema_version, "
|
|
97
|
+
"agent, and step_id sequence starting at 1. Enables single-file "
|
|
98
|
+
"storage of multi-agent workflows: when a "
|
|
99
|
+
"SubagentTrajectoryRef.trajectory_path is null, consumers resolve "
|
|
100
|
+
"the reference by matching SubagentTrajectoryRef.trajectory_id "
|
|
101
|
+
"against Trajectory.trajectory_id of entries in this array. "
|
|
102
|
+
"Uniqueness rules: every embedded subagent MUST set `trajectory_id`, "
|
|
103
|
+
"and `trajectory_id`s within this array MUST be unique. "
|
|
104
|
+
"`session_id`, by contrast, is run-scoped and MAY collide across "
|
|
105
|
+
"siblings (or match the parent) when all trajectories belong to "
|
|
106
|
+
"the same logical run; embedded subagents MAY also omit "
|
|
107
|
+
"`session_id` entirely to inherit the parent's run identity. "
|
|
108
|
+
"Added in ATIF-v1.7."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
model_config = {"extra": "forbid"}
|
|
113
|
+
|
|
114
|
+
def to_json_dict(self, exclude_none: bool = True) -> dict[str, Any]:
|
|
115
|
+
"""Export trajectory to a dictionary suitable for JSON serialization.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
exclude_none: If True, exclude fields with None values from output.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary representation of the trajectory.
|
|
122
|
+
"""
|
|
123
|
+
return self.model_dump(exclude_none=exclude_none, mode="json")
|
|
124
|
+
|
|
125
|
+
@model_validator(mode="after")
|
|
126
|
+
def validate_step_ids(self) -> "Trajectory":
|
|
127
|
+
"""Validate that step_ids are sequential starting from 1."""
|
|
128
|
+
for i, step in enumerate(self.steps):
|
|
129
|
+
expected_step_id = i + 1
|
|
130
|
+
if step.step_id != expected_step_id:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"steps[{i}].step_id: expected {expected_step_id} "
|
|
133
|
+
f"(sequential from 1), got {step.step_id}"
|
|
134
|
+
)
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
@model_validator(mode="after")
|
|
138
|
+
def validate_embedded_subagent_trajectory_ids(self) -> "Trajectory":
|
|
139
|
+
"""Every embedded subagent must carry a unique, non-null `trajectory_id`.
|
|
140
|
+
|
|
141
|
+
Embedded subagents are resolved by matching
|
|
142
|
+
`SubagentTrajectoryRef.trajectory_id` against
|
|
143
|
+
`Trajectory.trajectory_id`, so the identifier must be present and
|
|
144
|
+
unique within a parent's `subagent_trajectories` array. Note that
|
|
145
|
+
no such constraint is placed on `session_id` — siblings MAY share
|
|
146
|
+
a `session_id` (or omit it entirely to inherit the parent's) when
|
|
147
|
+
they represent the same logical agent run.
|
|
148
|
+
"""
|
|
149
|
+
if not self.subagent_trajectories:
|
|
150
|
+
return self
|
|
151
|
+
seen: set[str] = set()
|
|
152
|
+
for i, sub in enumerate(self.subagent_trajectories):
|
|
153
|
+
if sub.trajectory_id is None:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"subagent_trajectories[{i}].trajectory_id is required "
|
|
156
|
+
f"for embedded subagents "
|
|
157
|
+
f"(agent.name={sub.agent.name!r}, "
|
|
158
|
+
f"session_id={sub.session_id!r})"
|
|
159
|
+
)
|
|
160
|
+
if sub.trajectory_id in seen:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"subagent_trajectories[{i}].trajectory_id "
|
|
163
|
+
f"{sub.trajectory_id!r} is not unique within "
|
|
164
|
+
f"subagent_trajectories"
|
|
165
|
+
)
|
|
166
|
+
seen.add(sub.trajectory_id)
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
@model_validator(mode="after")
|
|
170
|
+
def validate_tool_call_references(self) -> "Trajectory":
|
|
171
|
+
"""Validate that observation source_call_ids reference valid tool_call_ids."""
|
|
172
|
+
for step in self.steps:
|
|
173
|
+
if step.observation is None:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Collect all tool_call_ids from this step
|
|
177
|
+
tool_call_ids = set()
|
|
178
|
+
if step.tool_calls:
|
|
179
|
+
tool_call_ids = {tc.tool_call_id for tc in step.tool_calls}
|
|
180
|
+
|
|
181
|
+
# Check that source_call_ids reference valid tool_call_ids
|
|
182
|
+
for result in step.observation.results:
|
|
183
|
+
if result.source_call_id is not None:
|
|
184
|
+
if result.source_call_id not in tool_call_ids:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Observation result references source_call_id "
|
|
187
|
+
f"'{result.source_call_id}' which is not found in "
|
|
188
|
+
f"step {step.step_id}'s tool_calls"
|
|
189
|
+
)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def has_multimodal_content(self) -> bool:
|
|
193
|
+
"""Check if any step contains multimodal content (images or audio).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if any step message or observation contains a media content
|
|
197
|
+
part. Audio counts since ATIF-v1.8.
|
|
198
|
+
"""
|
|
199
|
+
media_types = {"image", "audio"}
|
|
200
|
+
for step in self.steps:
|
|
201
|
+
if isinstance(step.message, list):
|
|
202
|
+
for part in step.message:
|
|
203
|
+
if part.type in media_types:
|
|
204
|
+
return True
|
|
205
|
+
if step.observation:
|
|
206
|
+
for result in step.observation.results:
|
|
207
|
+
if isinstance(result.content, list):
|
|
208
|
+
for part in result.content:
|
|
209
|
+
if part.type in media_types:
|
|
210
|
+
return True
|
|
211
|
+
return False
|