Skip to content

Commit 86b2b79

Browse files
feat: add an export format for dynamic llm annotations (#1738)
1 parent 3e8ba39 commit 86b2b79

5 files changed

Lines changed: 554 additions & 34 deletions

File tree

src/kili/services/export/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def export_labels( # pylint: disable=too-many-arguments, too-many-locals
7272
"pascal_voc": VocExporter,
7373
"geojson": GeoJsonExporter,
7474
"llm_v1": LLMExporter,
75+
"llm_dynamic_v1": LLMExporter,
7576
}
7677
assert set(format_exporter_selector_mapping.keys()) == set(
7778
get_args(LabelFormat)

src/kili/services/export/format/llm/__init__.py

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import json
44
import logging
5+
from ast import literal_eval
56
from pathlib import Path
67
from typing import Dict, List, Optional, Union
78

89
from kili.services.asset_import.helpers import SEPARATOR
910
from kili.services.export.exceptions import NotCompatibleInputType
1011
from kili.services.export.format.base import AbstractExporter
12+
from kili.services.export.format.llm.types import ExportLLMItem, RankingValue
1113
from kili.services.types import Job
1214

1315

@@ -44,14 +46,48 @@ def process_and_save(
4446
self, assets: List[Dict], output_filename: Path
4547
) -> Optional[List[Dict[str, Union[List[str], str]]]]:
4648
"""LLM specific process and save."""
47-
result = self._process(assets)
49+
result = self.process(assets)
4850
self._save_assets_export(result, output_filename)
4951

5052
def process(self, assets: List[Dict]) -> List[Dict[str, Union[List[str], str]]]:
5153
"""LLM specific process."""
52-
return self._process(assets)
54+
if self.label_format == "llm_v1":
55+
return self._process_llm_v1(assets)
56+
return self._process_llm_dynamic_v1(assets)
5357

54-
def _process(self, assets: List[Dict]) -> List[Dict[str, Union[List[str], str]]]:
58+
def _process_llm_dynamic_v1(self, assets: List[Dict]) -> List[Dict[str, Union[List[str], str]]]:
59+
result = []
60+
for asset in assets:
61+
step_number = _count_step(asset)
62+
label = asset["latestLabel"]
63+
steps = {}
64+
context = []
65+
formatted_asset = _format_raw_data(asset, all_model_keys=True)
66+
for i in range(step_number):
67+
steps[f"{i}"] = {
68+
"raw_data": context + _format_raw_data(asset, i),
69+
"status": asset["status"],
70+
"external_id": asset["externalId"],
71+
"metadata": asset["jsonMetadata"],
72+
"labels": [
73+
{
74+
"author": label["author"]["email"],
75+
"created_at": label["createdAt"],
76+
"label_type": label["labelType"],
77+
"label": _format_json_response_dynamic(
78+
self.project["jsonInterface"]["jobs"], label["jsonResponse"], i
79+
),
80+
}
81+
],
82+
}
83+
next_context = _get_next_step_context(formatted_asset, label["jsonResponse"], i)
84+
context = context + next_context
85+
86+
if step_number > 0:
87+
result.append(steps)
88+
return result
89+
90+
def _process_llm_v1(self, assets: List[Dict]) -> List[Dict[str, Union[List[str], str]]]:
5591
result = []
5692
for asset in assets:
5793
result.append(
@@ -60,26 +96,87 @@ def _process(self, assets: List[Dict]) -> List[Dict[str, Union[List[str], str]]]
6096
"status": asset["status"],
6197
"external_id": asset["externalId"],
6298
"metadata": asset["jsonMetadata"],
63-
"labels": [
64-
list(
65-
map(
66-
lambda label: {
67-
"author": label["author"]["email"],
68-
"created_at": label["createdAt"],
69-
"label_type": label["labelType"],
70-
"label": _format_json_response(
71-
self.project["jsonInterface"]["jobs"], label["jsonResponse"]
72-
),
73-
},
74-
asset["labels"],
75-
)
99+
"labels": list(
100+
map(
101+
lambda label: {
102+
"author": label["author"]["email"],
103+
"created_at": label["createdAt"],
104+
"label_type": label["labelType"],
105+
"label": _format_json_response(
106+
self.project["jsonInterface"]["jobs"], label["jsonResponse"]
107+
),
108+
},
109+
asset["labels"],
76110
)
77-
],
111+
),
78112
}
79113
)
80114
return result
81115

82116

117+
def _get_step_ranking_value(json_response: Dict, step_number: int) -> RankingValue:
118+
prefix = f"STEP_{step_number+1}_"
119+
for category in json_response["CLASSIFICATION_JOB"]["categories"]:
120+
if category["name"] != f"STEP_{step_number+1}":
121+
continue
122+
123+
for children_name, children_value in category["children"].items():
124+
if children_name == f"STEP_{step_number+1}_RANKING":
125+
raw_value = children_value["categories"][0]["name"]
126+
return raw_value[len(prefix) :]
127+
return RankingValue.TIE
128+
129+
130+
def _get_next_step_context(
131+
formatted_asset: List[ExportLLMItem], json_response: Dict, step_number: int
132+
) -> List[ExportLLMItem]:
133+
context = []
134+
skipped_context = 0
135+
completion_index = 0
136+
ranking = _get_step_ranking_value(json_response, step_number)
137+
for item in formatted_asset:
138+
if skipped_context > step_number:
139+
break
140+
141+
if skipped_context == step_number:
142+
if item["role"] == "user":
143+
context.append(item)
144+
else:
145+
if completion_index == 0 and ranking in ["A_1", "A_2", "A_3", "TIE"]:
146+
context.append(item)
147+
break
148+
if completion_index == 1 and ranking in ["B_1", "B_2", "B_3"]:
149+
context.append(item)
150+
break
151+
completion_index += 1
152+
153+
if item["role"] == "assistant":
154+
skipped_context += 1
155+
156+
return context
157+
158+
159+
def _count_step(asset: Dict) -> int:
160+
label = asset["latestLabel"]
161+
if "jsonResponse" not in label and "CLASSIFICATION_JOB" not in label["jsonResponse"]:
162+
return 0
163+
return len(label["jsonResponse"]["CLASSIFICATION_JOB"]["categories"])
164+
165+
166+
def _format_json_response_dynamic(
167+
jobs_config: Dict, json_response: Dict, step_number: int
168+
) -> Dict[str, Union[str, List[str]]]:
169+
# check subjobs of the step
170+
job_step = f"STEP_{step_number+1}"
171+
for item in json_response["CLASSIFICATION_JOB"]["categories"]:
172+
if item["name"] != job_step:
173+
continue
174+
response_step = _format_json_response(jobs_config, item["children"])
175+
formatted_response = literal_eval(str(response_step).replace(job_step + "_", ""))
176+
return formatted_response
177+
return {}
178+
179+
83180
def _format_json_response(
84181
jobs_config: Dict, json_response: Dict
85182
) -> Dict[str, Union[str, List[str]]]:
@@ -104,7 +201,9 @@ def _format_json_response(
104201
return result
105202

106203

107-
def _format_raw_data(asset) -> List[Dict]:
204+
def _format_raw_data(
205+
asset, step_number: Optional[int] = None, all_model_keys: Optional[bool] = False
206+
) -> List[ExportLLMItem]:
108207
raw_data = []
109208

110209
chat_id = asset["jsonMetadata"].get("chat_id", None)
@@ -115,6 +214,8 @@ def _format_raw_data(asset) -> List[Dict]:
115214
and len(asset["jsonMetadata"]["chat_item_ids"]) > 0
116215
):
117216
chat_items_ids = str.split(asset["jsonMetadata"]["chat_item_ids"], SEPARATOR)
217+
if step_number is not None:
218+
chat_items_ids = chat_items_ids[step_number * 3 :]
118219
else:
119220
chat_items_ids = []
120221

@@ -131,25 +232,34 @@ def _format_raw_data(asset) -> List[Dict]:
131232
data = json.load(file)
132233
version = data.get("version", None)
133234
if version == "0.1":
134-
for index, prompt in enumerate(data["prompts"]):
235+
prompts = data["prompts"]
236+
if step_number is not None:
237+
prompts = [prompts[step_number]]
238+
for index, prompt in enumerate(prompts):
135239
raw_data.append(
136-
{
137-
"role": prompt.get("title", "user"),
138-
"content": prompt["prompt"],
139-
"id": _safe_pop(chat_items_ids),
140-
"chat_id": chat_id,
141-
"model": None,
142-
}
240+
ExportLLMItem(
241+
{
242+
"role": prompt.get("title", "user"),
243+
"content": prompt["prompt"],
244+
"id": _safe_pop(chat_items_ids),
245+
"chat_id": chat_id,
246+
"model": None,
247+
}
248+
)
143249
)
144250
raw_data.extend(
145-
{
146-
"role": completion.get("title", "assistant"),
147-
"content": completion["content"],
148-
"id": _safe_pop(chat_items_ids),
149-
"chat_id": chat_id,
150-
"model": _safe_pop(models) if index == len(data["prompts"]) - 1 else None,
151-
}
152-
for completion in prompt["completions"]
251+
ExportLLMItem(
252+
{
253+
"role": completion.get("title", "assistant"),
254+
"content": completion["content"],
255+
"id": _safe_pop(chat_items_ids),
256+
"chat_id": chat_id,
257+
"model": models[index_completion]
258+
if (index == len(prompts) - 1 or all_model_keys)
259+
else None,
260+
}
261+
)
262+
for index_completion, completion in enumerate(prompt["completions"])
153263
)
154264
else:
155265
raise ValueError(f"Version {version} not supported")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Custom Types."""
2+
3+
from enum import Enum
4+
from typing import List, Optional, TypedDict
5+
6+
7+
class ExportLLMItem(TypedDict):
8+
"""LLM asset chat part."""
9+
10+
role: str
11+
content: str
12+
id: Optional[str]
13+
chat_id: Optional[str]
14+
model: Optional[str]
15+
16+
17+
class ExportLLMAsset(TypedDict):
18+
"""LLM export asset format."""
19+
20+
raw_data: List[ExportLLMItem]
21+
22+
23+
class RankingValue(str, Enum):
24+
"""Possible value for ranking."""
25+
26+
A_3 = "A_3"
27+
A_2 = "A_2"
28+
A_1 = "A_1"
29+
TIE = "TIE"
30+
B_1 = "B_1"
31+
B_2 = "B_2"
32+
B_3 = "B_3"

src/kili/services/export/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"pascal_voc",
1616
"geojson",
1717
"llm_v1",
18+
"llm_dynamic_v1",
1819
]
1920

2021

0 commit comments

Comments
 (0)