tracelabel 0.1.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.
- tracelabel/__init__.py +1 -0
- tracelabel/__main__.py +4 -0
- tracelabel/api/__init__.py +0 -0
- tracelabel/api/app.py +40 -0
- tracelabel/api/labeling.py +202 -0
- tracelabel/api/models.py +92 -0
- tracelabel/api/routes.py +37 -0
- tracelabel/api/static.py +37 -0
- tracelabel/cli/__init__.py +0 -0
- tracelabel/cli/app.py +163 -0
- tracelabel/cli/commands.py +228 -0
- tracelabel/cli/options.py +30 -0
- tracelabel/cli/output.py +44 -0
- tracelabel/config/__init__.py +0 -0
- tracelabel/config/loader.py +51 -0
- tracelabel/config/models.py +96 -0
- tracelabel/config/presets.py +29 -0
- tracelabel/config/resolver.py +72 -0
- tracelabel/config/validation.py +144 -0
- tracelabel/ctf/__init__.py +0 -0
- tracelabel/ctf/content.py +35 -0
- tracelabel/ctf/hashing.py +31 -0
- tracelabel/ctf/models.py +78 -0
- tracelabel/ctf/validation.py +273 -0
- tracelabel/db/__init__.py +0 -0
- tracelabel/db/annotations.py +204 -0
- tracelabel/db/database.py +101 -0
- tracelabel/db/locking.py +100 -0
- tracelabel/db/migrations.py +94 -0
- tracelabel/db/tasks.py +159 -0
- tracelabel/db/traces.py +168 -0
- tracelabel/demo_data/traces.jsonl +25 -0
- tracelabel/errors.py +17 -0
- tracelabel/exporting/__init__.py +0 -0
- tracelabel/exporting/serializers.py +71 -0
- tracelabel/exporting/service.py +168 -0
- tracelabel/imports/__init__.py +0 -0
- tracelabel/imports/adapters/__init__.py +0 -0
- tracelabel/imports/adapters/adk.py +131 -0
- tracelabel/imports/adapters/base.py +65 -0
- tracelabel/imports/adapters/ctf.py +24 -0
- tracelabel/imports/adapters/datadog.py +149 -0
- tracelabel/imports/adapters/documents.py +45 -0
- tracelabel/imports/adapters/loose.py +144 -0
- tracelabel/imports/parsing.py +178 -0
- tracelabel/imports/service.py +113 -0
- tracelabel/static/assets/index-Ggdv0yW5.js +68 -0
- tracelabel/static/assets/index-XiSIyDmC.css +1 -0
- tracelabel/static/index.html +23 -0
- tracelabel/suggestions/__init__.py +0 -0
- tracelabel/suggestions/client.py +75 -0
- tracelabel/suggestions/prompts.py +123 -0
- tracelabel/suggestions/service.py +203 -0
- tracelabel-0.1.0.dist-info/METADATA +176 -0
- tracelabel-0.1.0.dist-info/RECORD +58 -0
- tracelabel-0.1.0.dist-info/WHEEL +4 -0
- tracelabel-0.1.0.dist-info/entry_points.txt +2 -0
- tracelabel-0.1.0.dist-info/licenses/LICENSE +201 -0
tracelabel/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
tracelabel/__main__.py
ADDED
|
File without changes
|
tracelabel/api/app.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from tracelabel.config.models import ResolvedTaskConfig
|
|
7
|
+
from tracelabel.db.database import Database
|
|
8
|
+
from tracelabel.errors import NotFoundError, UserError
|
|
9
|
+
|
|
10
|
+
from .labeling import LabelingService
|
|
11
|
+
from .routes import create_api_router
|
|
12
|
+
from .static import configure_static, default_static_dir
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_app(
|
|
16
|
+
database: Database,
|
|
17
|
+
config: ResolvedTaskConfig,
|
|
18
|
+
queue: list[str],
|
|
19
|
+
static_dir: Path | None = None,
|
|
20
|
+
) -> FastAPI:
|
|
21
|
+
service = LabelingService(
|
|
22
|
+
config,
|
|
23
|
+
queue,
|
|
24
|
+
database.traces,
|
|
25
|
+
database.tasks,
|
|
26
|
+
database.annotations,
|
|
27
|
+
)
|
|
28
|
+
app = FastAPI()
|
|
29
|
+
|
|
30
|
+
@app.exception_handler(NotFoundError)
|
|
31
|
+
async def not_found(_request: Request, error: NotFoundError) -> JSONResponse:
|
|
32
|
+
return JSONResponse(status_code=404, content={"detail": str(error)})
|
|
33
|
+
|
|
34
|
+
@app.exception_handler(UserError)
|
|
35
|
+
async def user_error(_request: Request, error: UserError) -> JSONResponse:
|
|
36
|
+
return JSONResponse(status_code=422, content={"detail": str(error)})
|
|
37
|
+
|
|
38
|
+
app.include_router(create_api_router(service))
|
|
39
|
+
configure_static(app, static_dir or default_static_dir())
|
|
40
|
+
return app
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from tracelabel.config.models import ResolvedTaskConfig
|
|
5
|
+
from tracelabel.config.validation import AnnotationValidator
|
|
6
|
+
from tracelabel.db.annotations import AnnotationRepository
|
|
7
|
+
from tracelabel.db.database import decode_json
|
|
8
|
+
from tracelabel.db.tasks import TaskRepository
|
|
9
|
+
from tracelabel.db.traces import TraceRepository
|
|
10
|
+
from tracelabel.errors import NotFoundError, UserError
|
|
11
|
+
|
|
12
|
+
from .models import (
|
|
13
|
+
AnnotationIn,
|
|
14
|
+
AnnotationOut,
|
|
15
|
+
DocumentOut,
|
|
16
|
+
Progress,
|
|
17
|
+
QueueEntry,
|
|
18
|
+
SessionInfo,
|
|
19
|
+
SuggestionOut,
|
|
20
|
+
TraceDetail,
|
|
21
|
+
TraceInfo,
|
|
22
|
+
TurnOut,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LabelingService:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
config: ResolvedTaskConfig,
|
|
30
|
+
queue: list[str],
|
|
31
|
+
traces: TraceRepository,
|
|
32
|
+
tasks: TaskRepository,
|
|
33
|
+
annotations: AnnotationRepository,
|
|
34
|
+
validator: AnnotationValidator | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._config = config
|
|
37
|
+
self._queue = queue
|
|
38
|
+
self._traces = traces
|
|
39
|
+
self._tasks = tasks
|
|
40
|
+
self._annotations = annotations
|
|
41
|
+
self._validator = validator or AnnotationValidator(config.fields)
|
|
42
|
+
|
|
43
|
+
def session(self) -> SessionInfo:
|
|
44
|
+
return SessionInfo(
|
|
45
|
+
task=self._config.name,
|
|
46
|
+
level=self._config.level,
|
|
47
|
+
fields=self._config.fields,
|
|
48
|
+
label_roles=self._config.label_roles,
|
|
49
|
+
annotator=self._config.annotator,
|
|
50
|
+
schema_hash=self._config.schema_hash,
|
|
51
|
+
shuffle=self._config.shuffle,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def queue(self) -> list[QueueEntry]:
|
|
55
|
+
counts = self._annotations.target_counts(self._task(), self._config.annotator)
|
|
56
|
+
entries: list[QueueEntry] = []
|
|
57
|
+
for position, trace_id in enumerate(self._queue):
|
|
58
|
+
target_count, labeled_count, skipped_count = counts.get(trace_id, (0, 0, 0))
|
|
59
|
+
entries.append(
|
|
60
|
+
QueueEntry(
|
|
61
|
+
trace_id=trace_id,
|
|
62
|
+
position=position,
|
|
63
|
+
n_targets=target_count,
|
|
64
|
+
n_labeled=labeled_count,
|
|
65
|
+
n_skipped=skipped_count,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return entries
|
|
69
|
+
|
|
70
|
+
def trace_detail(self, trace_id: str) -> TraceDetail:
|
|
71
|
+
trace = self._traces.get(trace_id)
|
|
72
|
+
if trace is None:
|
|
73
|
+
raise NotFoundError(f"unknown trace '{trace_id}'")
|
|
74
|
+
document = self._document_out(trace)
|
|
75
|
+
turns = (
|
|
76
|
+
[] if document else [self._turn_out(row) for row in self._traces.get_turns(trace_id)]
|
|
77
|
+
)
|
|
78
|
+
annotations = {
|
|
79
|
+
str(row["target_id"]): self._annotation_out(row)
|
|
80
|
+
for row in self._annotations.annotations_for_trace(
|
|
81
|
+
self._config.name,
|
|
82
|
+
self._config.annotator,
|
|
83
|
+
trace_id,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
suggestions = {
|
|
87
|
+
str(row["target_id"]): self._suggestion_out(row)
|
|
88
|
+
for row in self._annotations.suggestions_for_trace(self._config.name, trace_id)
|
|
89
|
+
}
|
|
90
|
+
return TraceDetail(
|
|
91
|
+
trace=TraceInfo(
|
|
92
|
+
id=trace["id"],
|
|
93
|
+
source=trace["source"],
|
|
94
|
+
metadata=self._json_object(trace["metadata"]),
|
|
95
|
+
),
|
|
96
|
+
turns=turns,
|
|
97
|
+
document=document,
|
|
98
|
+
annotations=annotations,
|
|
99
|
+
suggestions=suggestions,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def put_annotation(self, annotation: AnnotationIn) -> AnnotationOut:
|
|
103
|
+
self._validate_target(annotation)
|
|
104
|
+
self._validator.validate(annotation.values, annotation.status)
|
|
105
|
+
row = self._annotations.upsert_annotation(
|
|
106
|
+
task=self._config.name,
|
|
107
|
+
target_type=annotation.target_type,
|
|
108
|
+
target_id=annotation.target_id,
|
|
109
|
+
status=annotation.status,
|
|
110
|
+
values=dict(annotation.values),
|
|
111
|
+
annotator=self._config.annotator,
|
|
112
|
+
schema_hash=self._config.schema_hash,
|
|
113
|
+
prefill_model=annotation.prefill_model,
|
|
114
|
+
)
|
|
115
|
+
return self._annotation_out(row)
|
|
116
|
+
|
|
117
|
+
def progress(self) -> Progress:
|
|
118
|
+
counts = self._annotations.target_counts(self._task(), self._config.annotator)
|
|
119
|
+
return Progress(
|
|
120
|
+
unit="turns" if self._config.level == "turn" else "traces",
|
|
121
|
+
total=sum(count[0] for count in counts.values()),
|
|
122
|
+
labeled=sum(count[1] for count in counts.values()),
|
|
123
|
+
skipped=sum(count[2] for count in counts.values()),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _task(self) -> sqlite3.Row:
|
|
127
|
+
task = self._tasks.get(self._config.name)
|
|
128
|
+
if task is None:
|
|
129
|
+
raise UserError(f"task '{self._config.name}' is not open")
|
|
130
|
+
return task
|
|
131
|
+
|
|
132
|
+
def _validate_target(self, annotation: AnnotationIn) -> None:
|
|
133
|
+
if annotation.target_type != self._config.level:
|
|
134
|
+
raise UserError(
|
|
135
|
+
f"target_type '{annotation.target_type}' must match task level "
|
|
136
|
+
f"'{self._config.level}'"
|
|
137
|
+
)
|
|
138
|
+
if annotation.target_type == "trace":
|
|
139
|
+
if self._traces.get(annotation.target_id) is None:
|
|
140
|
+
raise NotFoundError(f"unknown trace '{annotation.target_id}'")
|
|
141
|
+
return
|
|
142
|
+
turn = self._traces.get_turn(annotation.target_id)
|
|
143
|
+
if turn is None:
|
|
144
|
+
raise NotFoundError(f"unknown turn '{annotation.target_id}'")
|
|
145
|
+
if turn["role"] not in self._config.label_roles:
|
|
146
|
+
raise UserError(
|
|
147
|
+
f"turn '{annotation.target_id}' has role '{turn['role']}', not labelable"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _document_out(trace: sqlite3.Row) -> DocumentOut | None:
|
|
152
|
+
content = trace["content"]
|
|
153
|
+
if content is None:
|
|
154
|
+
return None
|
|
155
|
+
return DocumentOut(content=content, content_type=trace["content_type"] or "text")
|
|
156
|
+
|
|
157
|
+
def _turn_out(self, row: sqlite3.Row) -> TurnOut:
|
|
158
|
+
raw_tool_calls = row["tool_calls"]
|
|
159
|
+
tool_calls = (
|
|
160
|
+
cast(list[dict[str, Any]], decode_json(raw_tool_calls))
|
|
161
|
+
if raw_tool_calls is not None
|
|
162
|
+
else None
|
|
163
|
+
)
|
|
164
|
+
return TurnOut(
|
|
165
|
+
id=row["id"],
|
|
166
|
+
idx=row["idx"],
|
|
167
|
+
role=row["role"],
|
|
168
|
+
content=row["content"],
|
|
169
|
+
content_type=row["content_type"],
|
|
170
|
+
tool_calls=tool_calls,
|
|
171
|
+
tool_call_id=row["tool_call_id"],
|
|
172
|
+
name=row["name"],
|
|
173
|
+
labelable=(self._config.level == "turn" and row["role"] in self._config.label_roles),
|
|
174
|
+
metadata=self._json_object(row["metadata"]),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def _annotation_out(cls, row: sqlite3.Row) -> AnnotationOut:
|
|
179
|
+
return AnnotationOut(
|
|
180
|
+
target_type=row["target_type"],
|
|
181
|
+
target_id=row["target_id"],
|
|
182
|
+
status=row["status"],
|
|
183
|
+
values=cls._json_object(row["values"]),
|
|
184
|
+
prefill_model=row["prefill_model"],
|
|
185
|
+
schema_hash=row["schema_hash"],
|
|
186
|
+
annotator=row["annotator"],
|
|
187
|
+
created_at=row["created_at"],
|
|
188
|
+
updated_at=row["updated_at"],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def _suggestion_out(cls, row: sqlite3.Row) -> SuggestionOut:
|
|
193
|
+
return SuggestionOut(
|
|
194
|
+
target_id=row["target_id"],
|
|
195
|
+
values=cls._json_object(row["values"]),
|
|
196
|
+
model=row["model"],
|
|
197
|
+
created_at=row["created_at"],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _json_object(raw: str) -> dict[str, Any]:
|
|
202
|
+
return cast(dict[str, Any], decode_json(raw))
|
tracelabel/api/models.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from tracelabel.config.models import AnnotationStatus, Level
|
|
6
|
+
from tracelabel.db.annotations import TargetType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SessionInfo(BaseModel):
|
|
10
|
+
task: str
|
|
11
|
+
level: Level
|
|
12
|
+
fields: list[dict[str, Any]]
|
|
13
|
+
label_roles: list[str]
|
|
14
|
+
annotator: str
|
|
15
|
+
schema_hash: str
|
|
16
|
+
shuffle: bool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueueEntry(BaseModel):
|
|
20
|
+
trace_id: str
|
|
21
|
+
position: int
|
|
22
|
+
n_targets: int
|
|
23
|
+
n_labeled: int
|
|
24
|
+
n_skipped: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TraceInfo(BaseModel):
|
|
28
|
+
id: str
|
|
29
|
+
source: str | None = None
|
|
30
|
+
metadata: dict[str, Any]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DocumentOut(BaseModel):
|
|
34
|
+
content: str
|
|
35
|
+
content_type: Literal["text", "json", "html", "markdown"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TurnOut(BaseModel):
|
|
39
|
+
id: str
|
|
40
|
+
idx: int
|
|
41
|
+
role: str
|
|
42
|
+
content: str
|
|
43
|
+
content_type: Literal["text", "json", "html", "parts"]
|
|
44
|
+
tool_calls: list[dict[str, Any]] | None = None
|
|
45
|
+
tool_call_id: str | None = None
|
|
46
|
+
name: str | None = None
|
|
47
|
+
labelable: bool
|
|
48
|
+
metadata: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AnnotationIn(BaseModel):
|
|
52
|
+
model_config = ConfigDict(extra="forbid")
|
|
53
|
+
|
|
54
|
+
target_type: TargetType
|
|
55
|
+
target_id: str
|
|
56
|
+
status: AnnotationStatus
|
|
57
|
+
values: dict[str, str | list[str]]
|
|
58
|
+
prefill_model: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AnnotationOut(BaseModel):
|
|
62
|
+
target_type: TargetType
|
|
63
|
+
target_id: str
|
|
64
|
+
status: AnnotationStatus
|
|
65
|
+
values: dict[str, Any]
|
|
66
|
+
prefill_model: str | None = None
|
|
67
|
+
schema_hash: str
|
|
68
|
+
annotator: str
|
|
69
|
+
created_at: str
|
|
70
|
+
updated_at: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SuggestionOut(BaseModel):
|
|
74
|
+
target_id: str
|
|
75
|
+
values: dict[str, Any]
|
|
76
|
+
model: str
|
|
77
|
+
created_at: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TraceDetail(BaseModel):
|
|
81
|
+
trace: TraceInfo
|
|
82
|
+
turns: list[TurnOut]
|
|
83
|
+
document: DocumentOut | None = None
|
|
84
|
+
annotations: dict[str, AnnotationOut]
|
|
85
|
+
suggestions: dict[str, SuggestionOut]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Progress(BaseModel):
|
|
89
|
+
unit: Literal["turns", "traces"]
|
|
90
|
+
total: int
|
|
91
|
+
labeled: int
|
|
92
|
+
skipped: int
|
tracelabel/api/routes.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from .labeling import LabelingService
|
|
4
|
+
from .models import (
|
|
5
|
+
AnnotationIn,
|
|
6
|
+
AnnotationOut,
|
|
7
|
+
Progress,
|
|
8
|
+
QueueEntry,
|
|
9
|
+
SessionInfo,
|
|
10
|
+
TraceDetail,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_api_router(service: LabelingService) -> APIRouter:
|
|
15
|
+
router = APIRouter(prefix="/api")
|
|
16
|
+
|
|
17
|
+
@router.get("/session", response_model=SessionInfo)
|
|
18
|
+
async def get_session() -> SessionInfo:
|
|
19
|
+
return service.session()
|
|
20
|
+
|
|
21
|
+
@router.get("/queue", response_model=list[QueueEntry])
|
|
22
|
+
async def get_queue() -> list[QueueEntry]:
|
|
23
|
+
return service.queue()
|
|
24
|
+
|
|
25
|
+
@router.get("/traces/{trace_id}", response_model=TraceDetail)
|
|
26
|
+
async def get_trace(trace_id: str) -> TraceDetail:
|
|
27
|
+
return service.trace_detail(trace_id)
|
|
28
|
+
|
|
29
|
+
@router.put("/annotations", response_model=AnnotationOut)
|
|
30
|
+
async def put_annotation(annotation: AnnotationIn) -> AnnotationOut:
|
|
31
|
+
return service.put_annotation(annotation)
|
|
32
|
+
|
|
33
|
+
@router.get("/progress", response_model=Progress)
|
|
34
|
+
async def get_progress() -> Progress:
|
|
35
|
+
return service.progress()
|
|
36
|
+
|
|
37
|
+
return router
|
tracelabel/api/static.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, HTTPException
|
|
4
|
+
from fastapi.responses import FileResponse, Response
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from starlette.types import Scope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImmutableStaticFiles(StaticFiles):
|
|
10
|
+
async def get_response(self, path: str, scope: Scope) -> Response:
|
|
11
|
+
response = await super().get_response(path, scope)
|
|
12
|
+
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
|
13
|
+
return response
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def default_static_dir() -> Path:
|
|
17
|
+
return Path(__file__).parent.parent / "static"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def configure_static(app: FastAPI, static_dir: Path) -> None:
|
|
21
|
+
assets_dir = static_dir / "assets"
|
|
22
|
+
if assets_dir.is_dir():
|
|
23
|
+
app.mount("/assets", ImmutableStaticFiles(directory=assets_dir), name="assets")
|
|
24
|
+
|
|
25
|
+
async def spa(full_path: str) -> FileResponse:
|
|
26
|
+
if full_path == "api" or full_path.startswith("api/"):
|
|
27
|
+
raise HTTPException(status_code=404, detail="not found")
|
|
28
|
+
index = static_dir / "index.html"
|
|
29
|
+
if not index.is_file():
|
|
30
|
+
raise HTTPException(
|
|
31
|
+
status_code=503,
|
|
32
|
+
detail="frontend not built — run `npm run build` in frontend/ "
|
|
33
|
+
"or use the Vite dev server",
|
|
34
|
+
)
|
|
35
|
+
return FileResponse(index)
|
|
36
|
+
|
|
37
|
+
app.add_api_route("/{full_path:path}", spa, methods=["GET"])
|
|
File without changes
|
tracelabel/cli/app.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from tracelabel.config.loader import raw_config_for_target
|
|
8
|
+
from tracelabel.config.models import CliArgs
|
|
9
|
+
from tracelabel.config.resolver import ConfigResolver
|
|
10
|
+
from tracelabel.errors import TraceLabelError, UserError
|
|
11
|
+
|
|
12
|
+
from .commands import (
|
|
13
|
+
DemoCommand,
|
|
14
|
+
ExportCommand,
|
|
15
|
+
ImportCommand,
|
|
16
|
+
ServeCommand,
|
|
17
|
+
SuggestCommand,
|
|
18
|
+
TasksListCommand,
|
|
19
|
+
)
|
|
20
|
+
from .options import (
|
|
21
|
+
FormatChoice,
|
|
22
|
+
FromChoice,
|
|
23
|
+
LevelChoice,
|
|
24
|
+
OnConflictChoice,
|
|
25
|
+
StatusChoice,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(add_completion=False)
|
|
29
|
+
tasks_app = typer.Typer(add_completion=False)
|
|
30
|
+
app.add_typer(tasks_app, name="tasks")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.callback()
|
|
34
|
+
def main() -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def target_path(target: str | None) -> Path:
|
|
39
|
+
if target is not None:
|
|
40
|
+
return Path(target)
|
|
41
|
+
fallback = Path("config.yaml")
|
|
42
|
+
if fallback.exists():
|
|
43
|
+
return fallback
|
|
44
|
+
raise UserError("No data file given (arg or `data:` in YAML)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def serve(
|
|
49
|
+
target: Annotated[str | None, typer.Argument()] = None,
|
|
50
|
+
task: Annotated[str | None, typer.Option("--task")] = None,
|
|
51
|
+
level: Annotated[LevelChoice | None, typer.Option("--level")] = None,
|
|
52
|
+
annotator: Annotated[str | None, typer.Option("--annotator")] = None,
|
|
53
|
+
shuffle: Annotated[bool | None, typer.Option("--shuffle/--no-shuffle")] = None,
|
|
54
|
+
db: Annotated[Path | None, typer.Option("--db")] = None,
|
|
55
|
+
port: Annotated[int, typer.Option("--port")] = 8377,
|
|
56
|
+
no_browser: Annotated[bool, typer.Option("--no-browser")] = False,
|
|
57
|
+
yes: Annotated[bool, typer.Option("--yes")] = False,
|
|
58
|
+
) -> None:
|
|
59
|
+
path = target_path(target)
|
|
60
|
+
cli = CliArgs(
|
|
61
|
+
task=task,
|
|
62
|
+
level=level.value if level else None,
|
|
63
|
+
annotator=annotator,
|
|
64
|
+
shuffle=shuffle,
|
|
65
|
+
db=db,
|
|
66
|
+
yes=yes,
|
|
67
|
+
)
|
|
68
|
+
config = ConfigResolver().resolve(raw_config_for_target(path), cli)
|
|
69
|
+
project_dir = path if path.is_dir() else path.parent
|
|
70
|
+
ServeCommand().execute(config, project_dir, db, port, no_browser, yes)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command(name="import")
|
|
74
|
+
def import_(
|
|
75
|
+
target: Annotated[str, typer.Argument()],
|
|
76
|
+
from_: Annotated[FromChoice, typer.Option("--from")] = FromChoice.auto,
|
|
77
|
+
db: Annotated[Path | None, typer.Option("--db")] = None,
|
|
78
|
+
on_conflict: Annotated[
|
|
79
|
+
OnConflictChoice,
|
|
80
|
+
typer.Option("--on-conflict"),
|
|
81
|
+
] = OnConflictChoice.fail,
|
|
82
|
+
skip_invalid: Annotated[bool, typer.Option("--skip-invalid")] = False,
|
|
83
|
+
as_documents: Annotated[bool, typer.Option("--as-documents")] = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
ImportCommand().execute(
|
|
86
|
+
Path(target),
|
|
87
|
+
database_path=db,
|
|
88
|
+
from_=from_.value,
|
|
89
|
+
on_conflict=on_conflict.value,
|
|
90
|
+
skip_invalid=skip_invalid,
|
|
91
|
+
as_documents=as_documents,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command()
|
|
96
|
+
def export(
|
|
97
|
+
task: Annotated[str | None, typer.Option("--task")] = None,
|
|
98
|
+
db: Annotated[Path | None, typer.Option("--db")] = None,
|
|
99
|
+
format: Annotated[FormatChoice, typer.Option("--format")] = FormatChoice.jsonl,
|
|
100
|
+
joined: Annotated[bool, typer.Option("--joined")] = False,
|
|
101
|
+
out: Annotated[str | None, typer.Option("--out")] = None,
|
|
102
|
+
status: Annotated[StatusChoice, typer.Option("--status")] = StatusChoice.all,
|
|
103
|
+
) -> None:
|
|
104
|
+
ExportCommand().execute(
|
|
105
|
+
task=task,
|
|
106
|
+
database_path=db,
|
|
107
|
+
format=format.value,
|
|
108
|
+
joined=joined,
|
|
109
|
+
out=Path(out) if out is not None else None,
|
|
110
|
+
status=status.value,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@tasks_app.command("list")
|
|
115
|
+
def tasks_list(db: Annotated[Path | None, typer.Option("--db")] = None) -> None:
|
|
116
|
+
TasksListCommand().execute(db)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def suggest(
|
|
121
|
+
target: Annotated[str | None, typer.Argument()] = None,
|
|
122
|
+
task: Annotated[str | None, typer.Option("--task")] = None,
|
|
123
|
+
db: Annotated[Path | None, typer.Option("--db")] = None,
|
|
124
|
+
limit: Annotated[int | None, typer.Option("--limit")] = None,
|
|
125
|
+
overwrite: Annotated[bool, typer.Option("--overwrite")] = False,
|
|
126
|
+
concurrency: Annotated[int, typer.Option("--concurrency")] = 4,
|
|
127
|
+
) -> None:
|
|
128
|
+
path = target_path(target)
|
|
129
|
+
config = ConfigResolver().resolve(
|
|
130
|
+
raw_config_for_target(path),
|
|
131
|
+
CliArgs(task=task, db=db),
|
|
132
|
+
)
|
|
133
|
+
summary = SuggestCommand().execute(
|
|
134
|
+
config,
|
|
135
|
+
path.parent,
|
|
136
|
+
db,
|
|
137
|
+
limit=limit,
|
|
138
|
+
overwrite=overwrite,
|
|
139
|
+
concurrency=concurrency,
|
|
140
|
+
)
|
|
141
|
+
attempted = summary.ok + summary.failed
|
|
142
|
+
typer.echo(f"suggested {summary.ok}/{attempted} · {summary.failed} failed (re-run to retry)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command()
|
|
146
|
+
def demo(
|
|
147
|
+
port: Annotated[int, typer.Option("--port")] = 8377,
|
|
148
|
+
no_browser: Annotated[bool, typer.Option("--no-browser")] = False,
|
|
149
|
+
) -> None:
|
|
150
|
+
DemoCommand().execute(port, no_browser)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def run() -> None:
|
|
154
|
+
try:
|
|
155
|
+
app(standalone_mode=False)
|
|
156
|
+
except TraceLabelError as error:
|
|
157
|
+
typer.echo(str(error), err=True)
|
|
158
|
+
raise SystemExit(error.exit_code) from error
|
|
159
|
+
except (click.exceptions.Abort, KeyboardInterrupt) as error:
|
|
160
|
+
raise SystemExit(130) from error
|
|
161
|
+
except click.exceptions.ClickException as error:
|
|
162
|
+
error.show()
|
|
163
|
+
raise SystemExit(error.exit_code) from error
|