Skip to content

Commit cf0fa5b

Browse files
Merge pull request #59 from CentreForDigitalHumanities/feature/derived-problems
Feature/derived problems
2 parents 3a6aa24 + 7188e7f commit cf0fa5b

31 files changed

Lines changed: 420 additions & 149 deletions

backend/problem/management/commands/import_fracas.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def _annotate_section_subsections(tree: ET.ElementTree) -> None:
4343

4444
root = tree.getroot()
4545

46+
if root is None:
47+
raise ValueError("The XML file is empty or malformed.")
48+
4649
for element in root:
4750
if element.tag == "comment" and element.attrib.get("class") == "section":
4851
current_section = element.text.strip() if element.text else None
@@ -107,15 +110,15 @@ def import_fracas_problems(self, fracas_path: str) -> None:
107110
)
108111
skipped += 1
109112
continue
110-
hypothesis = Sentence.objects.create(
113+
hypothesis = Sentence.objects.update_or_create(
111114
text=FracasData._text_from_element(hypothesis_node)
112-
)
115+
)[0]
113116

114117
premises = []
115118
premise_nodes = problem.findall("p")
116119
for node in premise_nodes:
117120
if node.text:
118-
premises.append(Sentence.objects.create(text=node.text.strip()))
121+
premises.append(Sentence.objects.update_or_create(text=node.text.strip())[0])
119122

120123

121124
entailment_label = self.ENTAILMENT_LABELS.get(

backend/problem/management/commands/import_sick.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@ def import_sick_problems(self, sick_path: str) -> None:
5757

5858
extra_data = SickData.import_data(problem)
5959

60-
premise = Sentence.objects.create(text=problem["sentence_A"])
61-
62-
hypothesis = Sentence.objects.create(text=problem["sentence_B"])
60+
premise = Sentence.objects.update_or_create(text=problem["sentence_A"])[0]
61+
hypothesis = Sentence.objects.update_or_create(text=problem["sentence_B"])[0]
6362

6463
problem = Problem.objects.create(
6564
dataset=Problem.Dataset.SICK,

backend/problem/management/commands/import_snli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import csv
2+
from typing import Literal
23

34
from django.core.management.base import BaseCommand
45
from tqdm import tqdm
@@ -53,7 +54,9 @@ def handle(self, *args, **options):
5354

5455
self.import_snli_problems(snli_paths)
5556

56-
def import_snli_problems(self, snli_paths: list[tuple[str, str]]) -> None:
57+
def import_snli_problems(
58+
self, snli_paths: list[tuple[Literal["dev", "train", "test"], str]]
59+
) -> None:
5760
"""
5861
Import SNLI 1.0 problems from a list of SNLI TSV files and enter them into the database.
5962
"""
@@ -99,9 +102,13 @@ def import_snli_problems(self, snli_paths: list[tuple[str, str]]) -> None:
99102

100103
extra_data = SNLIData.import_data(problem, subset)
101104

102-
premise = Sentence.objects.create(text=problem["sentence1"])
105+
premise = Sentence.objects.update_or_create(
106+
text=problem["sentence1"]
107+
)[0]
103108

104-
hypothesis = Sentence.objects.create(text=problem["sentence2"])
109+
hypothesis = Sentence.objects.update_or_create(
110+
text=problem["sentence2"]
111+
)[0]
105112

106113
new_problem = Problem.objects.create(
107114
dataset=Problem.Dataset.SNLI,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 4.2.20 on 2025-09-22 11:47
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("problem", "0004_sentence_remove_problem_premises_knowledgebase_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="problem",
16+
name="base",
17+
field=models.ForeignKey(
18+
blank=True,
19+
help_text="The base problem from which this problem was derived, if any.",
20+
null=True,
21+
on_delete=django.db.models.deletion.SET_NULL,
22+
related_name="derived_problems",
23+
to="problem.problem",
24+
),
25+
),
26+
]

backend/problem/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ class EntailmentLabel(models.TextChoices):
2828
default=Dataset.USER,
2929
)
3030

31+
base = models.ForeignKey(
32+
"self",
33+
on_delete=models.SET_NULL,
34+
null=True,
35+
blank=True,
36+
related_name="derived_problems",
37+
help_text="The base problem from which this problem was derived, if any.",
38+
)
39+
3140
premises = models.ManyToManyField(
3241
Sentence,
3342
related_name="premise_problems",
@@ -56,8 +65,6 @@ def get_index(self, qs: QuerySet) -> int | None:
5665
except Exception as e:
5766
logger.exception(f"Error getting index for problem {self.pk}: {e}")
5867
return None
59-
60-
6168
class KnowledgeBase(models.Model):
6269
class Relationship(models.TextChoices):
6370
EQUAL = "equal", "Equal"

backend/problem/serializers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Meta:
6262
"entailmentLabel",
6363
"extraData",
6464
"kbItems",
65+
"base",
6566
]
6667

6768
def get_premises(self, problem):
@@ -104,6 +105,7 @@ def create(self, validated_data: dict) -> Problem:
104105
)[0]
105106

106107
problem = Problem.objects.create(
108+
base_id=validated_data.get("base", None),
107109
hypothesis=hypothesis_sentence,
108110
dataset=Problem.Dataset.USER,
109111
# TODO: Determine entailment label based on LangPro parser output.
@@ -132,6 +134,19 @@ def update(self, instance: Problem, validated_data: dict) -> Problem:
132134
instance.hypothesis = Sentence.objects.get_or_create(
133135
text=validated_data["hypothesis"],
134136
)[0]
137+
138+
validated_base_id = validated_data.get("base", None)
139+
if validated_base_id is None:
140+
instance.base = None
141+
else:
142+
try:
143+
base_problem = Problem.objects.get(id=validated_base_id)
144+
except Problem.DoesNotExist:
145+
raise serializers.ValidationError(
146+
f"Base problem with ID {validated_base_id} does not exist."
147+
)
148+
instance.base = base_problem # type: ignore
149+
135150
instance.save()
136151

137152
premise_sentences = [
@@ -188,6 +203,8 @@ class ProblemInputSerializer(serializers.Serializer):
188203
many=True, allow_empty=True, help_text="List of knowledge base items"
189204
)
190205

206+
base = serializers.IntegerField(required=False, allow_null=True)
207+
191208
def validate_id(self, value):
192209
"""Validate that the Problem ID, if provided, exists and belongs to a user-created problem."""
193210
if value is not None:
@@ -198,3 +215,12 @@ def validate_id(self, value):
198215
f"Problem with ID {value} does not exist."
199216
)
200217
return value
218+
219+
def validate_base(self, value):
220+
"""Validate that the base problem ID exists if provided."""
221+
if value is not None:
222+
if not Problem.objects.filter(id=value).exists():
223+
raise serializers.ValidationError(
224+
f"Base problem with ID {value} does not exist."
225+
)
226+
return value

backend/problem/views/problem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _get_problem_response(self, request: Request, pk: int | None) -> Response:
5555
qs = self.get_queryset()
5656

5757
if filters is not None:
58-
qs = qs.filter(filters)
58+
qs = qs.filter(filters).distinct()
5959

6060
problem = None
6161
if pk is not None:
@@ -111,7 +111,7 @@ def _handle_update_create_problem(
111111
problem_serializer = ProblemSerializer()
112112

113113
if problem_id is None:
114-
problem = problem_serializer.create(validated_input)
114+
problem = problem_serializer.create(validated_input) # type: ignore
115115
status = HTTP_201_CREATED
116116
else:
117117
problem_instance = get_object_or_404(

frontend/src/app/annotate/annotate.component.html

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,39 @@
44
@let adding = appMode === 'add';
55
@let editing = appMode === 'edit';
66

7-
87
<div class="row mb-4">
98
<div class="d-flex flex-column col-4 gap-2">
109
@if (browsing) {
1110
<div class="card text-bg-light p-3">
1211
<la-navigator />
1312
<la-search />
1413
</div>
15-
} @else if (adding) {
14+
} @else if (adding || editing) {
1615
<section class="d-flex flex-column gap-2">
16+
@if (adding) {
1717
<p class="text-muted" i18n>
1818
You are currently adding a new problem.
1919
</p>
20-
<p class="text-muted" i18n>
21-
Hit 'Start Parse' to send your problem data to the parser and
22-
inspect the results.
23-
</p>
24-
<p class="text-muted" i18n>
25-
If you are happy with the changes, save the problem by clicking
26-
the 'Save' button.
27-
</p>
28-
<p class="text-muted" i18n>
29-
This will add the problem to the database.
30-
</p>
31-
</section>
32-
} @else if (editing) {
33-
<section>
20+
} @else {
3421
<p class="text-muted" i18n>
3522
You are currently editing an existing problem.
3623
</p>
24+
}
3725
<p class="text-muted" i18n>
3826
Hit 'Start Parse' to send your problem data to the parser and
3927
inspect the results.
4028
</p>
4129
<p class="text-muted" i18n>
42-
If you are happy with the changes, save the changes by clicking
30+
If you are happy with the changes, save the problem by clicking
4331
the 'Save problem' button.
4432
</p>
45-
@if (isUserProblem$ | async) {
33+
@if (adding) {
4634
<p class="text-muted" i18n>
47-
This will update this problem in the database.
35+
This will add the problem to the database.
4836
</p>
4937
} @else {
5038
<p class="text-muted" i18n>
51-
This will create a new problem in the database, leaving the
52-
original problem unchanged.
39+
This will update this problem in the database.
5340
</p>
5441
}
5542
</section>

frontend/src/app/annotate/annotate.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { combineLatest, distinctUntilChanged, map } from "rxjs";
1111
import { CommonModule } from "@angular/common";
1212
import { Dataset } from "@/types";
1313
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
14+
import areParamsEqual from "@/shared/areParamsEqual";
1415

1516
@Component({
1617
selector: "la-annotate",
@@ -54,6 +55,7 @@ export class AnnotateComponent implements OnInit {
5455
editParam$
5556
])
5657
.pipe(
58+
distinctUntilChanged((oldParams, newParams) => areParamsEqual(oldParams, newParams)),
5759
takeUntilDestroyed(this.destroyRef)
5860
)
5961
.subscribe(([params, queryParams, edit]) => {

frontend/src/app/annotate/annotation-input/annotation-input.component.html

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,35 @@
1212
<la-knowledge-base-form [form]="form" />
1313
</div>
1414
<div class="d-flex justify-content-between mt-3">
15-
<button
16-
type="button"
17-
class="btn btn-success d-flex align-items-center justify-content-center"
15+
<la-icon-button
16+
buttonStyle="success"
17+
[icon]="faTree"
18+
label="Start parse"
1819
(click)="startParse()"
20+
/>
21+
@if (appMode === 'browse') {
22+
<a
23+
class="btn btn-secondary d-flex align-items-center justify-content-center"
24+
[routerLink]="['/', 'annotate', 'new']"
25+
[queryParams]="{base: form.getRawValue().id }"
26+
1927
>
2028
<fa-icon
21-
[icon]="faTree"
29+
[icon]="faCopy"
2230
class="fa-md"
2331
aria-hidden="true"
2432
focusable="false"
2533
/>
26-
<span class="ms-2" i18n>Start parse</span>
27-
</button>
34+
<span class="ms-2" i18n>Copy problem</span>
35+
</a>
36+
}
2837
@if ((appMode === 'edit' || appMode === 'add') && userProblem) {
29-
<button
30-
type="button"
31-
class="btn btn-primary d-flex align-items-center justify-content-center"
38+
<la-icon-button
39+
buttonStyle="primary"
40+
[icon]="faFloppyDisk"
41+
label="Save problem"
3242
(click)="saveProblem()"
33-
>
34-
<fa-icon
35-
[icon]="faFloppyDisk"
36-
aria-hidden="true"
37-
focusable="false"
38-
></fa-icon>
39-
<span class="ms-3" i18n>Save problem</span>
40-
</button>
43+
/>
4144
} @else if (userProblem) {
4245
<a
4346
class="btn btn-secondary d-flex align-items-center justify-content-center"

0 commit comments

Comments
 (0)