Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8747bf2
Remove unused random parameter
XanderVertegaal Sep 5, 2025
06e4a93
User-added problems WIP
XanderVertegaal Sep 5, 2025
6be3d10
Merge branch 'develop' into feature/user-problems
XanderVertegaal Sep 5, 2025
9c11b77
Clean up templates
XanderVertegaal Sep 7, 2025
c2eb6ed
Avoid NotImplemented error when gold param is not provided
XanderVertegaal Sep 7, 2025
579f5e1
Show toasts upon saving a problem
XanderVertegaal Sep 7, 2025
6f2db84
Rename problem to problemResponse
XanderVertegaal Sep 15, 2025
508991d
Move route watching to parent AnnotateComponent
XanderVertegaal Sep 15, 2025
cc0bbde
Add AppMode service
XanderVertegaal Sep 15, 2025
c22a67b
Properly separate problemResponse from problem
XanderVertegaal Sep 15, 2025
978a35e
Return KBs from backend; update in frontend
XanderVertegaal Sep 15, 2025
5d83ce6
Use consistent str for ID
XanderVertegaal Sep 16, 2025
289c622
Consistent casing for KB Relationship
XanderVertegaal Sep 16, 2025
fb23d19
Display KB items in browsing mode
XanderVertegaal Sep 16, 2025
830a3d8
Cleanup
XanderVertegaal Sep 16, 2025
49b03d4
Reuse sentences when creating new problem
XanderVertegaal Sep 16, 2025
cf339b8
Fix failing tests
XanderVertegaal Sep 16, 2025
9d70ffa
Show error message upon omitting hypothesis
XanderVertegaal Sep 16, 2025
51b235d
Add more tests
XanderVertegaal Sep 16, 2025
483c0f8
Use int for IDs
XanderVertegaal Sep 17, 2025
d144eb6
Fix failing tests
XanderVertegaal Sep 17, 2025
619439c
Avoid unnecessary requests
XanderVertegaal Sep 22, 2025
52b775a
Use serializers for input validation
XanderVertegaal Oct 23, 2025
4e8c4a9
Sensible naming for urls
XanderVertegaal Oct 23, 2025
aa0b009
Only update User problems
XanderVertegaal Oct 23, 2025
4c30ea7
Regular dict access
XanderVertegaal Oct 23, 2025
8e24695
Indentation
XanderVertegaal Oct 23, 2025
4f4576e
More intuitive related property access
XanderVertegaal Oct 23, 2025
b7f040c
Allow exceptions to bubble up to post()
XanderVertegaal Oct 23, 2025
83dbee6
Let Django crash hard if no KB items can be created
XanderVertegaal Oct 23, 2025
2c39be6
Remove appModeService, integrate logic into ProblemService
XanderVertegaal Oct 23, 2025
bd6b8ce
Let more exceptions bubble up
XanderVertegaal Oct 25, 2025
2663a29
Remove unused import
XanderVertegaal Nov 4, 2025
c4a387b
Refactor serializer tests to use pytest
XanderVertegaal Dec 4, 2025
a826af9
Reinstate Request type annotations
XanderVertegaal Dec 4, 2025
cf4a202
Replace manual serialization with ModelViewSet
XanderVertegaal Dec 4, 2025
1bfb7f8
Remove unused validation code
XanderVertegaal Dec 4, 2025
75146f9
Use serializer create/update methods
XanderVertegaal Dec 4, 2025
90b0668
Remove unused fields
XanderVertegaal Dec 4, 2025
f064c00
Replace routing buttons with anchor tags
XanderVertegaal Dec 4, 2025
a7b09f3
Add template veriable for shorthand
XanderVertegaal Dec 4, 2025
ad76efa
Fix failing tests
XanderVertegaal Dec 4, 2025
2ba0c98
Fix backend tests
XanderVertegaal Dec 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/langpro_annotator/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@

from rest_framework import routers

from problem.views.problem import ProblemView

from .index import index
from .proxy_frontend import proxy_frontend
from .i18n import i18n

api_router = routers.DefaultRouter() # register viewsets with this router
api_router.register(r"problem", ProblemView, basename="problem")


if settings.PROXY_FRONTEND:
Expand Down
33 changes: 8 additions & 25 deletions backend/problem/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.db.models import QuerySet

from problem.services import FracasData, SNLIData, SickData
Expand Down Expand Up @@ -58,30 +57,6 @@ def get_index(self, qs: QuerySet) -> int | None:
logger.exception(f"Error getting index for problem {self.pk}: {e}")
return None

def serialize(self) -> dict:
"""
Serialize the Problem instance to a dictionary.
"""

match self.dataset:
case self.Dataset.SICK:
serialized_extra_data = SickData.serialize(self.extra_data)
case self.Dataset.FRACAS:
serialized_extra_data = FracasData.serialize(self.extra_data)
case self.Dataset.SNLI:
serialized_extra_data = SNLIData.serialize(self.extra_data)
case _:
serialized_extra_data = {}

return {
"id": self.pk,
"dataset": self.dataset,
"premises": [premise.text for premise in self.premises.all()],
"hypothesis": self.hypothesis.text,
"entailmentLabel": self.entailment_label,
"extraData": serialized_extra_data,
}


class KnowledgeBase(models.Model):
class Relationship(models.TextChoices):
Expand All @@ -105,3 +80,11 @@ class Relationship(models.TextChoices):
on_delete=models.CASCADE,
related_name="knowledge_bases",
)

def serialize(self) -> dict:
return {
"id": self.pk,
"entity1": self.entity1,
"entity2": self.entity2,
"relationship": self.relationship,
}
26 changes: 13 additions & 13 deletions backend/problem/problem_details.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Optional
from dataclasses import dataclass

from django.http import QueryDict
Expand All @@ -10,16 +9,16 @@

@dataclass
class RelatedProblemIds:
first: Optional[str] = None
previous: Optional[str] = None
next: Optional[str] = None
last: Optional[str] = None
total: Optional[int] = None
first: int | None = None
previous: int | None = None
next: int | None = None
last: int | None = None
total: int | None = None


def get_related_problem_ids(
problem_qs: QuerySet[Problem],
problem_id: Optional[int],
problem_id: int | None = None,
) -> RelatedProblemIds:
"""
Retrieves the IDs of surrounding problem objects
Expand All @@ -44,10 +43,10 @@ def get_related_problem_ids(
problem = None

return RelatedProblemIds(
first=str(first_problem.pk) if first_problem else None,
previous=str(previous_problem.pk) if previous_problem else None,
next=str(next_problem.pk) if next_problem else None,
last=str(last_problem.pk) if last_problem else None,
first=first_problem.pk if first_problem else None,
previous=previous_problem.pk if previous_problem else None,
next=next_problem.pk if next_problem else None,
last=last_problem.pk if last_problem else None,
total=total,
)

Expand All @@ -70,8 +69,9 @@ def get_filters(query_params: QueryDict) -> Q | None:
filters &= Q(dataset=dataset)
if entailment_label:
filters &= Q(entailment_label=entailment_label)
if gold is not None:
raise NotImplementedError()
if gold:
logger.warning(f"Filtering by gold is not implemented yet.")
pass
if text:
filters &= Q(
Q(hypothesis__text__icontains=text) | Q(premises__text__icontains=text)
Expand Down
200 changes: 200 additions & 0 deletions backend/problem/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from rest_framework import serializers
from problem.services import FracasData, SNLIData, SickData
from problem.models import Problem, KnowledgeBase, Sentence


class KnowledgeBaseSerializer(serializers.ModelSerializer):

class Meta:
model = KnowledgeBase
fields = ["id", "entity1", "entity2", "relationship"]
extra_kwargs = {
# Without this, the relationship field is not required during validation.
"relationship": {"required": True},
}

def validate_id(self, value):
"""Validate that the KnowledgeBase ID exists if provided."""
if value is not None:
if not KnowledgeBase.objects.filter(id=value).exists():
raise serializers.ValidationError(
f"KnowledgeBase item with ID {value} does not exist."
)
return value

def create_for_problem(
self, validated_data: dict, problem: Problem
) -> KnowledgeBase:
"""Create a new KnowledgeBase item for a problem."""
return KnowledgeBase.objects.create(
**validated_data,
problem=problem,
)

def update(self, instance: KnowledgeBase, validated_data: dict) -> KnowledgeBase:
"""Update an existing KnowledgeBase item."""
instance.entity1 = validated_data["entity1"]
instance.relationship = validated_data["relationship"]
instance.entity2 = validated_data["entity2"]
instance.save()
return instance


class ProblemSerializer(serializers.ModelSerializer):
"""
Serializer for Problem model output.
Handles serialization of problems with all related data including labels.
"""

premises = serializers.SerializerMethodField()
hypothesis = serializers.SerializerMethodField()
entailmentLabel = serializers.CharField(source="entailment_label")
extraData = serializers.SerializerMethodField()
kbItems = serializers.SerializerMethodField()

class Meta:
model = Problem
fields = [
"id",
"dataset",
"premises",
"hypothesis",
"entailmentLabel",
"extraData",
"kbItems",
]

def get_premises(self, problem):
"""Get list of premise texts."""
return [premise.text for premise in problem.premises.all()]

def get_hypothesis(self, problem):
"""Get hypothesis text."""
return problem.hypothesis.text

def get_extraData(self, problem):
"""Get dataset-specific extra data."""
match problem.dataset:
case Problem.Dataset.SICK:
return SickData.serialize(problem.extra_data)
case Problem.Dataset.FRACAS:
return FracasData.serialize(problem.extra_data)
case Problem.Dataset.SNLI:
return SNLIData.serialize(problem.extra_data)
case _:
return {}

def get_kbItems(self, problem):
"""Get knowledge base items."""
kb_items = problem.knowledge_bases.all()
return KnowledgeBaseSerializer(kb_items, many=True).data

def create(self, validated_data: dict) -> Problem:
"""
Create a new Problem instance from validated input data.
Handles creation of related Sentence and KnowledgeBase objects.
"""
premise_sentences = [
Sentence.objects.get_or_create(text=premise)[0]
for premise in validated_data["premises"]
]

hypothesis_sentence = Sentence.objects.get_or_create(
text=validated_data["hypothesis"]
)[0]

problem = Problem.objects.create(
hypothesis=hypothesis_sentence,
dataset=Problem.Dataset.USER,
# TODO: Determine entailment label based on LangPro parser output.
entailment_label=Problem.EntailmentLabel.UNKNOWN,
extra_data={},
)

problem.premises.set(premise_sentences)

kb_items = validated_data.get("kbItems", [])
if kb_items:
self._update_or_create_kb_items(problem, kb_items)

return problem

def update(self, instance: Problem, validated_data: dict) -> Problem:
"""
Update an existing Problem instance from validated input data.
Handles updating of related Sentence and KnowledgeBase objects.
"""
if instance.dataset != Problem.Dataset.USER:
raise serializers.ValidationError(
"Cannot update a problem that is not a user-created problem."
)

instance.hypothesis = Sentence.objects.get_or_create(
text=validated_data["hypothesis"],
)[0]
instance.save()

premise_sentences = [
Sentence.objects.get_or_create(text=premise)[0]
for premise in validated_data["premises"]
]
instance.premises.set(premise_sentences)

self._update_or_create_kb_items(instance, validated_data.get("kbItems", []))

return instance

def _update_or_create_kb_items(
self, problem: Problem, kb_items: list[dict]
) -> None:
"""Create or update KnowledgeBase items for a problem."""
kb_ids: list[int] = []
kb_serializer = KnowledgeBaseSerializer()

for item in kb_items:
kb_id = item.get("id", None)

if kb_id is None:
kb = kb_serializer.create_for_problem(item, problem=problem) # type: ignore
else:
kb_instance = KnowledgeBase.objects.get(id=kb_id, problem_id=problem.pk)
kb = kb_serializer.update(kb_instance, item)

kb_ids.append(kb.pk)

# Delete existing knowledge bases associated to this problem that are
# not included in the input.
KnowledgeBase.objects.filter(problem_id=problem.pk).exclude(
id__in=kb_ids
).delete()


class ProblemInputSerializer(serializers.Serializer):
"""
Serializer for validating problem input data.
This is used for both creating and updating user-created problems.
"""

id = serializers.IntegerField(required=False, allow_null=True)
premises = serializers.ListField(
child=serializers.CharField(allow_blank=False),
allow_empty=False,
help_text="List of premise sentence texts",
)
hypothesis = serializers.CharField(
allow_blank=False, help_text="Hypothesis sentence text"
)
kbItems = KnowledgeBaseSerializer(
many=True, allow_empty=True, help_text="List of knowledge base items"
)

def validate_id(self, value):
"""Validate that the Problem ID, if provided, exists and belongs to a user-created problem."""
if value is not None:
if not Problem.objects.filter(
id=value, dataset=Problem.Dataset.USER
).exists():
raise serializers.ValidationError(
f"Problem with ID {value} does not exist."
)
return value
Loading