asta-artifact 1.0.0__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.
- asta_artifact-1.0.0/.gitignore +136 -0
- asta_artifact-1.0.0/PKG-INFO +10 -0
- asta_artifact-1.0.0/pyproject.toml +26 -0
- asta_artifact-1.0.0/src/asta_artifact/__init__.py +60 -0
- asta_artifact-1.0.0/src/asta_artifact/artifact.py +281 -0
- asta_artifact-1.0.0/src/asta_artifact/cli.py +77 -0
- asta_artifact-1.0.0/src/asta_artifact/content.py +149 -0
- asta_artifact-1.0.0/src/asta_artifact/exporter.py +891 -0
- asta_artifact-1.0.0/src/asta_artifact/models.py +112 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# =========================
|
|
2
|
+
# OS / System files
|
|
3
|
+
# =========================
|
|
4
|
+
.DS_Store
|
|
5
|
+
.DS_Store?
|
|
6
|
+
._*
|
|
7
|
+
.Spotlight-V100
|
|
8
|
+
.Trashes
|
|
9
|
+
Thumbs.db
|
|
10
|
+
ehthumbs.db
|
|
11
|
+
Desktop.ini
|
|
12
|
+
|
|
13
|
+
# =========================
|
|
14
|
+
# Editor / IDE files
|
|
15
|
+
# =========================
|
|
16
|
+
.vscode/
|
|
17
|
+
.idea/
|
|
18
|
+
*.swp
|
|
19
|
+
*.swo
|
|
20
|
+
*.swn
|
|
21
|
+
*~
|
|
22
|
+
*.bak
|
|
23
|
+
*.tmp
|
|
24
|
+
*.orig
|
|
25
|
+
|
|
26
|
+
# =========================
|
|
27
|
+
# Logs
|
|
28
|
+
# =========================
|
|
29
|
+
logs/
|
|
30
|
+
*.log
|
|
31
|
+
npm-debug.log*
|
|
32
|
+
yarn-debug.log*
|
|
33
|
+
yarn-error.log*
|
|
34
|
+
pnpm-debug.log*
|
|
35
|
+
|
|
36
|
+
# =========================
|
|
37
|
+
# Environment variables / secrets
|
|
38
|
+
# =========================
|
|
39
|
+
.env
|
|
40
|
+
.env.*
|
|
41
|
+
!.env.example
|
|
42
|
+
*.pem
|
|
43
|
+
*.key
|
|
44
|
+
*.crt
|
|
45
|
+
|
|
46
|
+
# =========================
|
|
47
|
+
# Build / dist artifacts
|
|
48
|
+
# =========================
|
|
49
|
+
dist/
|
|
50
|
+
build/
|
|
51
|
+
out/
|
|
52
|
+
target/
|
|
53
|
+
bin/
|
|
54
|
+
obj/
|
|
55
|
+
|
|
56
|
+
# =========================
|
|
57
|
+
# Dependency directories
|
|
58
|
+
# =========================
|
|
59
|
+
node_modules/
|
|
60
|
+
vendor/
|
|
61
|
+
.venv/
|
|
62
|
+
venv/
|
|
63
|
+
env/
|
|
64
|
+
__pycache__/
|
|
65
|
+
|
|
66
|
+
# Yarn
|
|
67
|
+
**/.yarn/*
|
|
68
|
+
!.yarn/patches
|
|
69
|
+
!.yarn/plugins
|
|
70
|
+
!.yarn/releases
|
|
71
|
+
!.yarn/sdks
|
|
72
|
+
!.yarn/versions
|
|
73
|
+
.pnp.*
|
|
74
|
+
|
|
75
|
+
# =========================
|
|
76
|
+
# Test / coverage output
|
|
77
|
+
# =========================
|
|
78
|
+
coverage/
|
|
79
|
+
.coverage
|
|
80
|
+
.nyc_output/
|
|
81
|
+
test-results/
|
|
82
|
+
pytest_cache/
|
|
83
|
+
|
|
84
|
+
# =========================
|
|
85
|
+
# Temporary / cache files
|
|
86
|
+
# =========================
|
|
87
|
+
.cache/
|
|
88
|
+
.tmp/
|
|
89
|
+
.temp/
|
|
90
|
+
*.cache
|
|
91
|
+
|
|
92
|
+
# =========================
|
|
93
|
+
# Archive files
|
|
94
|
+
# =========================
|
|
95
|
+
*.zip
|
|
96
|
+
*.tar
|
|
97
|
+
*.tar.gz
|
|
98
|
+
*.rar
|
|
99
|
+
*.7z
|
|
100
|
+
|
|
101
|
+
# =========================
|
|
102
|
+
# Runtime files
|
|
103
|
+
# =========================
|
|
104
|
+
*.pid
|
|
105
|
+
*.seed
|
|
106
|
+
*.pid.lock
|
|
107
|
+
|
|
108
|
+
# =========================
|
|
109
|
+
# Misc
|
|
110
|
+
# =========================
|
|
111
|
+
*.lock
|
|
112
|
+
!.gitignore
|
|
113
|
+
|
|
114
|
+
# =========================
|
|
115
|
+
# Terraform
|
|
116
|
+
# =========================
|
|
117
|
+
*.tfstate
|
|
118
|
+
*.tfstate.*
|
|
119
|
+
.terraform/
|
|
120
|
+
.terraform.lock.hcl
|
|
121
|
+
infra/terraform.tfvars
|
|
122
|
+
|
|
123
|
+
# =========================
|
|
124
|
+
# Generated deploy files
|
|
125
|
+
# =========================
|
|
126
|
+
_asta_modal_*.py
|
|
127
|
+
|
|
128
|
+
# =========================
|
|
129
|
+
# Python packaging
|
|
130
|
+
# =========================
|
|
131
|
+
*.egg-info/
|
|
132
|
+
|
|
133
|
+
# =========================
|
|
134
|
+
# Claude Code worktrees
|
|
135
|
+
# =========================
|
|
136
|
+
.claude/worktrees/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asta-artifact
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Asta artifact builder, models, content blocks, and HTML exporter
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: asta-types
|
|
7
|
+
Requires-Dist: markdown>=3.4
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Provides-Extra: cli
|
|
10
|
+
Requires-Dist: click>=8.0; extra == 'cli'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "asta-artifact"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Asta artifact builder, models, content blocks, and HTML exporter"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"markdown>=3.4",
|
|
12
|
+
"pydantic>=2.0",
|
|
13
|
+
"asta-types",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.uv.sources]
|
|
17
|
+
asta-types = { path = "../types" }
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
cli = ["click>=8.0"]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
artifacts = "asta_artifact.cli:main"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/asta_artifact"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
_AstaModel,
|
|
3
|
+
AnnotationType,
|
|
4
|
+
S2Author,
|
|
5
|
+
ContentBlockType,
|
|
6
|
+
DisplayText,
|
|
7
|
+
EntityType,
|
|
8
|
+
Facet,
|
|
9
|
+
Paper,
|
|
10
|
+
S2Paper,
|
|
11
|
+
Snippet,
|
|
12
|
+
paper_tag,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .content import (
|
|
16
|
+
Code,
|
|
17
|
+
Figure,
|
|
18
|
+
Markdown,
|
|
19
|
+
Section,
|
|
20
|
+
SectionContext,
|
|
21
|
+
Sections,
|
|
22
|
+
SectionsContext,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .artifact import Artifact
|
|
26
|
+
from .exporter import (
|
|
27
|
+
ArtifactExporter,
|
|
28
|
+
export_artifact_to_html,
|
|
29
|
+
export_artifacts_to_html_directory,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Enums
|
|
34
|
+
"EntityType",
|
|
35
|
+
"AnnotationType",
|
|
36
|
+
"ContentBlockType",
|
|
37
|
+
# Entity models
|
|
38
|
+
"S2Author",
|
|
39
|
+
"S2Paper",
|
|
40
|
+
"Paper",
|
|
41
|
+
# Annotation models
|
|
42
|
+
"Snippet",
|
|
43
|
+
"Facet",
|
|
44
|
+
"DisplayText",
|
|
45
|
+
"paper_tag",
|
|
46
|
+
# Content blocks
|
|
47
|
+
"Section",
|
|
48
|
+
"Sections",
|
|
49
|
+
"Markdown",
|
|
50
|
+
"Figure",
|
|
51
|
+
"Code",
|
|
52
|
+
"SectionContext",
|
|
53
|
+
"SectionsContext",
|
|
54
|
+
# Builder
|
|
55
|
+
"Artifact",
|
|
56
|
+
# Exporter
|
|
57
|
+
"ArtifactExporter",
|
|
58
|
+
"export_artifact_to_html",
|
|
59
|
+
"export_artifacts_to_html_directory",
|
|
60
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Artifact builder producing canonical format::
|
|
2
|
+
|
|
3
|
+
{
|
|
4
|
+
"id": "...", "name": "...", "description": "...",
|
|
5
|
+
"entities": {"paper-1": {id, type, displayLabel, s2Metadata}},
|
|
6
|
+
"annotations": {"ann-1": {id, type, entityId, text, ...}},
|
|
7
|
+
"content": [{id, type, title, childIds}, {id, type, text}, ...]
|
|
8
|
+
}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .models import (
|
|
19
|
+
Annotation,
|
|
20
|
+
AnnotationType,
|
|
21
|
+
DisplayText,
|
|
22
|
+
EntityType,
|
|
23
|
+
Facet,
|
|
24
|
+
Paper,
|
|
25
|
+
S2Paper,
|
|
26
|
+
Snippet,
|
|
27
|
+
)
|
|
28
|
+
from .content import (
|
|
29
|
+
ContentBlock,
|
|
30
|
+
Markdown,
|
|
31
|
+
Section,
|
|
32
|
+
SectionContext,
|
|
33
|
+
Sections,
|
|
34
|
+
SectionsContext,
|
|
35
|
+
content_block_from_dict,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Artifact:
|
|
40
|
+
"""Asta artifact with structured content, annotations, and entities."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, artifact_id: str, name: str,
|
|
43
|
+
description: str | None = None):
|
|
44
|
+
if not artifact_id:
|
|
45
|
+
raise ValueError("artifact_id is required")
|
|
46
|
+
if not name:
|
|
47
|
+
raise ValueError("name is required")
|
|
48
|
+
|
|
49
|
+
self._id = artifact_id
|
|
50
|
+
self._name = name
|
|
51
|
+
self._description = description
|
|
52
|
+
|
|
53
|
+
self._content: list[ContentBlock] = []
|
|
54
|
+
self._entities: dict[str, Paper] = {}
|
|
55
|
+
self._annotations: dict[str, Annotation] = {}
|
|
56
|
+
|
|
57
|
+
self._content_counter = 0
|
|
58
|
+
self._entity_counter = 0
|
|
59
|
+
self._annotation_counter = 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def id(self) -> str:
|
|
63
|
+
return self._id
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def name(self) -> str:
|
|
67
|
+
return self._name
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def description(self) -> str | None:
|
|
71
|
+
return self._description
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def content(self) -> list[ContentBlock]:
|
|
75
|
+
return self._content
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def entities(self) -> dict[str, Paper]:
|
|
79
|
+
return self._entities
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def annotations(self) -> dict[str, Annotation]:
|
|
83
|
+
return self._annotations
|
|
84
|
+
|
|
85
|
+
def _next_content_id(self) -> str:
|
|
86
|
+
self._content_counter += 1
|
|
87
|
+
return f"c_{self._content_counter:03d}"
|
|
88
|
+
|
|
89
|
+
def _next_entity_id(self) -> str:
|
|
90
|
+
self._entity_counter += 1
|
|
91
|
+
return f"ent_{self._entity_counter:03d}"
|
|
92
|
+
|
|
93
|
+
def _next_annotation_id(self) -> str:
|
|
94
|
+
self._annotation_counter += 1
|
|
95
|
+
return f"ann_{self._annotation_counter:03d}"
|
|
96
|
+
|
|
97
|
+
def _update_counter(self, counter_attr: str, id_str: str) -> None:
|
|
98
|
+
"""Update a counter to avoid ID collisions after loading."""
|
|
99
|
+
try:
|
|
100
|
+
num = int(id_str.split("_")[1])
|
|
101
|
+
setattr(self, counter_attr, max(getattr(self, counter_attr), num))
|
|
102
|
+
except (IndexError, ValueError):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# --- Entity Methods ---
|
|
106
|
+
|
|
107
|
+
def add_paper_entity(self, display_label: str,
|
|
108
|
+
s2_metadata: S2Paper | dict | None = None,
|
|
109
|
+
entity_id: str | None = None) -> str:
|
|
110
|
+
"""Add a Paper entity and return its ID."""
|
|
111
|
+
entity_id = entity_id or self._next_entity_id()
|
|
112
|
+
if isinstance(s2_metadata, dict):
|
|
113
|
+
s2_metadata = S2Paper.model_validate(s2_metadata)
|
|
114
|
+
entity = Paper(
|
|
115
|
+
id=entity_id,
|
|
116
|
+
display_label=display_label,
|
|
117
|
+
s2_metadata=s2_metadata,
|
|
118
|
+
)
|
|
119
|
+
self._entities[entity_id] = entity
|
|
120
|
+
return entity_id
|
|
121
|
+
|
|
122
|
+
# --- Annotation Methods ---
|
|
123
|
+
|
|
124
|
+
def add_snippet(self, entity_id: str, text: str = "",
|
|
125
|
+
heading: str | None = None,
|
|
126
|
+
sentence_id: str | None = None) -> str:
|
|
127
|
+
"""Add a Snippet annotation and return its ID.
|
|
128
|
+
|
|
129
|
+
To link the snippet to a content block, append the returned ID
|
|
130
|
+
to the block's ``annotation_ids`` list.
|
|
131
|
+
"""
|
|
132
|
+
ann_id = self._next_annotation_id()
|
|
133
|
+
ann = Snippet(
|
|
134
|
+
id=ann_id, entity_id=entity_id,
|
|
135
|
+
text=text, heading=heading, sentence_id=sentence_id,
|
|
136
|
+
)
|
|
137
|
+
self._annotations[ann_id] = ann
|
|
138
|
+
return ann_id
|
|
139
|
+
|
|
140
|
+
def add_facet(self, entity_id: str, name: str = "",
|
|
141
|
+
value: str | int | float | bool = "") -> str:
|
|
142
|
+
"""Add a Facet annotation and return its ID."""
|
|
143
|
+
ann_id = self._next_annotation_id()
|
|
144
|
+
ann = Facet(
|
|
145
|
+
id=ann_id, entity_id=entity_id,
|
|
146
|
+
name=name, value=value,
|
|
147
|
+
)
|
|
148
|
+
self._annotations[ann_id] = ann
|
|
149
|
+
return ann_id
|
|
150
|
+
|
|
151
|
+
def add_display_text(self, entity_id: str, text: str = "") -> str:
|
|
152
|
+
"""Add a DisplayText annotation and return its ID."""
|
|
153
|
+
ann_id = self._next_annotation_id()
|
|
154
|
+
ann = DisplayText(
|
|
155
|
+
id=ann_id, entity_id=entity_id,
|
|
156
|
+
text=text,
|
|
157
|
+
)
|
|
158
|
+
self._annotations[ann_id] = ann
|
|
159
|
+
return ann_id
|
|
160
|
+
|
|
161
|
+
# --- Content Methods ---
|
|
162
|
+
|
|
163
|
+
def markdown(self, text: str) -> Markdown:
|
|
164
|
+
"""Add a top-level markdown block (not inside a section)."""
|
|
165
|
+
content_id = self._next_content_id()
|
|
166
|
+
block = Markdown(id=content_id, text=text)
|
|
167
|
+
self._content.append(block)
|
|
168
|
+
return block
|
|
169
|
+
|
|
170
|
+
def table(self, headers: list[str], rows: list[list[str]]) -> Markdown:
|
|
171
|
+
"""Add a top-level markdown table."""
|
|
172
|
+
from .content import SectionContext
|
|
173
|
+
# Reuse SectionContext's table logic without a section
|
|
174
|
+
def _esc(v: str) -> str:
|
|
175
|
+
return str(v).replace("|", "\\|").replace("\n", " ")
|
|
176
|
+
lines = [
|
|
177
|
+
"| " + " | ".join(headers) + " |",
|
|
178
|
+
"| " + " | ".join("---" for _ in headers) + " |",
|
|
179
|
+
]
|
|
180
|
+
for row in rows:
|
|
181
|
+
lines.append("| " + " | ".join(_esc(v) for v in row) + " |")
|
|
182
|
+
return self.markdown("\n".join(lines))
|
|
183
|
+
|
|
184
|
+
@contextmanager
|
|
185
|
+
def section(self, title: str, description: str | None = None,
|
|
186
|
+
section_id: str | None = None):
|
|
187
|
+
"""Create a section and yield a SectionContext for adding content."""
|
|
188
|
+
content_id = section_id or self._next_content_id()
|
|
189
|
+
sec = Section(id=content_id, title=title, description=description)
|
|
190
|
+
self._content.append(sec)
|
|
191
|
+
ctx = SectionContext(self, sec)
|
|
192
|
+
yield ctx
|
|
193
|
+
|
|
194
|
+
@contextmanager
|
|
195
|
+
def sections(self, title: str):
|
|
196
|
+
"""Create a SECTIONS container and yield a SectionsContext."""
|
|
197
|
+
content_id = self._next_content_id()
|
|
198
|
+
block = Sections(id=content_id, title=title)
|
|
199
|
+
self._content.append(block)
|
|
200
|
+
ctx = SectionsContext(self, block)
|
|
201
|
+
yield ctx
|
|
202
|
+
|
|
203
|
+
# --- Export ---
|
|
204
|
+
|
|
205
|
+
def to_dict(self) -> dict:
|
|
206
|
+
"""Export as a dictionary matching the canonical schema."""
|
|
207
|
+
result: dict[str, Any] = {
|
|
208
|
+
"id": self._id,
|
|
209
|
+
"name": self._name,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if self._description:
|
|
213
|
+
result["description"] = self._description
|
|
214
|
+
|
|
215
|
+
result["entities"] = {
|
|
216
|
+
eid: e.to_dict() for eid, e in self._entities.items()
|
|
217
|
+
}
|
|
218
|
+
result["annotations"] = {
|
|
219
|
+
aid: a.to_dict() for aid, a in self._annotations.items()
|
|
220
|
+
}
|
|
221
|
+
result["content"] = [block.to_dict() for block in self._content]
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
def to_json(self, indent: int = 2) -> str:
|
|
226
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
227
|
+
|
|
228
|
+
# --- Load from existing ---
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def load(cls, path: str | Path) -> Artifact:
|
|
232
|
+
"""Load an artifact from a JSON file."""
|
|
233
|
+
path = Path(path)
|
|
234
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
235
|
+
data = json.load(f)
|
|
236
|
+
return cls.from_dict(data)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def from_dict(cls, data: dict) -> Artifact:
|
|
240
|
+
"""Create an Artifact from a canonical dictionary or A2A envelope."""
|
|
241
|
+
# Unwrap A2A envelope if present
|
|
242
|
+
if "artifactId" in data and "parts" in data:
|
|
243
|
+
part = data["parts"][0]
|
|
244
|
+
data = part["data"] if isinstance(part, dict) else part.root.data
|
|
245
|
+
builder = cls(
|
|
246
|
+
artifact_id=data["id"],
|
|
247
|
+
name=data["name"],
|
|
248
|
+
description=data.get("description"),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Load content
|
|
252
|
+
for block_data in data.get("content", []):
|
|
253
|
+
block = content_block_from_dict(block_data)
|
|
254
|
+
builder._content.append(block)
|
|
255
|
+
builder._update_counter("_content_counter", block.id)
|
|
256
|
+
|
|
257
|
+
# Load entities
|
|
258
|
+
for entity_id, entity_data in data.get("entities", {}).items():
|
|
259
|
+
entity_type = entity_data.get("type")
|
|
260
|
+
if entity_type == EntityType.PAPER.value:
|
|
261
|
+
entity = Paper.from_dict(entity_data)
|
|
262
|
+
else:
|
|
263
|
+
raise ValueError(f"Unknown entity type: {entity_type}")
|
|
264
|
+
builder._entities[entity_id] = entity
|
|
265
|
+
builder._update_counter("_entity_counter", entity_id)
|
|
266
|
+
|
|
267
|
+
# Load annotations
|
|
268
|
+
for ann_id, ann_data in data.get("annotations", {}).items():
|
|
269
|
+
ann_type = ann_data.get("type")
|
|
270
|
+
if ann_type == AnnotationType.SNIPPET.value:
|
|
271
|
+
ann = Snippet.from_dict(ann_data)
|
|
272
|
+
elif ann_type == AnnotationType.FACET.value:
|
|
273
|
+
ann = Facet.from_dict(ann_data)
|
|
274
|
+
elif ann_type == AnnotationType.DISPLAY_TEXT.value:
|
|
275
|
+
ann = DisplayText.from_dict(ann_data)
|
|
276
|
+
else:
|
|
277
|
+
raise ValueError(f"Unknown annotation type: {ann_type}")
|
|
278
|
+
builder._annotations[ann_id] = ann
|
|
279
|
+
builder._update_counter("_annotation_counter", ann_id)
|
|
280
|
+
|
|
281
|
+
return builder
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Artifacts CLI — export A2A task artifacts to various formats.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
artifacts --input test-output/ --output export/
|
|
5
|
+
artifacts --input test-output/ --output export/ --format html
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from asta_artifact import Artifact
|
|
17
|
+
from asta_artifact.exporter import export_artifacts_to_html_directory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_artifacts_from_dir(input_dir: str) -> list[Artifact]:
|
|
21
|
+
"""Load artifacts from all JSON files in a directory."""
|
|
22
|
+
artifacts = []
|
|
23
|
+
for fname in sorted(os.listdir(input_dir)):
|
|
24
|
+
if not fname.endswith(".json"):
|
|
25
|
+
continue
|
|
26
|
+
path = os.path.join(input_dir, fname)
|
|
27
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
# Support both raw artifact dicts and A2A envelopes with parts
|
|
30
|
+
if "artifacts" in data:
|
|
31
|
+
for art_data in data["artifacts"]:
|
|
32
|
+
if "parts" in art_data:
|
|
33
|
+
for part in art_data["parts"]:
|
|
34
|
+
part_data = part.get("data") or part
|
|
35
|
+
if "entities" in part_data and "content" in part_data:
|
|
36
|
+
artifacts.append(Artifact.from_dict(part_data))
|
|
37
|
+
elif "entities" in art_data and "content" in art_data:
|
|
38
|
+
artifacts.append(Artifact.from_dict(art_data))
|
|
39
|
+
elif "parts" in data:
|
|
40
|
+
for part in data["parts"]:
|
|
41
|
+
part_data = part.get("data") or part
|
|
42
|
+
if "entities" in part_data and "content" in part_data:
|
|
43
|
+
artifacts.append(Artifact.from_dict(part_data))
|
|
44
|
+
elif "entities" in data and "content" in data:
|
|
45
|
+
artifacts.append(Artifact.from_dict(data))
|
|
46
|
+
return artifacts
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.command()
|
|
50
|
+
@click.option("--input", "input_dir", required=True,
|
|
51
|
+
help="Directory containing result JSON files")
|
|
52
|
+
@click.option("--output", "output_dir", required=True,
|
|
53
|
+
help="Directory to write exported files")
|
|
54
|
+
@click.option("--format", "fmt", default="html",
|
|
55
|
+
type=click.Choice(["html"]),
|
|
56
|
+
help="Output format (default: html)")
|
|
57
|
+
def main(input_dir: str, output_dir: str, fmt: str) -> None:
|
|
58
|
+
"""Export A2A task artifacts to HTML."""
|
|
59
|
+
if not os.path.isdir(input_dir):
|
|
60
|
+
click.echo(f"Error: {input_dir} is not a directory")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
artifacts = _load_artifacts_from_dir(input_dir)
|
|
64
|
+
if not artifacts:
|
|
65
|
+
click.echo(f"Error: no artifacts found in {input_dir}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
click.echo(f"Loaded {len(artifacts)} artifacts from {input_dir}")
|
|
69
|
+
|
|
70
|
+
if fmt == "html":
|
|
71
|
+
export_artifacts_to_html_directory(artifacts, output_dir)
|
|
72
|
+
|
|
73
|
+
click.echo(f"Exported to {output_dir}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|