diff --git a/backend/BiLSTM.py b/backend/BiLSTM.py
index a0cff1b..aff9176 100644
--- a/backend/BiLSTM.py
+++ b/backend/BiLSTM.py
@@ -8,10 +8,10 @@
from __future__ import print_function
from util import BIOF1Validation
-import keras
-from keras.optimizers import *
-from keras.models import Model
-from keras.layers import *
+from tensorflow import keras
+from tensorflow.keras.optimizers import *
+from tensorflow.keras.models import Model
+from tensorflow.keras.layers import *
import math
import numpy as np
import sys
@@ -243,7 +243,7 @@ def buildModel(self):
elif self.params['optimizer'].lower() == 'adagrad':
opt = Adagrad(**optimizerParams)
elif self.params['optimizer'].lower() == 'sgd':
- opt = SGD(lr=0.1, **optimizerParams)
+ opt = SGD(learning_rate=0.1, **optimizerParams)
model = Model(inputs=inputNodes, outputs=[output])
diff --git a/backend/Dockerfile b/backend/Dockerfile
index a88a919..3a2197e 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,47 +1,40 @@
-# Use an official Python runtime as a parent image
-FROM python:3.5
+FROM python:3.11-slim@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf
-# Add all Data
ADD . /
-# Set the working directory to /
WORKDIR /
+RUN apt-get update && apt-get install -y --no-install-recommends wget git && rm -rf /var/lib/apt/lists/*
+
# Install any needed packages specified in requirements.txt
-RUN pip install -r requirements.txt
-RUN pip install torch==0.4.1 -f https://download.pytorch.org/whl/torch_stable.html
-RUN pip install torchvision==0.2.1 -f https://download.pytorch.org/whl/torch_stable.html
+RUN pip install --no-cache-dir -r requirements.txt
+RUN python -m nltk.downloader -d /usr/local/nltk_data punkt_tab
RUN git clone https://github.com/UKPLab/emnlp2017-bilstm-cnn-crf.git
RUN mv backend.py emnlp2017-bilstm-cnn-crf/ && mv Model.py emnlp2017-bilstm-cnn-crf/ && mv ModelNewES.py emnlp2017-bilstm-cnn-crf/ && mv ModelNewWD.py emnlp2017-bilstm-cnn-crf/ && mv Segmenter.py emnlp2017-bilstm-cnn-crf/
-# Download the .h5 file to the models directory
# Download the .h5 file
-RUN wget -q --show-progress -O models/IBM.h5 "https://huggingface.co/debela-arg/segmenter/resolve/main/IBM.h5?download=true" || \
+RUN wget -q -O models/IBM.h5 "https://huggingface.co/debela-arg/segmenter/resolve/main/IBM.h5?download=true" || \
(echo "Download failed! Check URL or authentication." && exit 1)
RUN mv models/* emnlp2017-bilstm-cnn-crf/models/
RUN mv -f BiLSTM.py emnlp2017-bilstm-cnn-crf/neuralnets/
+# Patch cloned repo keras imports for TF 2.x compatibility
+RUN sed -i \
+ -e 's/^import keras/from tensorflow import keras/' \
+ -e 's/^from keras import/from tensorflow.keras import/' \
+ -e 's/^from keras.engine import Layer, InputSpec/from tensorflow.keras.layers import Layer, InputSpec/' \
+ -e 's/self.add_weight((/self.add_weight(shape=(/' \
+ emnlp2017-bilstm-cnn-crf/neuralnets/keraslayers/ChainCRF.py
+
RUN mkdir emnlp2017-bilstm-cnn-crf/lstm
RUN git clone https://github.com/achernodub/bilstm-cnn-crf-tagger.git emnlp2017-bilstm-cnn-crf/lstm
-RUN pip install prometheus-flask-exporter==0.1.2
-# Make port 6000 available to the world outside this container
EXPOSE 6000
WORKDIR /emnlp2017-bilstm-cnn-crf
-# Run app.py when the container launches
-CMD ["python3", "backend.py"]
-
-
-
-
-
-
-
-
-
+CMD ["gunicorn", "--workers", "1", "--bind", "0.0.0.0:6000", "backend:app"]
diff --git a/backend/Segmenter.py b/backend/Segmenter.py
index 5f0662c..888c1a6 100644
--- a/backend/Segmenter.py
+++ b/backend/Segmenter.py
@@ -368,10 +368,8 @@ def cascading_anaphora_propositionalizer(self, path):
if path.endswith("json"):
is_json_file=self.is_json(path)
if is_json_file:
- data = open(path).read()
- null = None
- false = False
- extended_json_aif = eval(data)
+ data = open(path).read()
+ extended_json_aif = json.loads(data)
json_aif = json_dict = extended_json_aif['AIF']
if 'nodes' in json_dict and 'locutions' in json_dict and 'edges' in json_dict:
diff --git a/backend/backend.py b/backend/backend.py
index 678f4a8..abef81d 100644
--- a/backend/backend.py
+++ b/backend/backend.py
@@ -1,26 +1,16 @@
#!/usr/bin/env python3
"""be.py: Description."""
-from flask import Flask, jsonify, request
-from flasgger import Swagger, LazyString, LazyJSONEncoder
+from flask import Flask, jsonify, request, make_response
from flask_restful import Api, Resource, reqparse
-from flask import make_response
-from nltk.tokenize import sent_tokenize, word_tokenize
-import random
-import json
-from flask import jsonify
+from flask_cors import CORS
import json
import logging
from prometheus_flask_exporter import PrometheusMetrics
app = Flask(__name__)
-#app.json_encoder = LazyJSONEncoder
-
-
-# Initialize Prometheus metrics
-#metrics = PrometheusMetrics(app)
+CORS(app, resources={r"/*": {"origins": "https://arg-tech.github.io"}})
-# group by endpoint rather than path
metrics = PrometheusMetrics(app)
@app.route('/collection/:collection_id/item/:item_id')
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 2363b37..ee998e3 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,10 +1,65 @@
-Flask
-flasgger
-flask_restful
-nltk==3.4.5
-tensorflow==1.5.0
-keras==2.1.5
-numpy==1.17.3
-scipy==1.3.1
-h5py
-Jinja2
\ No newline at end of file
+absl-py==2.4.0
+aniso8601==10.0.1
+astunparse==1.6.3
+attrs==25.4.0
+blinker==1.9.0
+certifi==2026.2.25
+cffi==2.0.0
+charset-normalizer==3.4.4
+click==8.3.1
+cryptography==46.0.5
+flasgger==0.9.7.1
+Flask==3.1.3
+flask-cors==6.0.2
+Flask-RESTful==0.3.10
+flatbuffers==25.12.19
+gast==0.7.0
+google-auth==2.48.0
+google-auth-oauthlib==1.2.4
+google-pasta==0.2.0
+grpcio==1.78.1
+gunicorn==25.1.0
+h5py==3.15.1
+idna==3.11
+itsdangerous==2.2.0
+Jinja2==3.1.6
+joblib==1.5.3
+jsonschema==4.26.0
+jsonschema-specifications==2025.9.1
+keras==2.15.0
+libclang==18.1.1
+Markdown==3.10.2
+MarkupSafe==3.0.3
+mistune==3.2.0
+ml-dtypes==0.3.2
+nltk==3.9.3
+numpy==1.26.4
+oauthlib==3.3.1
+opt_einsum==3.4.0
+packaging==26.0
+prometheus_client==0.24.1
+prometheus_flask_exporter==0.23.2
+protobuf==4.25.8
+pyasn1==0.6.2
+pyasn1_modules==0.4.2
+pycparser==3.0
+pytz==2025.2
+PyYAML==6.0.3
+referencing==0.37.0
+regex==2026.2.19
+requests==2.32.5
+requests-oauthlib==2.0.0
+rpds-py==0.30.0
+rsa==4.9.1
+six==1.17.0
+tensorboard==2.15.2
+tensorboard-data-server==0.7.2
+tensorflow==2.15.1
+tensorflow-estimator==2.15.0
+tensorflow-io-gcs-filesystem==0.37.1
+termcolor==3.3.0
+tqdm==4.67.3
+typing_extensions==4.15.0
+urllib3==2.6.3
+Werkzeug==3.1.6
+wrapt==1.14.2
diff --git a/docker-compose.yml b/docker-compose.yml
index d9c055a..a0877f1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,3 @@
-version: "3"
services:
backend:
build: ./backend/
diff --git a/tests/api-requests/Targer/bruno.json b/tests/api-requests/Targer/bruno.json
new file mode 100644
index 0000000..fb3d3db
--- /dev/null
+++ b/tests/api-requests/Targer/bruno.json
@@ -0,0 +1,5 @@
+{
+ "version": "1",
+ "name": "Targer",
+ "type": "collection"
+}
diff --git a/tests/api-requests/Targer/environments/(1) local.bru b/tests/api-requests/Targer/environments/(1) local.bru
new file mode 100644
index 0000000..6075cd1
--- /dev/null
+++ b/tests/api-requests/Targer/environments/(1) local.bru
@@ -0,0 +1,3 @@
+vars {
+ baseUrl: http://localhost:10600
+}
diff --git a/tests/api-requests/Targer/environments/(2) staging.bru b/tests/api-requests/Targer/environments/(2) staging.bru
new file mode 100644
index 0000000..5d79023
--- /dev/null
+++ b/tests/api-requests/Targer/environments/(2) staging.bru
@@ -0,0 +1,3 @@
+vars {
+ baseUrl: http://targer.amfws.staging.arg.tech
+}
diff --git a/tests/api-requests/Targer/environments/(3) production.bru b/tests/api-requests/Targer/environments/(3) production.bru
new file mode 100644
index 0000000..574d9a6
--- /dev/null
+++ b/tests/api-requests/Targer/environments/(3) production.bru
@@ -0,0 +1,3 @@
+vars {
+ baseUrl: http://targer.amfws.arg.tech
+}
diff --git a/tests/api-requests/Targer/targer-am POST.bru b/tests/api-requests/Targer/targer-am POST.bru
new file mode 100644
index 0000000..0e9fe55
--- /dev/null
+++ b/tests/api-requests/Targer/targer-am POST.bru
@@ -0,0 +1,104 @@
+meta {
+ name: targer-am POST
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{baseUrl}}/targer-am
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/json-aif.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF envelope", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ expect(data.AIF).to.have.property('locutions');
+ });
+
+ test("should preserve all original I-nodes", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const iNodes = nodes.filter(n => n.type === 'I');
+ // Original input has 5 I-nodes
+ expect(iNodes.length).to.be.at.least(5);
+ const iTexts = iNodes.map(n => n.text.trim());
+ expect(iTexts).to.include('We should go eat.');
+ expect(iTexts).to.include('Because I\'m hungry');
+ });
+
+ test("should preserve all original L-nodes and YA-nodes", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const lNodes = nodes.filter(n => n.type === 'L');
+ const yaNodes = nodes.filter(n => n.type === 'YA');
+ expect(lNodes.length).to.be.at.least(5);
+ expect(yaNodes.length).to.be.at.least(5);
+ });
+
+ test("every edge should reference existing nodeIDs", function() {
+ const data = res.getBody();
+ const nodeIDs = new Set(data.AIF.nodes.map(n => n.nodeID));
+ for (const edge of data.AIF.edges) {
+ expect(nodeIDs.has(edge.fromID), `fromID ${edge.fromID} not in nodes`).to.be.true;
+ expect(nodeIDs.has(edge.toID), `toID ${edge.toID} not in nodes`).to.be.true;
+ }
+ });
+
+ test("every node should have required fields", function() {
+ const data = res.getBody();
+ for (const node of data.AIF.nodes) {
+ expect(node).to.have.property('nodeID');
+ expect(node).to.have.property('text');
+ expect(node).to.have.property('type');
+ }
+ });
+
+ test("every edge should have required fields", function() {
+ const data = res.getBody();
+ for (const edge of data.AIF.edges) {
+ expect(edge).to.have.property('edgeID');
+ expect(edge).to.have.property('fromID');
+ expect(edge).to.have.property('toID');
+ }
+ });
+
+ test("RA/CA relation nodes should have proper edge structure", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const edges = data.AIF.edges;
+ const relationNodes = nodes.filter(n => n.type === 'RA' || n.type === 'CA');
+
+ for (const rNode of relationNodes) {
+ // Each RA/CA node should have at least one incoming and one outgoing edge
+ const incoming = edges.filter(e => e.toID === rNode.nodeID);
+ const outgoing = edges.filter(e => e.fromID === rNode.nodeID);
+ expect(incoming.length, `RA/CA node ${rNode.nodeID} should have incoming edge`).to.be.at.least(1);
+ expect(outgoing.length, `RA/CA node ${rNode.nodeID} should have outgoing edge`).to.be.at.least(1);
+ }
+ });
+
+ test("node IDs should be unique", function() {
+ const data = res.getBody();
+ const nodeIDs = data.AIF.nodes.map(n => n.nodeID);
+ const uniqueIDs = new Set(nodeIDs);
+ expect(uniqueIDs.size).to.equal(nodeIDs.length);
+ });
+
+ test("edge IDs should be unique", function() {
+ const data = res.getBody();
+ const edgeIDs = data.AIF.edges.map(e => e.edgeID);
+ const uniqueIDs = new Set(edgeIDs);
+ expect(uniqueIDs.size).to.equal(edgeIDs.length);
+ });
+}
diff --git a/tests/api-requests/Targer/targer-am argumentative POST.bru b/tests/api-requests/Targer/targer-am argumentative POST.bru
new file mode 100644
index 0000000..e7b1e13
--- /dev/null
+++ b/tests/api-requests/Targer/targer-am argumentative POST.bru
@@ -0,0 +1,77 @@
+meta {
+ name: targer-am argumentative POST
+ type: http
+ seq: 4
+}
+
+post {
+ url: {{baseUrl}}/targer-am
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/argumentative-aif.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF with AIF section", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ expect(data.AIF).to.have.property('locutions');
+ });
+
+ test("should preserve both original I-nodes", function() {
+ const data = res.getBody();
+ const iNodes = data.AIF.nodes.filter(n => n.type === 'I');
+ expect(iNodes.length).to.be.at.least(2);
+ });
+
+ test("should have more nodes/edges than input (relations added)", function() {
+ const data = res.getBody();
+ // Input has 6 nodes and 4 edges; AM should add RA/CA nodes + edges
+ const nodes = data.AIF.nodes;
+ const edges = data.AIF.edges;
+ expect(nodes.length).to.be.at.least(6);
+ expect(edges.length).to.be.at.least(4);
+ });
+
+ test("RA nodes should link between I-nodes via edges", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const edges = data.AIF.edges;
+ const raNodes = nodes.filter(n => n.type === 'RA');
+
+ for (const ra of raNodes) {
+ expect(ra.text).to.equal('Default Inference');
+ // RA node should have an incoming edge from an I-node
+ const incoming = edges.filter(e => e.toID === ra.nodeID);
+ expect(incoming.length).to.be.at.least(1);
+ // RA node should have an outgoing edge to an I-node
+ const outgoing = edges.filter(e => e.fromID === ra.nodeID);
+ expect(outgoing.length).to.be.at.least(1);
+ }
+ });
+
+ test("all node types should be valid AIF types", function() {
+ const data = res.getBody();
+ const validTypes = ['L', 'I', 'YA', 'RA', 'CA', 'MA', 'TA', 'PA'];
+ for (const node of data.AIF.nodes) {
+ expect(validTypes).to.include(node.type);
+ }
+ });
+
+ test("every edge should reference existing nodes", function() {
+ const data = res.getBody();
+ const nodeIDs = new Set(data.AIF.nodes.map(n => n.nodeID));
+ for (const edge of data.AIF.edges) {
+ expect(nodeIDs.has(edge.fromID), `fromID ${edge.fromID} missing`).to.be.true;
+ expect(nodeIDs.has(edge.toID), `toID ${edge.toID} missing`).to.be.true;
+ }
+ });
+}
diff --git a/tests/api-requests/Targer/targer-am single-proposition POST.bru b/tests/api-requests/Targer/targer-am single-proposition POST.bru
new file mode 100644
index 0000000..842c9dc
--- /dev/null
+++ b/tests/api-requests/Targer/targer-am single-proposition POST.bru
@@ -0,0 +1,50 @@
+meta {
+ name: targer-am single-proposition POST
+ type: http
+ seq: 5
+}
+
+post {
+ url: {{baseUrl}}/targer-am
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/single-proposition-aif.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ });
+
+ test("should not add RA/CA nodes for a single proposition", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const relationNodes = nodes.filter(n => n.type === 'RA' || n.type === 'CA');
+ // With only one I-node there are no pairs to compare, so no relations
+ expect(relationNodes.length).to.equal(0);
+ });
+
+ test("should preserve the original graph unchanged", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const edges = data.AIF.edges;
+ // Original: 3 nodes (L, I, YA) and 2 edges
+ expect(nodes.length).to.equal(3);
+ expect(edges.length).to.equal(2);
+ });
+
+ test("original node types should be preserved", function() {
+ const data = res.getBody();
+ const types = data.AIF.nodes.map(n => n.type).sort();
+ expect(types).to.deep.equal(['I', 'L', 'YA']);
+ });
+}
diff --git a/tests/api-requests/Targer/targer-segmenter GET.bru b/tests/api-requests/Targer/targer-segmenter GET.bru
new file mode 100644
index 0000000..fdc417d
--- /dev/null
+++ b/tests/api-requests/Targer/targer-segmenter GET.bru
@@ -0,0 +1,33 @@
+meta {
+ name: targer-segmenter GET
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{baseUrl}}/targer-segmenter
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return service description", function() {
+ const body = res.getBody();
+ expect(body).to.be.a('string');
+ expect(body).to.include('Segmenter');
+ expect(body).to.include('AMF');
+ expect(body).to.include('propositions');
+ });
+
+ test("should describe input/output format", function() {
+ const body = res.getBody();
+ expect(body).to.include('xIAF');
+ });
+
+ test("should mention the BIO labeling scheme", function() {
+ const body = res.getBody();
+ expect(body).to.include('BIO');
+ });
+}
diff --git a/tests/api-requests/Targer/targer-segmenter POST.bru b/tests/api-requests/Targer/targer-segmenter POST.bru
new file mode 100644
index 0000000..9b225df
--- /dev/null
+++ b/tests/api-requests/Targer/targer-segmenter POST.bru
@@ -0,0 +1,77 @@
+meta {
+ name: targer-segmenter POST
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{baseUrl}}/targer-segmenter
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/propositionUnitizer.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF envelope", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ expect(data.AIF).to.have.property('locutions');
+ });
+
+ test("every node should have required fields", function() {
+ const data = res.getBody();
+ for (const node of data.AIF.nodes) {
+ expect(node).to.have.property('nodeID');
+ expect(node).to.have.property('text');
+ expect(node).to.have.property('type');
+ }
+ });
+
+ test("every edge should reference existing nodeIDs", function() {
+ const data = res.getBody();
+ const nodeIDs = new Set(data.AIF.nodes.map(n => n.nodeID));
+ for (const edge of data.AIF.edges) {
+ expect(nodeIDs.has(edge.fromID), `fromID ${edge.fromID} not in nodes`).to.be.true;
+ expect(nodeIDs.has(edge.toID), `toID ${edge.toID} not in nodes`).to.be.true;
+ }
+ });
+
+ test("node IDs should be unique", function() {
+ const data = res.getBody();
+ const nodeIDs = data.AIF.nodes.map(n => n.nodeID);
+ const uniqueIDs = new Set(nodeIDs);
+ expect(uniqueIDs.size).to.equal(nodeIDs.length);
+ });
+
+ test("edge IDs should be unique", function() {
+ const data = res.getBody();
+ const edgeIDs = data.AIF.edges.map(e => e.edgeID);
+ const uniqueIDs = new Set(edgeIDs);
+ expect(uniqueIDs.size).to.equal(edgeIDs.length);
+ });
+
+ test("all node types should be valid AIF types", function() {
+ const data = res.getBody();
+ const validTypes = ['L', 'I', 'YA', 'RA', 'CA', 'MA', 'TA', 'PA'];
+ for (const node of data.AIF.nodes) {
+ expect(validTypes).to.include(node.type);
+ }
+ });
+
+ test("each L-node should have a corresponding locution entry", function() {
+ const data = res.getBody();
+ const lNodeIDs = new Set(data.AIF.nodes.filter(n => n.type === 'L').map(n => n.nodeID));
+ const locutionNodeIDs = new Set(data.AIF.locutions.map(l => l.nodeID));
+ for (const lID of lNodeIDs) {
+ expect(locutionNodeIDs.has(lID), `L-node ${lID} missing locution`).to.be.true;
+ }
+ });
+}
diff --git a/tests/api-requests/Targer/targer-segmenter monologue POST.bru b/tests/api-requests/Targer/targer-segmenter monologue POST.bru
new file mode 100644
index 0000000..b354356
--- /dev/null
+++ b/tests/api-requests/Targer/targer-segmenter monologue POST.bru
@@ -0,0 +1,74 @@
+meta {
+ name: targer-segmenter monologue POST
+ type: http
+ seq: 6
+}
+
+post {
+ url: {{baseUrl}}/targer-segmenter
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/monologue-segmenter.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ expect(data.AIF).to.have.property('locutions');
+ });
+
+ test("segmenter should produce L-nodes from the input", function() {
+ const data = res.getBody();
+ const lNodes = data.AIF.nodes.filter(n => n.type === 'L');
+ // The input has one long L-node with multiple argument components;
+ // the segmenter should either keep it or split it into segments
+ expect(lNodes.length).to.be.at.least(1);
+ });
+
+ test("every new L-node should have a YA illocuting node connected", function() {
+ const data = res.getBody();
+ const nodes = data.AIF.nodes;
+ const edges = data.AIF.edges;
+ const yaNodes = nodes.filter(n => n.type === 'YA');
+
+ for (const ya of yaNodes) {
+ // Each YA should have at least one incoming and one outgoing edge
+ const incoming = edges.filter(e => e.toID === ya.nodeID);
+ const outgoing = edges.filter(e => e.fromID === ya.nodeID);
+ expect(incoming.length, `YA ${ya.nodeID} needs incoming edge`).to.be.at.least(1);
+ }
+ });
+
+ test("every edge should reference existing nodes", function() {
+ const data = res.getBody();
+ const nodeIDs = new Set(data.AIF.nodes.map(n => n.nodeID));
+ for (const edge of data.AIF.edges) {
+ expect(nodeIDs.has(edge.fromID), `fromID ${edge.fromID} missing`).to.be.true;
+ expect(nodeIDs.has(edge.toID), `toID ${edge.toID} missing`).to.be.true;
+ }
+ });
+
+ test("node IDs should be unique", function() {
+ const data = res.getBody();
+ const nodeIDs = data.AIF.nodes.map(n => n.nodeID);
+ expect(new Set(nodeIDs).size).to.equal(nodeIDs.length);
+ });
+
+ test("L-node texts should be non-empty strings", function() {
+ const data = res.getBody();
+ const lNodes = data.AIF.nodes.filter(n => n.type === 'L');
+ for (const node of lNodes) {
+ expect(node.text).to.be.a('string');
+ expect(node.text.trim().length).to.be.greaterThan(0);
+ }
+ });
+}
diff --git a/tests/api-requests/Targer/targer-segmenter multi-speaker POST.bru b/tests/api-requests/Targer/targer-segmenter multi-speaker POST.bru
new file mode 100644
index 0000000..3cf0db6
--- /dev/null
+++ b/tests/api-requests/Targer/targer-segmenter multi-speaker POST.bru
@@ -0,0 +1,74 @@
+meta {
+ name: targer-segmenter multi-speaker POST
+ type: http
+ seq: 7
+}
+
+post {
+ url: {{baseUrl}}/targer-segmenter
+ body: multipartForm
+}
+
+body:multipart-form {
+ file: @file(test-inputs/multi-speaker-segmenter.json)
+}
+
+assert {
+ res.status: eq 200
+}
+
+tests {
+ test("should return valid xAIF", function() {
+ const data = res.getBody();
+ expect(data).to.have.property('AIF');
+ expect(data.AIF).to.have.property('nodes');
+ expect(data.AIF).to.have.property('edges');
+ expect(data.AIF).to.have.property('locutions');
+ });
+
+ test("should have L-nodes in the output", function() {
+ const data = res.getBody();
+ const lNodes = data.AIF.nodes.filter(n => n.type === 'L');
+ // Input has 3 L-nodes with compound arguments; segmenter may split them
+ expect(lNodes.length).to.be.at.least(1);
+ });
+
+ test("should preserve participant information", function() {
+ const data = res.getBody();
+ // The segmenter should not lose locution entries
+ expect(data.AIF.locutions.length).to.be.at.least(1);
+ });
+
+ test("speaker IDs in locutions should reference original participants", function() {
+ const data = res.getBody();
+ // At least some locutions should have personIDs from the original input
+ const personIDs = data.AIF.locutions.map(l => l.personID);
+ const hasValidSpeaker = personIDs.some(id => id === 0 || id === 1);
+ expect(hasValidSpeaker).to.be.true;
+ });
+
+ test("all node types should be valid AIF types", function() {
+ const data = res.getBody();
+ const validTypes = ['L', 'I', 'YA', 'RA', 'CA', 'MA', 'TA', 'PA'];
+ for (const node of data.AIF.nodes) {
+ expect(validTypes).to.include(node.type);
+ }
+ });
+
+ test("every edge should reference existing nodeIDs", function() {
+ const data = res.getBody();
+ const nodeIDs = new Set(data.AIF.nodes.map(n => n.nodeID));
+ for (const edge of data.AIF.edges) {
+ expect(nodeIDs.has(edge.fromID), `fromID ${edge.fromID} not in nodes`).to.be.true;
+ expect(nodeIDs.has(edge.toID), `toID ${edge.toID} not in nodes`).to.be.true;
+ }
+ });
+
+ test("node and edge IDs should be unique", function() {
+ const data = res.getBody();
+ const nodeIDs = data.AIF.nodes.map(n => n.nodeID);
+ expect(new Set(nodeIDs).size).to.equal(nodeIDs.length);
+ const edgeIDs = data.AIF.edges.map(e => e.edgeID);
+ expect(new Set(edgeIDs).size).to.equal(edgeIDs.length);
+ });
+}
diff --git a/tests/api-requests/Targer/test-inputs/argumentative-aif.json b/tests/api-requests/Targer/test-inputs/argumentative-aif.json
new file mode 100644
index 0000000..04fdd63
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/argumentative-aif.json
@@ -0,0 +1,14 @@
+{"AIF": {"descriptorfulfillments": null,
+"edges": [{"edgeID": 1, "fromID": 1, "toID": 4}, {"edgeID": 2, "fromID": 4, "toID": 3},
+ {"edgeID": 3, "fromID": 5, "toID": 8}, {"edgeID": 4, "fromID": 8, "toID": 7}],
+ "locutions": [
+ {"nodeID": 1, "personID": "Alice:"},
+ {"nodeID": 5, "personID": "Bob:"}],
+ "nodes": [
+ {"nodeID": 1, "text": "We should ban smoking in all public places because it harms non-smokers.", "type": "L"},
+ {"nodeID": 3, "text": "We should ban smoking in all public places because it harms non-smokers.", "type": "I"},
+ {"nodeID": 4, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 5, "text": "Second-hand smoke is a proven cause of lung cancer and heart disease in non-smokers.", "type": "L"},
+ {"nodeID": 7, "text": "Second-hand smoke is a proven cause of lung cancer and heart disease in non-smokers.", "type": "I"},
+ {"nodeID": 8, "text": "Default Illocuting", "type": "YA"}],
+ "participants": null, "schemefulfillments": null}, "OVA": [], "dialog": true, "text": ""}
\ No newline at end of file
diff --git a/tests/api-requests/Targer/test-inputs/json-aif.json b/tests/api-requests/Targer/test-inputs/json-aif.json
new file mode 100644
index 0000000..1878377
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/json-aif.json
@@ -0,0 +1,28 @@
+{"AIF": {"descriptorfulfillments": null,
+"edges": [{"edgeID": 5, "fromID": 1, "toID": 4},
+{"edgeID": 6, "fromID": 4, "toID": 3}, {"edgeID": 13, "fromID": 9, "toID": 12},
+{"edgeID": 14, "fromID": 12, "toID": 11}, {"edgeID": 21, "fromID": 17, "toID": 20},
+{"edgeID": 22, "fromID": 20, "toID": 19}, {"edgeID": 29, "fromID": 25, "toID": 28},
+ {"edgeID": 30, "fromID": 28, "toID": 27}, {"edgeID": 37, "fromID": 33, "toID": 36},
+ {"edgeID": 38, "fromID": 36, "toID": 35}],
+ "locutions": [
+ {"nodeID": 1, "personID": "Bob:"}, {"nodeID": 9, "personID": "Wilma:"},
+ {"nodeID": 17, "personID": "Bob:"}, {"nodeID": 25, "personID": "Wilma:"},
+ {"nodeID": 33, "personID": "Bob:"}],
+ "nodes": [
+ {"nodeID": 1, "text": " We should go eat. ", "type": "L"},
+ {"nodeID": 3, "text": " We should go eat. ", "type": "I"},
+ {"nodeID": 4, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 9, "text": " Why? ", "type": "L"},
+ {"nodeID": 11, "text": " Why? ", "type": "I"},
+ {"nodeID": 12, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 17, "text": " Because I'm hungry ", "type": "L"},
+ {"nodeID": 19, "text": " Because I'm hungry ", "type": "I"},
+ {"nodeID": 20, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 25, "text": " Yeah me too.", "type": "L"},
+ {"nodeID": 27, "text": " Yeah me too.", "type": "I"},
+ {"nodeID": 28, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 33, "text": " So let's eat.", "type": "L"},
+ {"nodeID": 35, "text": " So let's eat.", "type": "I"},
+ {"nodeID": 36, "text": "Default Illocuting", "type": "YA"}],
+ "participants": null, "schemefulfillments": null}, "OVA": [], "dialog": true, "text": " Bob: We should go eat. .
Wilma: Why? .
Bob: Because I'm hungry .
Wilma: Yeah me too..
Bob: So let's eat..
"}
\ No newline at end of file
diff --git a/tests/api-requests/Targer/test-inputs/monologue-segmenter.json b/tests/api-requests/Targer/test-inputs/monologue-segmenter.json
new file mode 100644
index 0000000..84ab88b
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/monologue-segmenter.json
@@ -0,0 +1,9 @@
+{"AIF": {"descriptorfulfillments": null,
+"edges": [{"edgeID": 0, "fromID": 0, "toID": 2}, {"edgeID": 1, "fromID": 2, "toID": 1}],
+ "locutions": [{"nodeID": 0, "personID": 0}],
+ "nodes": [
+ {"nodeID": 0, "text": "We should invest more in renewable energy because fossil fuels cause climate change and renewable sources create more jobs than traditional energy.", "type": "L"},
+ {"nodeID": 1, "text": "We should invest more in renewable energy because fossil fuels cause climate change and renewable sources create more jobs than traditional energy.", "type": "I"},
+ {"nodeID": 2, "text": "Default Illocuting", "type": "YA"}],
+ "participants": [{"firstname": "Alice", "participantID": 0, "surname": "None"}],
+ "schemefulfillments": null}, "OVA": [], "dialog": true, "text": ""}
\ No newline at end of file
diff --git a/tests/api-requests/Targer/test-inputs/multi-speaker-segmenter.json b/tests/api-requests/Targer/test-inputs/multi-speaker-segmenter.json
new file mode 100644
index 0000000..b6041fc
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/multi-speaker-segmenter.json
@@ -0,0 +1,18 @@
+{"AIF": {"descriptorfulfillments": null,
+"edges": [
+ {"edgeID": 0, "fromID": 0, "toID": 6}, {"edgeID": 1, "fromID": 6, "toID": 5},
+ {"edgeID": 2, "fromID": 1, "toID": 8}, {"edgeID": 3, "fromID": 8, "toID": 7},
+ {"edgeID": 4, "fromID": 2, "toID": 10}, {"edgeID": 5, "fromID": 10, "toID": 9}],
+ "locutions": [{"nodeID": 0, "personID": 0}, {"nodeID": 1, "personID": 1}, {"nodeID": 2, "personID": 0}],
+ "nodes": [
+ {"nodeID": 0, "text": "Governments should fund universal healthcare because it reduces poverty and improves productivity.", "type": "L"},
+ {"nodeID": 1, "text": "Private healthcare delivers better quality through competition and it gives patients more choice.", "type": "L"},
+ {"nodeID": 2, "text": "Studies show that universal systems have better outcomes per dollar spent than private ones.", "type": "L"},
+ {"nodeID": 5, "text": "Governments should fund universal healthcare because it reduces poverty and improves productivity.", "type": "I"},
+ {"nodeID": 6, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 7, "text": "Private healthcare delivers better quality through competition and it gives patients more choice.", "type": "I"},
+ {"nodeID": 8, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 9, "text": "Studies show that universal systems have better outcomes per dollar spent than private ones.", "type": "I"},
+ {"nodeID": 10, "text": "Default Illocuting", "type": "YA"}],
+ "participants": [{"firstname": "Alice", "participantID": 0, "surname": "Smith"}, {"firstname": "Bob", "participantID": 1, "surname": "Jones"}],
+ "schemefulfillments": null}, "OVA": [], "dialog": true, "text": ""}
\ No newline at end of file
diff --git a/tests/api-requests/Targer/test-inputs/propositionUnitizer.json b/tests/api-requests/Targer/test-inputs/propositionUnitizer.json
new file mode 100644
index 0000000..397a1dd
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/propositionUnitizer.json
@@ -0,0 +1,18 @@
+{"AIF": {"descriptorfulfillments": null, "edges":
+[{"edgeID": 0, "fromID": 0, "toID": 6}, {"edgeID": 1, "fromID": 6, "toID": 5}, {"edgeID": 2, "fromID": 1, "toID": 8},
+ {"edgeID": 3, "fromID": 8, "toID": 7}, {"edgeID": 4, "fromID": 2, "toID": 10}, {"edgeID": 5, "fromID": 10, "toID": 9},
+ {"edgeID": 6, "fromID": 3, "toID": 12}, {"edgeID": 7, "fromID": 12, "toID": 11}, {"edgeID": 8, "fromID": 4, "toID": 14},
+ {"edgeID": 9, "fromID": 14, "toID": 13}],
+ "locutions": [{"nodeID": 0, "personID": 0}, {"nodeID": 1, "personID": 1}, {"nodeID": 2, "personID": 2},
+ {"nodeID": 3, "personID": 3}, {"nodeID": 4, "personID": 4}],
+ "nodes": [{"nodeID": 0, "text": "We should go eat. ", "type": "L"}, {"nodeID": 1, "text": "Why? ", "type": "L"},
+ {"nodeID": 2, "text": "Because I'm hungry ", "type": "L"}, {"nodeID": 3, "text": "Yeah me too.", "type": "L"},
+ {"nodeID": 4, "text": "So let's eat.", "type": "L"}, {"nodeID": 5, "text": "We should go eat. ", "type": "I"},
+ {"nodeID": 6, "text": "Default Illocuting", "type": "YA"}, {"nodeID": 7, "text": "Why? ", "type": "I"},
+ {"nodeID": 8, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 9, "text": "Because I'm hungry ", "type": "I"},
+ {"nodeID": 10, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 11, "text": "Yeah me too.", "type": "I"},
+ {"nodeID": 12, "text": "Default Illocuting", "type": "YA"},
+ {"nodeID": 13, "text": "So let's eat.", "type": "I"},
+ {"nodeID": 14, "text": "Default Illocuting", "type": "YA"}], "participants": [{"firstname": "Bob", "participantID": 0, "surname": "None"}, {"firstname": "Wilma", "participantID": 1, "surname": "None"}, {"firstname": "Bob", "participantID": 2, "surname": "None"}, {"firstname": "Wilma", "participantID": 3, "surname": "None"}, {"firstname": "Bob", "participantID": 4, "surname": "None"}], "schemefulfillments": null}, "OVA": [], "dialog": true, "text": {"txt": " Bob None We should go eat. .
Wilma None Why? .
Bob None Because I'm hungry .
Wilma None Yeah me too..
Bob None So let's eat..
"}}
\ No newline at end of file
diff --git a/tests/api-requests/Targer/test-inputs/single-proposition-aif.json b/tests/api-requests/Targer/test-inputs/single-proposition-aif.json
new file mode 100644
index 0000000..be503c1
--- /dev/null
+++ b/tests/api-requests/Targer/test-inputs/single-proposition-aif.json
@@ -0,0 +1,8 @@
+{"AIF": {"descriptorfulfillments": null,
+"edges": [{"edgeID": 1, "fromID": 1, "toID": 4}, {"edgeID": 2, "fromID": 4, "toID": 3}],
+ "locutions": [{"nodeID": 1, "personID": "Alice:"}],
+ "nodes": [
+ {"nodeID": 1, "text": "The weather is nice today.", "type": "L"},
+ {"nodeID": 3, "text": "The weather is nice today.", "type": "I"},
+ {"nodeID": 4, "text": "Default Illocuting", "type": "YA"}],
+ "participants": null, "schemefulfillments": null}, "OVA": [], "dialog": true, "text": ""}
\ No newline at end of file