diff --git a/libs/labelbox/src/labelbox/data/serialization/ndjson/converter.py b/libs/labelbox/src/labelbox/data/serialization/ndjson/converter.py index 4d3c160b5..9d0bbdc5a 100644 --- a/libs/labelbox/src/labelbox/data/serialization/ndjson/converter.py +++ b/libs/labelbox/src/labelbox/data/serialization/ndjson/converter.py @@ -1,24 +1,9 @@ -import copy import logging -import uuid -from collections import defaultdict, deque -from typing import Any, Deque, Dict, Generator, List, Set, Union +from typing import Any, Dict, Generator -from labelbox.data.annotation_types.annotation import ObjectAnnotation -from labelbox.data.annotation_types.classification.classification import ( - ClassificationAnnotation, -) -from labelbox.data.annotation_types.metrics.confusion_matrix import ( - ConfusionMatrixMetric, -) -from labelbox.data.annotation_types.metrics.scalar import ScalarMetric -from labelbox.data.annotation_types.video import VideoMaskAnnotation from ...annotation_types.collection import LabelCollection -from ...annotation_types.relationship import RelationshipAnnotation -from ...annotation_types.mmc import MessageEvaluationTaskAnnotation from .label import NDLabel -import copy logger = logging.getLogger(__name__) @@ -42,67 +27,8 @@ def serialize( Returns: A generator for accessing the ndjson representation of the data """ - used_uuids: Set[uuid.UUID] = set() - relationship_uuids: Dict[uuid.UUID, Deque[uuid.UUID]] = defaultdict( - deque - ) - - # UUIDs are private properties used to enhance UX when defining relationships. - # They are created for all annotations, but only utilized for relationships. - # To avoid overwriting, UUIDs must be unique across labels. - # Non-relationship annotation UUIDs are regenerated when they are reused. - # For relationship annotations, during first pass, we update the UUIDs of the source and target annotations. - # During the second pass, we update the UUIDs of the annotations referenced by the relationship annotations. for label in labels: - uuid_safe_annotations: List[ - Union[ - ClassificationAnnotation, - ObjectAnnotation, - VideoMaskAnnotation, - ScalarMetric, - ConfusionMatrixMetric, - RelationshipAnnotation, - MessageEvaluationTaskAnnotation, - ] - ] = [] - # First pass to get all RelationshipAnnotaitons - # and update the UUIDs of the source and target annotations - for annotation in label.annotations: - if isinstance(annotation, RelationshipAnnotation): - annotation = copy.deepcopy(annotation) - new_source_uuid = uuid.uuid4() - new_target_uuid = uuid.uuid4() - relationship_uuids[annotation.value.source._uuid].append( - new_source_uuid - ) - relationship_uuids[annotation.value.target._uuid].append( - new_target_uuid - ) - annotation.value.source._uuid = new_source_uuid - annotation.value.target._uuid = new_target_uuid - if annotation._uuid in used_uuids: - annotation._uuid = uuid.uuid4() - used_uuids.add(annotation._uuid) - uuid_safe_annotations.append(annotation) - # Second pass to update UUIDs for annotations referenced by RelationshipAnnotations - for annotation in label.annotations: - if not isinstance( - annotation, RelationshipAnnotation - ) and hasattr(annotation, "_uuid"): - annotation = copy.deepcopy(annotation) - next_uuids = relationship_uuids[annotation._uuid] - if len(next_uuids) > 0: - annotation._uuid = next_uuids.popleft() - - if annotation._uuid in used_uuids: - annotation._uuid = uuid.uuid4() - used_uuids.add(annotation._uuid) - uuid_safe_annotations.append(annotation) - else: - if not isinstance(annotation, RelationshipAnnotation): - uuid_safe_annotations.append(annotation) - label.annotations = uuid_safe_annotations for example in NDLabel.from_common([label]): annotation_uuid = getattr(example, "uuid", None) res = example.model_dump( diff --git a/libs/labelbox/src/labelbox/data/serialization/ndjson/label.py b/libs/labelbox/src/labelbox/data/serialization/ndjson/label.py index f8fd832f4..9ceb5dafd 100644 --- a/libs/labelbox/src/labelbox/data/serialization/ndjson/label.py +++ b/libs/labelbox/src/labelbox/data/serialization/ndjson/label.py @@ -1,7 +1,9 @@ from collections import defaultdict +import copy from itertools import groupby from operator import itemgetter -from typing import Dict, Generator, List, Tuple, Union +from typing import Generator, List, Tuple, Union +from uuid import uuid4 from pydantic import BaseModel @@ -9,8 +11,7 @@ ClassificationAnnotation, ObjectAnnotation, ) -from ...annotation_types.collection import LabelCollection, LabelGenerator -from ...annotation_types.data.generic_data_row_data import GenericDataRowData +from ...annotation_types.collection import LabelCollection from ...annotation_types.label import Label from ...annotation_types.llm_prompt_response.prompt import ( PromptClassificationAnnotation, @@ -23,7 +24,6 @@ VideoMaskAnnotation, VideoObjectAnnotation, ) -from .base import DataRow from .classification import ( NDChecklistSubclass, NDClassification, @@ -60,143 +60,19 @@ class NDLabel(BaseModel): annotations: AnnotationType - class _Relationship(BaseModel): - """This object holds information about the relationship""" - - ndjson: NDRelationship - source: str - target: str - - class _AnnotationGroup(BaseModel): - """Stores all the annotations and relationships per datarow""" - - data_row: DataRow = None - ndjson_annotations: Dict[str, AnnotationType] = {} - relationships: List["NDLabel._Relationship"] = [] - - def to_common(self) -> LabelGenerator: - annotation_groups = defaultdict(NDLabel._AnnotationGroup) - - for ndjson_annotation in self.annotations: - key = ( - ndjson_annotation.data_row.id - or ndjson_annotation.data_row.global_key - ) - group = annotation_groups[key] - - if isinstance(ndjson_annotation, NDRelationship): - group.relationships.append( - NDLabel._Relationship( - ndjson=ndjson_annotation, - source=ndjson_annotation.relationship.source, - target=ndjson_annotation.relationship.target, - ) - ) - else: - # if this is the first object in this group, we - # take note of the DataRow this group belongs to - # and store it in the _AnnotationGroupTuple - if not group.ndjson_annotations: - group.data_row = ndjson_annotation.data_row - - # if this assertion fails and it's a valid case, - # we need to change the value type of - # `_AnnotationGroupTuple.ndjson_objects` to accept a list of objects - # and adapt the code to support duplicate UUIDs - assert ( - ndjson_annotation.uuid not in group.ndjson_annotations - ), f"UUID '{ndjson_annotation.uuid}' is not unique" - - group.ndjson_annotations[ndjson_annotation.uuid] = ( - ndjson_annotation - ) - - return LabelGenerator( - data=self._generate_annotations(annotation_groups) - ) - @classmethod def from_common( cls, data: LabelCollection ) -> Generator["NDLabel", None, None]: for label in data: + if all( + isinstance(model, RelationshipAnnotation) + for model in label.annotations + ): + yield from cls._create_relationship_annotations(label) yield from cls._create_non_video_annotations(label) yield from cls._create_video_annotations(label) - def _generate_annotations( - self, annotation_groups: Dict[str, _AnnotationGroup] - ) -> Generator[Label, None, None]: - for _, group in annotation_groups.items(): - relationship_annotations: Dict[str, ObjectAnnotation] = {} - annotations = [] - # first, we iterate through all the NDJSON objects and store the - # deserialized objects in the _AnnotationGroupTuple - # object *if* the object can be used in a relationship - for uuid, ndjson_annotation in group.ndjson_annotations.items(): - if isinstance(ndjson_annotation, NDSegments): - annotations.extend( - NDSegments.to_common( - ndjson_annotation, - ndjson_annotation.name, - ndjson_annotation.schema_id, - ) - ) - elif isinstance(ndjson_annotation, NDVideoMasks): - annotations.append( - NDVideoMasks.to_common(ndjson_annotation) - ) - elif isinstance(ndjson_annotation, NDObjectType.__args__): - annotation = NDObject.to_common(ndjson_annotation) - annotations.append(annotation) - relationship_annotations[uuid] = annotation - elif isinstance( - ndjson_annotation, NDClassificationType.__args__ - ): - annotations.extend( - NDClassification.to_common(ndjson_annotation) - ) - elif isinstance( - ndjson_annotation, (NDScalarMetric, NDConfusionMatrixMetric) - ): - annotations.append( - NDMetricAnnotation.to_common(ndjson_annotation) - ) - elif isinstance(ndjson_annotation, NDPromptClassificationType): - annotation = NDPromptClassification.to_common( - ndjson_annotation - ) - annotations.append(annotation) - elif isinstance(ndjson_annotation, NDMessageTask): - annotations.append(ndjson_annotation.to_common()) - else: - raise TypeError( - f"Unsupported annotation. {type(ndjson_annotation)}" - ) - - # after all the annotations have been discovered, we can now create - # the relationship objects and use references to the objects - # involved - for relationship in group.relationships: - try: - source, target = ( - relationship_annotations[relationship.source], - relationship_annotations[relationship.target], - ) - except KeyError: - raise ValueError( - f"Relationship object refers to nonexistent object with UUID '{relationship.source}' and/or '{relationship.target}'" - ) - annotations.append( - NDRelationship.to_common( - relationship.ndjson, source, target - ) - ) - - yield Label( - annotations=annotations, - data=GenericDataRowData, - ) - @staticmethod def _get_consecutive_frames( frames_indices: List[int], @@ -317,3 +193,26 @@ def _create_non_video_annotations(cls, label: Label): raise TypeError( f"Unable to convert object to MAL format. `{type(getattr(annotation, 'value',annotation))}`" ) + + def _create_relationship_annotations(cls, label: Label): + relationship_annotations = [ + annotation + for annotation in label.annotations + if isinstance(annotation, RelationshipAnnotation) + ] + for relationship_annotation in relationship_annotations: + uuid1 = uuid4() + uuid2 = uuid4() + source = copy.copy(relationship_annotation.value.source) + target = copy.copy(relationship_annotation.value.target) + if not isinstance(source, ObjectAnnotation) or not isinstance( + target, ObjectAnnotation + ): + raise TypeError( + f"Unable to create relationship with non ObjectAnnotations. `Source: {type(source)} Target: {type(target)}`" + ) + if not source._uuid: + source._uuid = uuid1 + if not target._uuid: + target._uuid = uuid2 + yield relationship_annotation diff --git a/libs/labelbox/tests/data/annotation_import/test_mea_prediction_import.py b/libs/labelbox/tests/data/annotation_import/test_mea_prediction_import.py index f309cf188..9a286b55e 100644 --- a/libs/labelbox/tests/data/annotation_import/test_mea_prediction_import.py +++ b/libs/labelbox/tests/data/annotation_import/test_mea_prediction_import.py @@ -218,9 +218,6 @@ def test_create_from_label_objects( annotations=[ ObjectAnnotation( name="polygon", - extra={ - "uuid": "6d10fa30-3ea0-4e6c-bbb1-63f5c29fe3e4", - }, value=Polygon( points=[ Point(x=147.692, y=118.154), @@ -233,9 +230,6 @@ def test_create_from_label_objects( ), ObjectAnnotation( name="bbox", - extra={ - "uuid": "15b7138f-4bbc-42c5-ae79-45d87b0a3b2a", - }, value=Rectangle( start=Point(x=58.0, y=48.0), end=Point(x=70.0, y=113.0), @@ -243,9 +237,6 @@ def test_create_from_label_objects( ), ObjectAnnotation( name="polyline", - extra={ - "uuid": "cf4c6df9-c39c-4fbc-9541-470f6622978a", - }, value=Line( points=[ Point(x=147.692, y=118.154), diff --git a/libs/labelbox/tests/data/annotation_import/test_relationships.py b/libs/labelbox/tests/data/annotation_import/test_relationships.py new file mode 100644 index 000000000..038767e49 --- /dev/null +++ b/libs/labelbox/tests/data/annotation_import/test_relationships.py @@ -0,0 +1,217 @@ +import datetime +import labelbox as lb +from labelbox.client import Client +from labelbox.schema.enums import AnnotationImportState +from labelbox.schema.media_type import MediaType +from labelbox.schema.project import Project +from labelbox.types import ( + Label, + ObjectAnnotation, + RelationshipAnnotation, + Relationship, + TextEntity, +) +import pytest + + +def validate_iso_format(date_string: str): + parsed_t = datetime.datetime.fromisoformat( + date_string + ) # this will blow up if the string is not in iso format + assert parsed_t.hour is not None + assert parsed_t.minute is not None + assert parsed_t.second is not None + + +def _get_text_relationship_label(): + ner_source = ObjectAnnotation( + name="e1", + value=TextEntity(start=10, end=12), + ) + ner_source2 = ObjectAnnotation( + name="e4", + value=TextEntity(start=40, end=70), + ) + ner_target = ObjectAnnotation( + name="e2", + value=TextEntity(start=30, end=35), + ) + ner_target2 = ObjectAnnotation( + name="e3", + value=TextEntity(start=40, end=60), + ) + + ner_relationship1 = RelationshipAnnotation( + name="rel", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target, + type=Relationship.Type.UNIDIRECTIONAL, + ), + ) + + ner_relationship2 = RelationshipAnnotation( + name="rel2", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target2, + type=Relationship.Type.UNIDIRECTIONAL, + ), + ) + + ner_relationship3 = RelationshipAnnotation( + name="rel3", + value=Relationship( + source=ner_target, # UUID is not required for annotation types + target=ner_source2, + type=Relationship.Type.BIDIRECTIONAL, + ), + ) + + return [ + ner_source, + ner_source2, + ner_target, + ner_target2, + ner_relationship1, + ner_relationship2, + ner_relationship3, + ] + + +@pytest.fixture(scope="module", autouse=True) +def normalized_ontology_by_media_type_relationship(): + """Returns NDJSON of ontology based on media type""" + + entity_source_tool = { + "required": False, + "name": "e1", + "tool": "named-entity", + "color": "#006FA6", + "classifications": [], + } + entity_target_tool = { + "required": False, + "name": "e2", + "tool": "named-entity", + "color": "#006FA6", + "classifications": [], + } + entity_target_2_tool = { + "required": False, + "name": "e3", + "tool": "named-entity", + "color": "#006FA6", + "classifications": [], + } + entity_source_2_tool = { + "required": False, + "name": "e4", + "tool": "named-entity", + "color": "#006FA6", + "classifications": [], + } + relationship_1 = { + "name": "rel", + "tool": "edge", + } + relationship_2 = { + "name": "rel2", + "tool": "edge", + } + relationship_3 = { + "name": "rel3", + "tool": "edge", + } + + return { + MediaType.Text: { + "tools": [ + entity_source_tool, + entity_source_2_tool, + entity_target_tool, + entity_target_2_tool, + relationship_1, + relationship_2, + relationship_3, + ], + }, + } + + +@pytest.fixture +def configured_project( + client: Client, + rand_gen, + data_row_json_by_media_type, + normalized_ontology_by_media_type_relationship, +): + """Configure project for test. Request.param will contain the media type if not present will use Image MediaType. The project will have 10 data rows.""" + + media_type = MediaType.Text + + dataset = None + + dataset = client.create_dataset(name=rand_gen(str)) + + project = client.create_project( + name=f"{media_type}-{rand_gen(str)}", media_type=media_type + ) + + ontology = client.create_ontology( + name=f"{media_type}-{rand_gen(str)}", + normalized=normalized_ontology_by_media_type_relationship[media_type], + media_type=media_type, + ) + + project.connect_ontology(ontology) + data_row_data = [] + + for _ in range(3): + data_row_data.append( + data_row_json_by_media_type[media_type](rand_gen(str)) + ) + + task = dataset.create_data_rows(data_row_data) + task.wait_till_done() + global_keys = [row["global_key"] for row in task.result] + data_row_ids = [row["id"] for row in task.result] + + project.create_batch( + rand_gen(str), + data_row_ids, # sample of data row objects + 5, # priority between 1(Highest) - 5(lowest) + ) + project.data_row_ids = data_row_ids + project.global_keys = global_keys + + yield project + + +@pytest.mark.parametrize( + "configured_project", + [MediaType.Text], + indirect=["configured_project"], +) +def test_import_media_types( + client: Client, + configured_project: Project, +): + labels = [] + media_type = configured_project.media_type + for data_row in configured_project.data_row_ids: + annotations = _get_text_relationship_label() + + label = Label( + data={"uid": data_row}, + annotations=annotations, + ) + labels.append(label) + + label_import = lb.MALPredictionImport.create_from_objects( + client, configured_project.uid, f"test-import-{media_type}", labels + ) + label_import.wait_until_done() + + assert label_import.state == AnnotationImportState.FINISHED + assert len(label_import.errors) == 0 diff --git a/libs/labelbox/tests/data/serialization/ndjson/test_relationship.py b/libs/labelbox/tests/data/serialization/ndjson/test_relationship.py index 235b66957..13ff088aa 100644 --- a/libs/labelbox/tests/data/serialization/ndjson/test_relationship.py +++ b/libs/labelbox/tests/data/serialization/ndjson/test_relationship.py @@ -1,165 +1,194 @@ -import json - -from labelbox.data.annotation_types.data.generic_data_row_data import ( - GenericDataRowData, -) - from labelbox.data.serialization.ndjson.converter import NDJsonConverter from labelbox.types import ( Label, ObjectAnnotation, - Point, - Rectangle, RelationshipAnnotation, Relationship, + TextEntity, ) -def test_relationship(): - with open("tests/data/assets/ndjson/relationship_import.json", "r") as file: - data = json.load(file) - - res = [ - Label( - data=GenericDataRowData( - uid="clf98gj90000qp38ka34yhptl", - ), - annotations=[ - ObjectAnnotation( - name="cat", - extra={ - "uuid": "d8813907-b15d-4374-bbe6-b9877fb42ccd", - }, - value=Rectangle( - start=Point(x=100.0, y=200.0), - end=Point(x=200.0, y=300.0), - ), - ), - ObjectAnnotation( - name="dog", - extra={ - "uuid": "9b1e1249-36b4-4665-b60a-9060e0d18660", - }, - value=Rectangle( - start=Point(x=400.0, y=500.0), - end=Point(x=600.0, y=700.0), - ), - ), - RelationshipAnnotation( - name="is chasing", - extra={"uuid": "0e6354eb-9adb-47e5-8e52-217ed016d948"}, - value=Relationship( - source=ObjectAnnotation( - name="dog", - extra={ - "uuid": "9b1e1249-36b4-4665-b60a-9060e0d18660", - }, - value=Rectangle( - start=Point(x=400.0, y=500.0), - end=Point(x=600.0, y=700.0), - ), - ), - target=ObjectAnnotation( - name="cat", - extra={ - "uuid": "d8813907-b15d-4374-bbe6-b9877fb42ccd", - }, - value=Rectangle( - extra={}, - start=Point(x=100.0, y=200.0), - end=Point(x=200.0, y=300.0), - ), - ), - type=Relationship.Type.UNIDIRECTIONAL, - ), - ), - ], +def test_unidirectional_relationship(): + ner_source = ObjectAnnotation( + name="e1", + value=TextEntity(start=10, end=12), + ) + + ner_target = ObjectAnnotation( + name="e2", + value=TextEntity(start=30, end=35), + ) + + ner_target2 = ObjectAnnotation( + name="e3", + value=TextEntity(start=40, end=60), + ) + + ner_relationship1 = RelationshipAnnotation( + name="rel", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target, + type=Relationship.Type.UNIDIRECTIONAL, + ), + ) + + ner_relationship2 = RelationshipAnnotation( + name="rel2", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target2, + type=Relationship.Type.UNIDIRECTIONAL, + ), + ) + + label = Label( + data={"uid": "clqbkpy236syk07978v3pscw1"}, + annotations=[ + ner_source, + ner_target, + ner_target2, + ner_relationship1, + ner_relationship2, + ], + ) + + serialized_label = list(NDJsonConverter.serialize([label])) + + ner_source_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_source.name + ) + ner_target_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_target.name + ) + ner_target_2_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_target2.name + ) + rel_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_relationship1.name + ) + rel_2_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_relationship2.name + ) + + assert ( + rel_serialized["relationship"]["source"] + == ner_source_serialized["uuid"] + ) + assert ( + rel_serialized["relationship"]["target"] + == ner_target_serialized["uuid"] + ) + assert ( + rel_2_serialized["relationship"]["source"] + == ner_source_serialized["uuid"] + ) + assert ( + rel_2_serialized["relationship"]["target"] + == ner_target_2_serialized["uuid"] + ) + assert rel_serialized["relationship"]["type"] == "unidirectional" + assert rel_2_serialized["relationship"]["type"] == "unidirectional" + + +def test_bidirectional_relationship(): + ner_source = ObjectAnnotation( + name="e1", + value=TextEntity(start=10, end=12), + ) + + ner_target = ObjectAnnotation( + name="e2", + value=TextEntity(start=30, end=35), + ) + + ner_target2 = ObjectAnnotation( + name="e3", + value=TextEntity(start=40, end=60), + ) + + ner_relationship1 = RelationshipAnnotation( + name="rel", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target, + type=Relationship.Type.BIDIRECTIONAL, ), - Label( - data=GenericDataRowData( - uid="clf98gj90000qp38ka34yhptl-DIFFERENT", - ), - annotations=[ - ObjectAnnotation( - name="cat", - extra={ - "uuid": "d8813907-b15d-4374-bbe6-b9877fb42ccd", - }, - value=Rectangle( - start=Point(x=100.0, y=200.0), - end=Point(x=200.0, y=300.0), - ), - ), - ObjectAnnotation( - name="dog", - extra={ - "uuid": "9b1e1249-36b4-4665-b60a-9060e0d18660", - }, - value=Rectangle( - start=Point(x=400.0, y=500.0), - end=Point(x=600.0, y=700.0), - ), - ), - RelationshipAnnotation( - name="is chasing", - extra={"uuid": "0e6354eb-9adb-47e5-8e52-217ed016d948"}, - value=Relationship( - source=ObjectAnnotation( - name="dog", - extra={ - "uuid": "9b1e1249-36b4-4665-b60a-9060e0d18660", - }, - value=Rectangle( - start=Point(x=400.0, y=500.0), - end=Point(x=600.0, y=700.0), - ), - ), - target=ObjectAnnotation( - name="cat", - extra={ - "uuid": "d8813907-b15d-4374-bbe6-b9877fb42ccd", - }, - value=Rectangle( - start=Point(x=100.0, y=200.0), - end=Point(x=200.0, y=300.0), - ), - ), - type=Relationship.Type.UNIDIRECTIONAL, - ), - ), - ], + ) + + ner_relationship2 = RelationshipAnnotation( + name="rel2", + value=Relationship( + source=ner_source, # UUID is not required for annotation types + target=ner_target2, + type=Relationship.Type.BIDIRECTIONAL, ), - ] - res = list(NDJsonConverter.serialize(res)) - assert len(res) == len(data) - - res_relationship_annotation, res_relationship_second_annotation = [ - annot for annot in res if "relationship" in annot - ] - res_source_and_target = [ - annot for annot in res if "relationship" not in annot - ] - assert res_relationship_annotation - - assert res_relationship_annotation["relationship"]["source"] in [ - annot["uuid"] for annot in res_source_and_target - ] - assert res_relationship_annotation["relationship"]["target"] in [ - annot["uuid"] for annot in res_source_and_target - ] - - assert res_relationship_second_annotation + ) + + label = Label( + data={"uid": "clqbkpy236syk07978v3pscw1"}, + annotations=[ + ner_source, + ner_target, + ner_target2, + ner_relationship1, + ner_relationship2, + ], + ) + + serialized_label = list(NDJsonConverter.serialize([label])) + + ner_source_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_source.name + ) + ner_target_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_target.name + ) + ner_target_2_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_target2.name + ) + rel_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_relationship1.name + ) + rel_2_serialized = next( + annotation + for annotation in serialized_label + if annotation["name"] == ner_relationship2.name + ) + + assert ( + rel_serialized["relationship"]["source"] + == ner_source_serialized["uuid"] + ) assert ( - res_relationship_second_annotation["relationship"]["source"] - != res_relationship_annotation["relationship"]["source"] + rel_serialized["relationship"]["target"] + == ner_target_serialized["uuid"] ) assert ( - res_relationship_second_annotation["relationship"]["target"] - != res_relationship_annotation["relationship"]["target"] - ) - assert res_relationship_second_annotation["relationship"]["source"] in [ - annot["uuid"] for annot in res_source_and_target - ] - assert res_relationship_second_annotation["relationship"]["target"] in [ - annot["uuid"] for annot in res_source_and_target - ] + rel_2_serialized["relationship"]["source"] + == ner_source_serialized["uuid"] + ) + assert ( + rel_2_serialized["relationship"]["target"] + == ner_target_2_serialized["uuid"] + ) + assert rel_serialized["relationship"]["type"] == "bidirectional" + assert rel_2_serialized["relationship"]["type"] == "bidirectional"