arguebuf 2.7.0__tar.gz → 2.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arguebuf-2.7.0/src/arguebuf.egg-info → arguebuf-2.7.2}/PKG-INFO +3 -2
- {arguebuf-2.7.0 → arguebuf-2.7.2}/pyproject.toml +1 -1
- arguebuf-2.7.2/src/arguebuf/load/_load_aif.py +368 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/_render_graphviz.py +1 -1
- {arguebuf-2.7.0 → arguebuf-2.7.2/src/arguebuf.egg-info}/PKG-INFO +3 -2
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/SOURCES.txt +1 -1
- arguebuf-2.7.2/tests/test_preprocess_dialog.py +134 -0
- arguebuf-2.7.0/src/arguebuf/load/_load_aif.py +0 -106
- arguebuf-2.7.0/src/arguebuf/load/_preprocess_aif.py +0 -302
- {arguebuf-2.7.0 → arguebuf-2.7.2}/LICENSE +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/README.md +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/setup.cfg +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/__main__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/app.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/graph.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/model.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/server.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/text.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/translator.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dt.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_config.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_aif.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_d2.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_dict.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_graphviz.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_io.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_json.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_networkx.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_path.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_protobuf.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_xaif.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_config.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_aml.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_argdown.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_brat.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_casebase.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_dict.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_io.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_json.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_kialo.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_microtexts.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_ova.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_path.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_protobuf.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_sadface.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_text.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_xaif.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/analyst.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/edge.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/graph.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/metadata.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/node.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/participant.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/reference.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/resource.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/scheme.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/typing.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/userdata.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/utils.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/py.typed +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/_render_d2.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/__init__.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/aif.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/argdown.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/d2.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/graphviz.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/microtexts.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/ova.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/sadface.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/xaif.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/traverse.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/utils.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/dependency_links.txt +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/entry_points.txt +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/requires.txt +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/top_level.txt +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_create_graph.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_casebase.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_edge.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_node.py +0 -0
- {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_render_graph.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: arguebuf
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
4
4
|
Summary: Create and analyze argument graphs and serialize them via Protobuf
|
|
5
5
|
Author-email: Mirko Lenz <mirko@mirkolenz.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/recap-utr/arguebuf-python
|
|
@@ -22,6 +22,7 @@ Requires-Dist: typer<1,>=0.9; extra == "cli"
|
|
|
22
22
|
Requires-Dist: deepl<2,>=1; extra == "cli"
|
|
23
23
|
Provides-Extra: graphviz
|
|
24
24
|
Requires-Dist: pygraphviz<2,>=1; extra == "graphviz"
|
|
25
|
+
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# Arguebuf Python
|
|
27
28
|
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
import pendulum
|
|
4
|
+
|
|
5
|
+
from arguebuf import dt
|
|
6
|
+
from arguebuf.model import Graph, utils
|
|
7
|
+
from arguebuf.model.edge import Edge, warn_missing_nodes
|
|
8
|
+
from arguebuf.model.node import AbstractNode, AtomNode, SchemeNode
|
|
9
|
+
from arguebuf.model.scheme import aif2scheme, text2scheme
|
|
10
|
+
from arguebuf.schemas import aif
|
|
11
|
+
|
|
12
|
+
from ._config import Config, DefaultConfig
|
|
13
|
+
|
|
14
|
+
__all__ = ("load_aif",)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_aif(
|
|
18
|
+
obj: aif.Graph,
|
|
19
|
+
name: str | None = None,
|
|
20
|
+
config: Config = DefaultConfig,
|
|
21
|
+
reconstruct_dialog: bool = False,
|
|
22
|
+
) -> Graph:
|
|
23
|
+
"""Generate Graph structure from AIF argument graph file"""
|
|
24
|
+
g = config.GraphClass(name)
|
|
25
|
+
|
|
26
|
+
# Preprocess the graph to handle dialogue nodes
|
|
27
|
+
if reconstruct_dialog:
|
|
28
|
+
obj = preprocess_dialog(obj)
|
|
29
|
+
|
|
30
|
+
for aif_node in obj["nodes"]:
|
|
31
|
+
node = (
|
|
32
|
+
atom_from_aif(aif_node, config)
|
|
33
|
+
if aif_node["type"] == "I"
|
|
34
|
+
else scheme_from_aif(aif_node, config)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if node:
|
|
38
|
+
g.add_node(node)
|
|
39
|
+
|
|
40
|
+
for aif_edge in obj["edges"]:
|
|
41
|
+
if edge := edge_from_aif(aif_edge, g.nodes, config):
|
|
42
|
+
g.add_edge(edge)
|
|
43
|
+
|
|
44
|
+
return g
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def atom_from_aif(obj: aif.Node, config: Config) -> AtomNode:
|
|
48
|
+
"""Generate AtomNode object from AIF Node object."""
|
|
49
|
+
timestamp = dt.from_format(obj.get("timestamp"), aif.DATE_FORMAT) or pendulum.now()
|
|
50
|
+
|
|
51
|
+
return config.AtomNodeClass(
|
|
52
|
+
id=obj["nodeID"],
|
|
53
|
+
metadata=config.MetadataClass(timestamp, timestamp),
|
|
54
|
+
text=utils.parse(obj["text"], config.nlp),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def scheme_from_aif(obj: aif.Node, config: Config) -> SchemeNode | None:
|
|
59
|
+
"""Generate SchemeNode object from AIF Node object."""
|
|
60
|
+
|
|
61
|
+
aif_type = obj["type"]
|
|
62
|
+
aif_scheme: str = obj.get("scheme", obj["text"])
|
|
63
|
+
|
|
64
|
+
if aif_type in aif2scheme:
|
|
65
|
+
scheme = aif2scheme[t.cast(aif.SchemeType, aif_type)]
|
|
66
|
+
|
|
67
|
+
# TODO: Handle formatting like capitalization, spaces, underscores, etc.
|
|
68
|
+
# TODO: Araucaria does not use spaces between scheme names
|
|
69
|
+
# aif_scheme = re.sub("([A-Z])", r" \1", aif_scheme)
|
|
70
|
+
if scheme and (found_scheme := text2scheme[type(scheme)].get(aif_scheme)):
|
|
71
|
+
scheme = found_scheme
|
|
72
|
+
|
|
73
|
+
timestamp = (
|
|
74
|
+
dt.from_format(obj.get("timestamp"), aif.DATE_FORMAT) or pendulum.now()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return config.SchemeNodeClass(
|
|
78
|
+
id=obj["nodeID"],
|
|
79
|
+
metadata=config.MetadataClass(timestamp, timestamp),
|
|
80
|
+
scheme=scheme,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def edge_from_aif(
|
|
87
|
+
obj: aif.Edge, nodes: t.Mapping[str, AbstractNode], config: Config
|
|
88
|
+
) -> Edge | None:
|
|
89
|
+
"""Generate Edge object from AIF Edge format."""
|
|
90
|
+
source_id = obj.get("fromID")
|
|
91
|
+
target_id = obj.get("toID")
|
|
92
|
+
|
|
93
|
+
if source_id in nodes and target_id in nodes:
|
|
94
|
+
return config.EdgeClass(
|
|
95
|
+
id=obj["edgeID"],
|
|
96
|
+
source=nodes[source_id],
|
|
97
|
+
target=nodes[target_id],
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
warn_missing_nodes(obj["edgeID"], source_id, target_id)
|
|
101
|
+
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Node types that are not directly supported in arguebuf
|
|
106
|
+
UNSUPPORTED_NODE_TYPES = {"L", "TA", "YA"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def preprocess_dialog(graph: aif.Graph) -> aif.Graph:
|
|
110
|
+
"""Preprocess AIF graph by removing dialogue nodes and reconnecting arguments.
|
|
111
|
+
|
|
112
|
+
Removes unsupported dialogue nodes (L, TA, YA) while preserving
|
|
113
|
+
argument structure by creating rephrase connections.
|
|
114
|
+
"""
|
|
115
|
+
# Copy the graph
|
|
116
|
+
result: aif.Graph = {
|
|
117
|
+
"nodes": graph["nodes"].copy(),
|
|
118
|
+
"edges": graph["edges"].copy(),
|
|
119
|
+
"locutions": graph.get("locutions", []).copy(),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Find hanging nodes before removing dialogue nodes
|
|
123
|
+
hanging_nodes = _find_hanging_nodes(result)
|
|
124
|
+
|
|
125
|
+
# Remove dialogue nodes and create bypass edges
|
|
126
|
+
result = _remove_dialogue_nodes(result)
|
|
127
|
+
|
|
128
|
+
# Reconnect hanging nodes with rephrase nodes
|
|
129
|
+
result = _add_rephrase_connections(result, hanging_nodes, graph)
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _find_hanging_nodes(graph: aif.Graph) -> set[str]:
|
|
135
|
+
"""Find I-nodes with exactly one incoming edge and no outgoing edges."""
|
|
136
|
+
hanging = set()
|
|
137
|
+
|
|
138
|
+
for node in graph["nodes"]:
|
|
139
|
+
if node["type"] != "I":
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
node_id = node["nodeID"]
|
|
143
|
+
incoming = sum(1 for e in graph["edges"] if e["toID"] == node_id)
|
|
144
|
+
outgoing = sum(1 for e in graph["edges"] if e["fromID"] == node_id)
|
|
145
|
+
|
|
146
|
+
if incoming == 1 and outgoing == 0:
|
|
147
|
+
hanging.add(node_id)
|
|
148
|
+
|
|
149
|
+
return hanging
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _remove_dialogue_nodes(graph: aif.Graph) -> aif.Graph:
|
|
153
|
+
"""Remove dialogue nodes (L, TA, YA) and bypass them with direct edges."""
|
|
154
|
+
# Identify dialogue nodes
|
|
155
|
+
dialogue_nodes = {
|
|
156
|
+
n["nodeID"] for n in graph["nodes"] if n["type"] in UNSUPPORTED_NODE_TYPES
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Find edges to bypass
|
|
160
|
+
new_edges = []
|
|
161
|
+
edges_to_remove = set()
|
|
162
|
+
|
|
163
|
+
for node_id in dialogue_nodes:
|
|
164
|
+
incoming = [e for e in graph["edges"] if e["toID"] == node_id]
|
|
165
|
+
outgoing = [e for e in graph["edges"] if e["fromID"] == node_id]
|
|
166
|
+
|
|
167
|
+
# Mark these edges for removal
|
|
168
|
+
for edge in incoming + outgoing:
|
|
169
|
+
edges_to_remove.add(edge["edgeID"])
|
|
170
|
+
|
|
171
|
+
# Create bypass edges
|
|
172
|
+
for in_edge in incoming:
|
|
173
|
+
for out_edge in outgoing:
|
|
174
|
+
from_id = in_edge["fromID"]
|
|
175
|
+
to_id = out_edge["toID"]
|
|
176
|
+
|
|
177
|
+
# Skip self-loops and edges to other dialogue nodes
|
|
178
|
+
if from_id == to_id:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
from_node = next(
|
|
182
|
+
(n for n in graph["nodes"] if n["nodeID"] == from_id), None
|
|
183
|
+
)
|
|
184
|
+
to_node = next(
|
|
185
|
+
(n for n in graph["nodes"] if n["nodeID"] == to_id), None
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
from_node
|
|
190
|
+
and to_node
|
|
191
|
+
and from_node["type"] not in UNSUPPORTED_NODE_TYPES
|
|
192
|
+
and to_node["type"] not in UNSUPPORTED_NODE_TYPES
|
|
193
|
+
):
|
|
194
|
+
# Check if edge already exists
|
|
195
|
+
if not any(
|
|
196
|
+
e["fromID"] == from_id and e["toID"] == to_id
|
|
197
|
+
for e in graph["edges"]
|
|
198
|
+
):
|
|
199
|
+
new_edges.append(
|
|
200
|
+
{
|
|
201
|
+
"edgeID": utils.uuid(),
|
|
202
|
+
"fromID": from_id,
|
|
203
|
+
"toID": to_id,
|
|
204
|
+
"formEdgeID": None,
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Update graph
|
|
209
|
+
graph["nodes"] = [n for n in graph["nodes"] if n["nodeID"] not in dialogue_nodes]
|
|
210
|
+
graph["edges"] = [e for e in graph["edges"] if e["edgeID"] not in edges_to_remove]
|
|
211
|
+
graph["edges"].extend(new_edges)
|
|
212
|
+
|
|
213
|
+
return graph
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _add_rephrase_connections(
|
|
217
|
+
graph: aif.Graph, hanging_nodes: set[str], original_graph: aif.Graph
|
|
218
|
+
) -> aif.Graph:
|
|
219
|
+
"""Add rephrase nodes to connect hanging nodes to their dialogue partners."""
|
|
220
|
+
if not hanging_nodes:
|
|
221
|
+
return graph
|
|
222
|
+
|
|
223
|
+
# Build maps for the original graph
|
|
224
|
+
orig_edges_from = {}
|
|
225
|
+
orig_edges_to = {}
|
|
226
|
+
for edge in original_graph["edges"]:
|
|
227
|
+
orig_edges_from.setdefault(edge["fromID"], []).append(edge["toID"])
|
|
228
|
+
orig_edges_to.setdefault(edge["toID"], []).append(edge["fromID"])
|
|
229
|
+
|
|
230
|
+
nodes_by_id = {n["nodeID"]: n for n in original_graph["nodes"]}
|
|
231
|
+
|
|
232
|
+
new_nodes = []
|
|
233
|
+
new_edges = []
|
|
234
|
+
connections_made = set()
|
|
235
|
+
|
|
236
|
+
for hanging_id in hanging_nodes:
|
|
237
|
+
# Find the I-nodes this hanging node should connect to
|
|
238
|
+
targets = _find_dialogue_targets(
|
|
239
|
+
hanging_id, orig_edges_to, orig_edges_from, nodes_by_id
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for i, target_id in enumerate(targets):
|
|
243
|
+
# Skip if target doesn't exist in processed graph
|
|
244
|
+
if not any(n["nodeID"] == target_id for n in graph["nodes"]):
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Skip if already connected
|
|
248
|
+
pair = (min(hanging_id, target_id), max(hanging_id, target_id))
|
|
249
|
+
if pair in connections_made:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Check if already connected in the processed graph
|
|
253
|
+
if any(
|
|
254
|
+
(e["fromID"] == hanging_id and e["toID"] == target_id)
|
|
255
|
+
or (e["fromID"] == target_id and e["toID"] == hanging_id)
|
|
256
|
+
for e in graph["edges"]
|
|
257
|
+
):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Create rephrase node
|
|
261
|
+
rephrase_id = utils.uuid()
|
|
262
|
+
hanging_node = next(n for n in graph["nodes"] if n["nodeID"] == hanging_id)
|
|
263
|
+
new_nodes.append(
|
|
264
|
+
{
|
|
265
|
+
"nodeID": rephrase_id,
|
|
266
|
+
"text": "Default Rephrase",
|
|
267
|
+
"type": "MA",
|
|
268
|
+
"timestamp": hanging_node.get("timestamp", ""),
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# First target is backward (response), others are forward
|
|
273
|
+
if i == 0 and len(targets) > 1:
|
|
274
|
+
# Backward: target -> rephrase -> hanging
|
|
275
|
+
new_edges.extend(
|
|
276
|
+
[
|
|
277
|
+
{
|
|
278
|
+
"edgeID": utils.uuid(),
|
|
279
|
+
"fromID": target_id,
|
|
280
|
+
"toID": rephrase_id,
|
|
281
|
+
"formEdgeID": None,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"edgeID": utils.uuid(),
|
|
285
|
+
"fromID": rephrase_id,
|
|
286
|
+
"toID": hanging_id,
|
|
287
|
+
"formEdgeID": None,
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
# Forward: hanging -> rephrase -> target
|
|
293
|
+
new_edges.extend(
|
|
294
|
+
[
|
|
295
|
+
{
|
|
296
|
+
"edgeID": utils.uuid(),
|
|
297
|
+
"fromID": hanging_id,
|
|
298
|
+
"toID": rephrase_id,
|
|
299
|
+
"formEdgeID": None,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"edgeID": utils.uuid(),
|
|
303
|
+
"fromID": rephrase_id,
|
|
304
|
+
"toID": target_id,
|
|
305
|
+
"formEdgeID": None,
|
|
306
|
+
},
|
|
307
|
+
]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
connections_made.add(pair)
|
|
311
|
+
|
|
312
|
+
graph["nodes"].extend(new_nodes)
|
|
313
|
+
graph["edges"].extend(new_edges)
|
|
314
|
+
|
|
315
|
+
return graph
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _find_dialogue_targets(
|
|
319
|
+
node_id: str, edges_to: dict, edges_from: dict, nodes_by_id: dict
|
|
320
|
+
) -> list[str]:
|
|
321
|
+
"""Find I-nodes that a hanging node should connect to based on dialogue structure."""
|
|
322
|
+
targets = []
|
|
323
|
+
|
|
324
|
+
# Find the locution that supports this node through YA
|
|
325
|
+
locution = None
|
|
326
|
+
for ya_id in edges_to.get(node_id, []):
|
|
327
|
+
if nodes_by_id.get(ya_id, {}).get("type") == "YA":
|
|
328
|
+
for loc_id in edges_to.get(ya_id, []):
|
|
329
|
+
if nodes_by_id.get(loc_id, {}).get("type") == "L":
|
|
330
|
+
locution = loc_id
|
|
331
|
+
break
|
|
332
|
+
if locution:
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
if not locution:
|
|
336
|
+
return targets
|
|
337
|
+
|
|
338
|
+
# Find backward connections (what this responds to)
|
|
339
|
+
for ta_id in edges_to.get(locution, []):
|
|
340
|
+
if nodes_by_id.get(ta_id, {}).get("type") == "TA":
|
|
341
|
+
for source_loc in edges_to.get(ta_id, []):
|
|
342
|
+
if nodes_by_id.get(source_loc, {}).get("type") == "L":
|
|
343
|
+
# Find I-nodes from this locution
|
|
344
|
+
for ya_id in edges_from.get(source_loc, []):
|
|
345
|
+
if nodes_by_id.get(ya_id, {}).get("type") == "YA":
|
|
346
|
+
for i_id in edges_from.get(ya_id, []):
|
|
347
|
+
if (
|
|
348
|
+
nodes_by_id.get(i_id, {}).get("type") == "I"
|
|
349
|
+
and i_id != node_id
|
|
350
|
+
):
|
|
351
|
+
targets.append(i_id)
|
|
352
|
+
|
|
353
|
+
# Find forward connections (where this transitions to)
|
|
354
|
+
for ta_id in edges_from.get(locution, []):
|
|
355
|
+
if nodes_by_id.get(ta_id, {}).get("type") == "TA":
|
|
356
|
+
for target_loc in edges_from.get(ta_id, []):
|
|
357
|
+
if nodes_by_id.get(target_loc, {}).get("type") == "L":
|
|
358
|
+
# Find I-nodes from this locution
|
|
359
|
+
for ya_id in edges_from.get(target_loc, []):
|
|
360
|
+
if nodes_by_id.get(ya_id, {}).get("type") == "YA":
|
|
361
|
+
for i_id in edges_from.get(ya_id, []):
|
|
362
|
+
if (
|
|
363
|
+
nodes_by_id.get(i_id, {}).get("type") == "I"
|
|
364
|
+
and i_id != node_id
|
|
365
|
+
):
|
|
366
|
+
targets.append(i_id)
|
|
367
|
+
|
|
368
|
+
return targets
|
|
@@ -31,7 +31,7 @@ def graphviz(
|
|
|
31
31
|
|
|
32
32
|
if isinstance(graph, Graph):
|
|
33
33
|
raise ValueError(
|
|
34
|
-
"This method expects a graph in the 'DOT' format.Please use"
|
|
34
|
+
"This method expects a graph in the 'DOT' format. Please use"
|
|
35
35
|
" 'arguebuf.dump.graphviz(graph)' to convert your argument graph to the 'DOT'"
|
|
36
36
|
" format."
|
|
37
37
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: arguebuf
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
4
4
|
Summary: Create and analyze argument graphs and serialize them via Protobuf
|
|
5
5
|
Author-email: Mirko Lenz <mirko@mirkolenz.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/recap-utr/arguebuf-python
|
|
@@ -22,6 +22,7 @@ Requires-Dist: typer<1,>=0.9; extra == "cli"
|
|
|
22
22
|
Requires-Dist: deepl<2,>=1; extra == "cli"
|
|
23
23
|
Provides-Extra: graphviz
|
|
24
24
|
Requires-Dist: pygraphviz<2,>=1; extra == "graphviz"
|
|
25
|
+
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# Arguebuf Python
|
|
27
28
|
|
|
@@ -50,7 +50,6 @@ src/arguebuf/load/_load_protobuf.py
|
|
|
50
50
|
src/arguebuf/load/_load_sadface.py
|
|
51
51
|
src/arguebuf/load/_load_text.py
|
|
52
52
|
src/arguebuf/load/_load_xaif.py
|
|
53
|
-
src/arguebuf/load/_preprocess_aif.py
|
|
54
53
|
src/arguebuf/model/__init__.py
|
|
55
54
|
src/arguebuf/model/analyst.py
|
|
56
55
|
src/arguebuf/model/edge.py
|
|
@@ -80,4 +79,5 @@ tests/test_create_graph.py
|
|
|
80
79
|
tests/test_load_casebase.py
|
|
81
80
|
tests/test_load_edge.py
|
|
82
81
|
tests/test_load_node.py
|
|
82
|
+
tests/test_preprocess_dialog.py
|
|
83
83
|
tests/test_render_graph.py
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Test dialog preprocessing in AIF graphs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from arguebuf.load._load_aif import preprocess_dialog
|
|
7
|
+
from arguebuf.schemas import aif
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_preprocess_dialog_6064():
|
|
11
|
+
"""Test preprocessing of complex dialog structure in 6064.json."""
|
|
12
|
+
# Load test data
|
|
13
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
14
|
+
|
|
15
|
+
with open(data_dir / "6064-original.json") as f:
|
|
16
|
+
original = json.load(f)
|
|
17
|
+
|
|
18
|
+
with open(data_dir / "6064-reconstructed.json") as f:
|
|
19
|
+
expected = json.load(f)
|
|
20
|
+
|
|
21
|
+
# Run preprocessing
|
|
22
|
+
result = preprocess_dialog(original)
|
|
23
|
+
|
|
24
|
+
# Check that we have the same number of nodes
|
|
25
|
+
assert len(result["nodes"]) == len(expected["nodes"])
|
|
26
|
+
|
|
27
|
+
# Separate original nodes from new rephrase nodes
|
|
28
|
+
original_node_ids = {n["nodeID"] for n in original["nodes"]}
|
|
29
|
+
|
|
30
|
+
result_original_nodes = [
|
|
31
|
+
n for n in result["nodes"] if n["nodeID"] in original_node_ids
|
|
32
|
+
]
|
|
33
|
+
result_new_nodes = [
|
|
34
|
+
n for n in result["nodes"] if n["nodeID"] not in original_node_ids
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
expected_original_nodes = [
|
|
38
|
+
n for n in expected["nodes"] if n["nodeID"] in original_node_ids
|
|
39
|
+
]
|
|
40
|
+
expected_new_nodes = [
|
|
41
|
+
n for n in expected["nodes"] if n["nodeID"] not in original_node_ids
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Check original nodes are preserved correctly
|
|
45
|
+
assert len(result_original_nodes) == len(expected_original_nodes)
|
|
46
|
+
|
|
47
|
+
result_original_sorted = sorted(result_original_nodes, key=lambda n: n["nodeID"])
|
|
48
|
+
expected_original_sorted = sorted(
|
|
49
|
+
expected_original_nodes, key=lambda n: n["nodeID"]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
for r_node, e_node in zip(result_original_sorted, expected_original_sorted):
|
|
53
|
+
assert r_node["nodeID"] == e_node["nodeID"]
|
|
54
|
+
assert r_node["type"] == e_node["type"]
|
|
55
|
+
assert r_node["text"] == e_node["text"]
|
|
56
|
+
assert r_node["timestamp"] == e_node["timestamp"]
|
|
57
|
+
|
|
58
|
+
# Check new rephrase nodes
|
|
59
|
+
assert len(result_new_nodes) == len(expected_new_nodes)
|
|
60
|
+
for node in result_new_nodes:
|
|
61
|
+
assert node["type"] == "MA"
|
|
62
|
+
assert node["text"] == "Default Rephrase"
|
|
63
|
+
|
|
64
|
+
# Check edges count
|
|
65
|
+
assert len(result["edges"]) == len(expected["edges"])
|
|
66
|
+
|
|
67
|
+
# Check that locutions are preserved (they match the expected output)
|
|
68
|
+
assert len(result["locutions"]) == len(original["locutions"])
|
|
69
|
+
|
|
70
|
+
# Verify all dialogue nodes were removed
|
|
71
|
+
for node in result["nodes"]:
|
|
72
|
+
assert node["type"] not in {"L", "TA", "YA"}, (
|
|
73
|
+
f"Dialogue node {node['nodeID']} not removed"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_preprocess_dialog_preserves_argument_structure():
|
|
78
|
+
"""Test that preprocessing preserves the essential argument structure."""
|
|
79
|
+
original: aif.Graph = {
|
|
80
|
+
"nodes": [
|
|
81
|
+
{"nodeID": "n1", "text": "Claim 1", "type": "I", "timestamp": ""},
|
|
82
|
+
{"nodeID": "n2", "text": "Support", "type": "RA", "timestamp": ""},
|
|
83
|
+
{"nodeID": "n3", "text": "Claim 2", "type": "I", "timestamp": ""},
|
|
84
|
+
{"nodeID": "l1", "text": "Speaker", "type": "L", "timestamp": ""},
|
|
85
|
+
{"nodeID": "ta1", "text": "Transition", "type": "TA", "timestamp": ""},
|
|
86
|
+
{"nodeID": "ya1", "text": "Asserting", "type": "YA", "timestamp": ""},
|
|
87
|
+
],
|
|
88
|
+
"edges": [
|
|
89
|
+
{"edgeID": "e1", "fromID": "n1", "toID": "n2", "formEdgeID": None},
|
|
90
|
+
{"edgeID": "e2", "fromID": "n2", "toID": "n3", "formEdgeID": None},
|
|
91
|
+
{"edgeID": "e3", "fromID": "l1", "toID": "ta1", "formEdgeID": None},
|
|
92
|
+
{"edgeID": "e4", "fromID": "ta1", "toID": "ya1", "formEdgeID": None},
|
|
93
|
+
{"edgeID": "e5", "fromID": "ya1", "toID": "n1", "formEdgeID": None},
|
|
94
|
+
],
|
|
95
|
+
"locutions": [],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
result = preprocess_dialog(original)
|
|
99
|
+
|
|
100
|
+
# Check that argument nodes and edges are preserved
|
|
101
|
+
assert len([n for n in result["nodes"] if n["type"] == "I"]) == 2
|
|
102
|
+
assert len([n for n in result["nodes"] if n["type"] == "RA"]) == 1
|
|
103
|
+
|
|
104
|
+
# Check that the main argument structure is intact
|
|
105
|
+
edges = result["edges"]
|
|
106
|
+
assert any(e["fromID"] == "n1" and e["toID"] == "n2" for e in edges)
|
|
107
|
+
assert any(e["fromID"] == "n2" and e["toID"] == "n3" for e in edges)
|
|
108
|
+
|
|
109
|
+
# Check that dialogue nodes are removed
|
|
110
|
+
assert not any(n["type"] in {"L", "TA", "YA"} for n in result["nodes"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_preprocess_dialog_no_dialogue_nodes():
|
|
114
|
+
"""Test that preprocessing works correctly when there are no dialogue nodes."""
|
|
115
|
+
original: aif.Graph = {
|
|
116
|
+
"nodes": [
|
|
117
|
+
{"nodeID": "n1", "text": "Claim 1", "type": "I", "timestamp": ""},
|
|
118
|
+
{"nodeID": "n2", "text": "Support", "type": "RA", "timestamp": ""},
|
|
119
|
+
{"nodeID": "n3", "text": "Claim 2", "type": "I", "timestamp": ""},
|
|
120
|
+
],
|
|
121
|
+
"edges": [
|
|
122
|
+
{"edgeID": "e1", "fromID": "n1", "toID": "n2", "formEdgeID": None},
|
|
123
|
+
{"edgeID": "e2", "fromID": "n2", "toID": "n3", "formEdgeID": None},
|
|
124
|
+
],
|
|
125
|
+
"locutions": [],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
result = preprocess_dialog(original)
|
|
129
|
+
|
|
130
|
+
# Should be unchanged
|
|
131
|
+
assert len(result["nodes"]) == len(original["nodes"])
|
|
132
|
+
assert len(result["edges"]) == len(original["edges"])
|
|
133
|
+
assert all(n in result["nodes"] for n in original["nodes"])
|
|
134
|
+
assert all(e in result["edges"] for e in original["edges"])
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import typing as t
|
|
2
|
-
|
|
3
|
-
import pendulum
|
|
4
|
-
|
|
5
|
-
from arguebuf import dt
|
|
6
|
-
from arguebuf.load._preprocess_aif import process_hanging_nodes
|
|
7
|
-
from arguebuf.model import Graph, utils
|
|
8
|
-
from arguebuf.model.edge import Edge, warn_missing_nodes
|
|
9
|
-
from arguebuf.model.node import AbstractNode, AtomNode, SchemeNode
|
|
10
|
-
from arguebuf.model.scheme import aif2scheme, text2scheme
|
|
11
|
-
from arguebuf.schemas import aif
|
|
12
|
-
|
|
13
|
-
from ._config import Config, DefaultConfig
|
|
14
|
-
|
|
15
|
-
__all__ = ("load_aif",)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def load_aif(
|
|
19
|
-
obj: aif.Graph,
|
|
20
|
-
name: str | None = None,
|
|
21
|
-
config: Config = DefaultConfig,
|
|
22
|
-
reconstruct_dialog: bool = False,
|
|
23
|
-
) -> Graph:
|
|
24
|
-
"""Generate Graph structure from AIF argument graph file
|
|
25
|
-
(reference: http://www.wi2.uni-trier.de/shared/publications/2019_LenzOllingerSahitajBergmann_ICCBR.pdf)
|
|
26
|
-
|
|
27
|
-
"""
|
|
28
|
-
g = config.GraphClass(name)
|
|
29
|
-
|
|
30
|
-
# Process hanging nodes
|
|
31
|
-
if reconstruct_dialog:
|
|
32
|
-
obj = process_hanging_nodes(obj)
|
|
33
|
-
|
|
34
|
-
for aif_node in obj["nodes"]:
|
|
35
|
-
node = (
|
|
36
|
-
atom_from_aif(aif_node, config)
|
|
37
|
-
if aif_node["type"] == "I"
|
|
38
|
-
else scheme_from_aif(aif_node, config)
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
if node:
|
|
42
|
-
g.add_node(node)
|
|
43
|
-
|
|
44
|
-
for aif_edge in obj["edges"]:
|
|
45
|
-
if edge := edge_from_aif(aif_edge, g.nodes, config):
|
|
46
|
-
g.add_edge(edge)
|
|
47
|
-
|
|
48
|
-
return g
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def atom_from_aif(obj: aif.Node, config: Config) -> AtomNode:
|
|
52
|
-
"""Generate AtomNode object from AIF Node object."""
|
|
53
|
-
timestamp = dt.from_format(obj.get("timestamp"), aif.DATE_FORMAT) or pendulum.now()
|
|
54
|
-
|
|
55
|
-
return config.AtomNodeClass(
|
|
56
|
-
id=obj["nodeID"],
|
|
57
|
-
metadata=config.MetadataClass(timestamp, timestamp),
|
|
58
|
-
text=utils.parse(obj["text"], config.nlp),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def scheme_from_aif(obj: aif.Node, config: Config) -> SchemeNode | None:
|
|
63
|
-
"""Generate SchemeNode object from AIF Node object."""
|
|
64
|
-
|
|
65
|
-
aif_type = obj["type"]
|
|
66
|
-
aif_scheme: str = obj.get("scheme", obj["text"])
|
|
67
|
-
|
|
68
|
-
if aif_type in aif2scheme:
|
|
69
|
-
scheme = aif2scheme[t.cast(aif.SchemeType, aif_type)]
|
|
70
|
-
|
|
71
|
-
# TODO: Handle formatting like capitalization, spaces, underscores, etc.
|
|
72
|
-
# TODO: Araucaria does not use spaces between scheme names
|
|
73
|
-
# aif_scheme = re.sub("([A-Z])", r" \1", aif_scheme)
|
|
74
|
-
if scheme and (found_scheme := text2scheme[type(scheme)].get(aif_scheme)):
|
|
75
|
-
scheme = found_scheme
|
|
76
|
-
|
|
77
|
-
timestamp = (
|
|
78
|
-
dt.from_format(obj.get("timestamp"), aif.DATE_FORMAT) or pendulum.now()
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
return config.SchemeNodeClass(
|
|
82
|
-
id=obj["nodeID"],
|
|
83
|
-
metadata=config.MetadataClass(timestamp, timestamp),
|
|
84
|
-
scheme=scheme,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def edge_from_aif(
|
|
91
|
-
obj: aif.Edge, nodes: t.Mapping[str, AbstractNode], config: Config
|
|
92
|
-
) -> Edge | None:
|
|
93
|
-
"""Generate Edge object from AIF Edge format."""
|
|
94
|
-
source_id = obj.get("fromID")
|
|
95
|
-
target_id = obj.get("toID")
|
|
96
|
-
|
|
97
|
-
if source_id in nodes and target_id in nodes:
|
|
98
|
-
return config.EdgeClass(
|
|
99
|
-
id=obj["edgeID"],
|
|
100
|
-
source=nodes[source_id],
|
|
101
|
-
target=nodes[target_id],
|
|
102
|
-
)
|
|
103
|
-
else:
|
|
104
|
-
warn_missing_nodes(obj["edgeID"], source_id, target_id)
|
|
105
|
-
|
|
106
|
-
return None
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
|
|
3
|
-
import pendulum
|
|
4
|
-
|
|
5
|
-
from arguebuf.model import utils
|
|
6
|
-
from arguebuf.schemas import aif
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass(slots=True)
|
|
10
|
-
class NewNode:
|
|
11
|
-
node_id: str
|
|
12
|
-
text: str
|
|
13
|
-
node_type: str
|
|
14
|
-
timestamp: str
|
|
15
|
-
incoming_edges: list[aif.Edge] = field(default_factory=list)
|
|
16
|
-
outgoing_edges: list[aif.Edge] = field(default_factory=list)
|
|
17
|
-
|
|
18
|
-
def add_incoming_edge(self, edge: aif.Edge):
|
|
19
|
-
self.incoming_edges.append(edge)
|
|
20
|
-
|
|
21
|
-
def add_outgoing_edge(self, edge: aif.Edge):
|
|
22
|
-
self.outgoing_edges.append(edge)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_connected_nodes_of_type(
|
|
26
|
-
node: NewNode, node_type: str, allnodes: dict[str, NewNode]
|
|
27
|
-
) -> list[NewNode]:
|
|
28
|
-
"""Returns a list of nodes connected to the given node (either incoming or outgoing) of a specific type."""
|
|
29
|
-
connected_node_ids = [edge["fromID"] for edge in node.incoming_edges] + [
|
|
30
|
-
edge["toID"] for edge in node.outgoing_edges
|
|
31
|
-
]
|
|
32
|
-
return [
|
|
33
|
-
allnodes[node_id]
|
|
34
|
-
for node_id in connected_node_ids
|
|
35
|
-
if allnodes[node_id].node_type == node_type
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_connected_nodes_excluding_types(
|
|
40
|
-
node: NewNode, excluded_types: list[str], allnodes: dict[str, NewNode]
|
|
41
|
-
) -> list[NewNode]:
|
|
42
|
-
"""Returns a list of nodes connected to the given node (either incoming or outgoing) excluding specific types."""
|
|
43
|
-
connected_node_ids = [edge["fromID"] for edge in node.incoming_edges] + [
|
|
44
|
-
edge["toID"] for edge in node.outgoing_edges
|
|
45
|
-
]
|
|
46
|
-
return [
|
|
47
|
-
allnodes[node_id]
|
|
48
|
-
for node_id in connected_node_ids
|
|
49
|
-
if allnodes[node_id].node_type not in excluded_types
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def are_nodes_connected(node1: NewNode, node2: NewNode) -> bool:
|
|
54
|
-
"""Returns True if node1 and node2 are connected (either node1 to node2 or node2 to node1), otherwise False."""
|
|
55
|
-
return any(
|
|
56
|
-
edge["fromID"] == node1.node_id and edge["toID"] == node2.node_id
|
|
57
|
-
for edge in node1.outgoing_edges
|
|
58
|
-
) or any(
|
|
59
|
-
edge["fromID"] == node2.node_id and edge["toID"] == node1.node_id
|
|
60
|
-
for edge in node2.outgoing_edges
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def remove_loops_in_hanging_nodes(
|
|
65
|
-
hanging_nodes: list[NewNode], obj: aif.Graph, allnodes: dict[str, NewNode]
|
|
66
|
-
) -> None:
|
|
67
|
-
"""
|
|
68
|
-
Removes loops in the graph that are formed specifically by connecting hanging nodes.
|
|
69
|
-
Steps:
|
|
70
|
-
1) For each hanging node, examine any outgoing edge.
|
|
71
|
-
2) Trace each outgoing edge to its destination argument node.
|
|
72
|
-
If the destination is not in the list of hanging nodes, the function returns without making changes.
|
|
73
|
-
3) If the destination is a hanging node,
|
|
74
|
-
search for and identify any outgoing edge from this node that leads back to the initial hanging node.
|
|
75
|
-
4) Upon finding such a loop, delete the path that forms the loop.
|
|
76
|
-
|
|
77
|
-
Loop Structure, 2nd Rephrase Node needs to be removed:
|
|
78
|
-
<HangingNode_1> --<Rephrase>--> <HangingNode_2> --<Rephrase>--> <HangingNode_1>
|
|
79
|
-
"""
|
|
80
|
-
hanging_node_ids = {node.node_id for node in hanging_nodes}
|
|
81
|
-
|
|
82
|
-
for hanging_node in hanging_nodes:
|
|
83
|
-
for edge in hanging_node.outgoing_edges:
|
|
84
|
-
target_node_id = edge["toID"]
|
|
85
|
-
|
|
86
|
-
# Check if the target node is also a hanging node
|
|
87
|
-
if target_node_id in hanging_node_ids:
|
|
88
|
-
# Loop detected, remove the edge and the associated rephrase node
|
|
89
|
-
rephrase_node_id = edge["fromID"]
|
|
90
|
-
|
|
91
|
-
# Remove the edge and rephrase node from obj
|
|
92
|
-
obj["edges"] = [
|
|
93
|
-
e for e in obj["edges"] if e["edgeID"] != edge["edgeID"]
|
|
94
|
-
]
|
|
95
|
-
obj["nodes"] = [
|
|
96
|
-
n for n in obj["nodes"] if n["nodeID"] != rephrase_node_id
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
# Update the nodes dictionary
|
|
100
|
-
if rephrase_node_id in allnodes:
|
|
101
|
-
del allnodes[rephrase_node_id]
|
|
102
|
-
|
|
103
|
-
# Update the hanging_node's outgoing edges
|
|
104
|
-
hanging_node.outgoing_edges = [
|
|
105
|
-
e
|
|
106
|
-
for e in hanging_node.outgoing_edges
|
|
107
|
-
if e["edgeID"] != edge["edgeID"]
|
|
108
|
-
]
|
|
109
|
-
break # Break after removing the loop for this hanging node
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def process_each_hanging_node(
|
|
113
|
-
hanging_nodes: list[NewNode],
|
|
114
|
-
obj: aif.Graph,
|
|
115
|
-
allnodes: dict[str, NewNode],
|
|
116
|
-
updated_hanging_nodes: list[NewNode],
|
|
117
|
-
) -> aif.Graph:
|
|
118
|
-
"""
|
|
119
|
-
Finds and connects plausible connected Nodes based on specific criteria related to dialogues and transitions.
|
|
120
|
-
|
|
121
|
-
The process involves several steps:
|
|
122
|
-
1) Navigate back to a Dialog Node, e.g., "Chris Philp: <text>", through the only connected "Asserting" Node, and save the speaker's name.
|
|
123
|
-
2) Move to the one or two directly connected "Default Transition" Nodes, considering both ingoing and outgoing connections.
|
|
124
|
-
3) For each "Default Transition" Node, navigate to the directly connected Dialog Node and check if the speaker's name is similar.
|
|
125
|
-
4) If the speaker's name matches, proceed from this Dialog Node to the next connected Node (e.g., "Asserting" or another type),
|
|
126
|
-
excluding "Default Transition" Nodes.
|
|
127
|
-
5) From the new node, such as an "Asserting" Node, move to another Argument Node, for example, "Mrs. X thinks that Y is good."
|
|
128
|
-
6) Verify if this Argument Node has a connection (either incoming or outgoing) from the hanging_node. If there is no such connection, create one.
|
|
129
|
-
|
|
130
|
-
Rough graph structure example:
|
|
131
|
-
|
|
132
|
-
---DefaultTransition--> <DialogNode> ---<Assert>--> <ArgumentNode>
|
|
133
|
-
<HangingNode> <--<Assert>-- <DialogNode>
|
|
134
|
-
<--DefaultTransition--- <DialogNode> ---<Assert>--> <ArgumentNode>
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
# 1)
|
|
139
|
-
# Iterate over hanging nodes
|
|
140
|
-
# all generated "Rephrase" Nodes should have the same datetime to filter them out for postprocessing
|
|
141
|
-
similar_datetime = pendulum.now().format(aif.DATE_FORMAT)
|
|
142
|
-
for hanging_node in hanging_nodes:
|
|
143
|
-
# Check if the hanging node has exactly one incoming edge
|
|
144
|
-
if len(hanging_node.incoming_edges) == 1:
|
|
145
|
-
# Get the ID of the connected node (Asserting node)
|
|
146
|
-
asserting_node_id = hanging_node.incoming_edges[0]["fromID"]
|
|
147
|
-
asserting_node = allnodes.get(asserting_node_id)
|
|
148
|
-
|
|
149
|
-
# Ensure the asserting node exists and follow its incoming edge
|
|
150
|
-
if asserting_node and len(asserting_node.incoming_edges) == 1:
|
|
151
|
-
# Get the ID of the next node (should be of type "L")
|
|
152
|
-
l_node_id = asserting_node.incoming_edges[0]["fromID"]
|
|
153
|
-
l_node = allnodes.get(l_node_id)
|
|
154
|
-
|
|
155
|
-
# Check if the node is of type "L" and process the speaker's name
|
|
156
|
-
if l_node and l_node.node_type == "L":
|
|
157
|
-
speaker_name = l_node.text.split(" :", 1)[
|
|
158
|
-
0
|
|
159
|
-
] # Extracting speaker's name
|
|
160
|
-
speakerNode = l_node # Save the original speaker's node
|
|
161
|
-
|
|
162
|
-
# 2) + 3)
|
|
163
|
-
# Find connected "Default Transition" nodes of type "TA"
|
|
164
|
-
default_transition_nodes = get_connected_nodes_of_type(
|
|
165
|
-
l_node, "TA", allnodes
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# For each "Default Transition" node, find connected "L" nodes and check if the speaker name is =
|
|
169
|
-
for dt_node in default_transition_nodes:
|
|
170
|
-
connected_dialog_nodes = get_connected_nodes_of_type(
|
|
171
|
-
dt_node, "L", allnodes
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Check if the speaker name aligns in the connected "L" nodes,excluding the original speakerNode
|
|
175
|
-
# For each Dialog-Node, find the next connected nodes excluding types "L" and "TA"
|
|
176
|
-
for dialog_node in connected_dialog_nodes:
|
|
177
|
-
if (
|
|
178
|
-
dialog_node.node_id != speakerNode.node_id
|
|
179
|
-
and dialog_node.text.startswith(speaker_name + " :")
|
|
180
|
-
):
|
|
181
|
-
# 4) + 5)
|
|
182
|
-
# Find connecting nodes
|
|
183
|
-
connecting_nodes = get_connected_nodes_excluding_types(
|
|
184
|
-
dialog_node, ["L", "TA"], allnodes
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
# For each connecting node, find the next Argument-Nodes
|
|
188
|
-
for connecting_node in connecting_nodes:
|
|
189
|
-
argument_nodes = (
|
|
190
|
-
get_connected_nodes_excluding_types(
|
|
191
|
-
connecting_node, ["L", "TA"], allnodes
|
|
192
|
-
)
|
|
193
|
-
)
|
|
194
|
-
# 6)
|
|
195
|
-
for (
|
|
196
|
-
argument_node
|
|
197
|
-
) in argument_nodes: # datetime für alle gleich
|
|
198
|
-
if not are_nodes_connected(
|
|
199
|
-
hanging_node, argument_node
|
|
200
|
-
):
|
|
201
|
-
new_node_id = utils.uuid()
|
|
202
|
-
new_node: aif.Node = {
|
|
203
|
-
"nodeID": new_node_id,
|
|
204
|
-
"text": "Default Rephrase",
|
|
205
|
-
"type": "MA",
|
|
206
|
-
"timestamp": similar_datetime,
|
|
207
|
-
}
|
|
208
|
-
obj["nodes"].append(new_node)
|
|
209
|
-
|
|
210
|
-
# Create edges connecting hanging_node -> new_node and new_node -> ArgumentNode
|
|
211
|
-
new_edge_1_id = utils.uuid()
|
|
212
|
-
new_edge_1: aif.Edge = {
|
|
213
|
-
"edgeID": new_edge_1_id,
|
|
214
|
-
"fromID": hanging_node.node_id,
|
|
215
|
-
"toID": new_node_id,
|
|
216
|
-
"formEdgeID": None,
|
|
217
|
-
}
|
|
218
|
-
obj["edges"].append(new_edge_1)
|
|
219
|
-
|
|
220
|
-
new_edge_2_id = utils.uuid()
|
|
221
|
-
new_edge_2: aif.Edge = {
|
|
222
|
-
"edgeID": new_edge_2_id,
|
|
223
|
-
"fromID": new_node_id,
|
|
224
|
-
"toID": argument_node.node_id,
|
|
225
|
-
"formEdgeID": None,
|
|
226
|
-
}
|
|
227
|
-
obj["edges"].append(new_edge_2)
|
|
228
|
-
# Update the edges in the copied hanging nodes list
|
|
229
|
-
# Find and update the hanging_node and argument_node in updated_hanging_nodes
|
|
230
|
-
for node in updated_hanging_nodes:
|
|
231
|
-
if node.node_id == hanging_node.node_id:
|
|
232
|
-
node.outgoing_edges.append(
|
|
233
|
-
new_edge_1
|
|
234
|
-
)
|
|
235
|
-
if (
|
|
236
|
-
node.node_id
|
|
237
|
-
== argument_node.node_id
|
|
238
|
-
):
|
|
239
|
-
node.incoming_edges.append(
|
|
240
|
-
new_edge_2
|
|
241
|
-
)
|
|
242
|
-
# check for loops:
|
|
243
|
-
remove_loops_in_hanging_nodes(updated_hanging_nodes, obj, allnodes)
|
|
244
|
-
return obj
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def create_and_process_nodes(obj: aif.Graph) -> dict[str, NewNode]:
|
|
248
|
-
"""
|
|
249
|
-
creates and processes all Nodes, so they contain every ingoing/outgoing edge
|
|
250
|
-
"""
|
|
251
|
-
nodes: dict[str, NewNode] = {}
|
|
252
|
-
for aif_node in obj["nodes"]:
|
|
253
|
-
node_id = aif_node["nodeID"]
|
|
254
|
-
node_text = aif_node.get("text", "")
|
|
255
|
-
node_type = aif_node.get("type", "")
|
|
256
|
-
node_timestamp = aif_node.get("timestamp", "")
|
|
257
|
-
nodes[node_id] = NewNode(node_id, node_text, node_type, node_timestamp)
|
|
258
|
-
|
|
259
|
-
# process edges and link them to nodes
|
|
260
|
-
for aif_edge in obj["edges"]:
|
|
261
|
-
edge_id = aif_edge["edgeID"]
|
|
262
|
-
from_id = aif_edge["fromID"]
|
|
263
|
-
to_id = aif_edge["toID"]
|
|
264
|
-
edge_data: aif.Edge = {
|
|
265
|
-
"edgeID": edge_id,
|
|
266
|
-
"fromID": from_id,
|
|
267
|
-
"toID": to_id,
|
|
268
|
-
"formEdgeID": None,
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if from_id in nodes:
|
|
272
|
-
nodes[from_id].add_outgoing_edge(edge_data)
|
|
273
|
-
if to_id in nodes:
|
|
274
|
-
nodes[to_id].add_incoming_edge(edge_data)
|
|
275
|
-
return nodes
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def find_hanging_nodes(allnodes: dict[str, NewNode]) -> list[NewNode]:
|
|
279
|
-
"""
|
|
280
|
-
Identifies nodes within a graph that have no outgoing edges and exactly one incoming edge, excluding nodes of type "L".
|
|
281
|
-
Returns:
|
|
282
|
-
A list of identified hanging nodes
|
|
283
|
-
"""
|
|
284
|
-
hanging_nodes = [
|
|
285
|
-
node
|
|
286
|
-
for node in allnodes.values()
|
|
287
|
-
if len(node.incoming_edges) == 1
|
|
288
|
-
and not node.outgoing_edges
|
|
289
|
-
and node.node_type != "L"
|
|
290
|
-
]
|
|
291
|
-
return hanging_nodes
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def process_hanging_nodes(obj: aif.Graph) -> aif.Graph:
|
|
295
|
-
"""
|
|
296
|
-
Finds and connects specific Nodes which aren't connected to other ArgumentNodes yet
|
|
297
|
-
"""
|
|
298
|
-
allnodes = create_and_process_nodes(obj)
|
|
299
|
-
hanging_nodes = find_hanging_nodes(allnodes)
|
|
300
|
-
updated_hanging_nodes = hanging_nodes.copy()
|
|
301
|
-
obj = process_each_hanging_node(hanging_nodes, obj, allnodes, updated_hanging_nodes)
|
|
302
|
-
return obj
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|