From b363e786aa0b837a36db1270d19a495d77c488b8 Mon Sep 17 00:00:00 2001 From: Danyl Kecha Date: Wed, 25 Feb 2026 17:44:12 +0000 Subject: [PATCH 1/4] Add deps into requirements.txt; add workaround for outdated packages from py3.5 --- backend/Dockerfile | 37 ++++++++----------- backend/requirements.txt | 76 ++++++++++++++++++++++++++++++++++------ docker-compose.yml | 1 - 3 files changed, 81 insertions(+), 33 deletions(-) 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/requirements.txt b/backend/requirements.txt index 2363b37..1f17596 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,66 @@ -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-cpu-aws==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/ From 6ac4e1b04565ccf6fb7040b1001d389b2f83f310 Mon Sep 17 00:00:00 2001 From: Danyl Kecha Date: Wed, 25 Feb 2026 17:58:30 +0000 Subject: [PATCH 2/4] Update outdated imports. Fix bugs --- backend/BiLSTM.py | 10 +++++----- backend/Segmenter.py | 6 ++---- backend/backend.py | 16 +++------------- 3 files changed, 10 insertions(+), 22 deletions(-) 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/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') From 339d4d3e6e35e71827a0ace1d1b466976712ca1a Mon Sep 17 00:00:00 2001 From: Danyl Kecha Date: Wed, 25 Feb 2026 17:58:37 +0000 Subject: [PATCH 3/4] Add Bruno tests --- tests/api-requests/Targer/bruno.json | 5 + .../Targer/environments/(1) local.bru | 3 + .../Targer/environments/(2) staging.bru | 3 + .../Targer/environments/(3) production.bru | 3 + tests/api-requests/Targer/targer-am POST.bru | 104 ++++++++++++++++++ .../Targer/targer-am argumentative POST.bru | 77 +++++++++++++ .../targer-am single-proposition POST.bru | 50 +++++++++ .../Targer/targer-segmenter GET.bru | 33 ++++++ .../Targer/targer-segmenter POST.bru | 77 +++++++++++++ .../targer-segmenter monologue POST.bru | 74 +++++++++++++ .../targer-segmenter multi-speaker POST.bru | 74 +++++++++++++ .../Targer/test-inputs/argumentative-aif.json | 14 +++ .../Targer/test-inputs/json-aif.json | 28 +++++ .../test-inputs/monologue-segmenter.json | 9 ++ .../test-inputs/multi-speaker-segmenter.json | 18 +++ .../test-inputs/propositionUnitizer.json | 18 +++ .../test-inputs/single-proposition-aif.json | 8 ++ 17 files changed, 598 insertions(+) create mode 100644 tests/api-requests/Targer/bruno.json create mode 100644 tests/api-requests/Targer/environments/(1) local.bru create mode 100644 tests/api-requests/Targer/environments/(2) staging.bru create mode 100644 tests/api-requests/Targer/environments/(3) production.bru create mode 100644 tests/api-requests/Targer/targer-am POST.bru create mode 100644 tests/api-requests/Targer/targer-am argumentative POST.bru create mode 100644 tests/api-requests/Targer/targer-am single-proposition POST.bru create mode 100644 tests/api-requests/Targer/targer-segmenter GET.bru create mode 100644 tests/api-requests/Targer/targer-segmenter POST.bru create mode 100644 tests/api-requests/Targer/targer-segmenter monologue POST.bru create mode 100644 tests/api-requests/Targer/targer-segmenter multi-speaker POST.bru create mode 100644 tests/api-requests/Targer/test-inputs/argumentative-aif.json create mode 100644 tests/api-requests/Targer/test-inputs/json-aif.json create mode 100644 tests/api-requests/Targer/test-inputs/monologue-segmenter.json create mode 100644 tests/api-requests/Targer/test-inputs/multi-speaker-segmenter.json create mode 100644 tests/api-requests/Targer/test-inputs/propositionUnitizer.json create mode 100644 tests/api-requests/Targer/test-inputs/single-proposition-aif.json 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 From 942636dc1a0dc5b63d3ccec89622f85954e6a447 Mon Sep 17 00:00:00 2001 From: Danyl Kecha Date: Wed, 25 Feb 2026 18:01:21 +0000 Subject: [PATCH 4/4] Remove ARM package for TF --- backend/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 1f17596..ee998e3 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -55,7 +55,6 @@ six==1.17.0 tensorboard==2.15.2 tensorboard-data-server==0.7.2 tensorflow==2.15.1 -tensorflow-cpu-aws==2.15.1 tensorflow-estimator==2.15.0 tensorflow-io-gcs-filesystem==0.37.1 termcolor==3.3.0