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.
Files changed (87) hide show
  1. {arguebuf-2.7.0/src/arguebuf.egg-info → arguebuf-2.7.2}/PKG-INFO +3 -2
  2. {arguebuf-2.7.0 → arguebuf-2.7.2}/pyproject.toml +1 -1
  3. arguebuf-2.7.2/src/arguebuf/load/_load_aif.py +368 -0
  4. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/_render_graphviz.py +1 -1
  5. {arguebuf-2.7.0 → arguebuf-2.7.2/src/arguebuf.egg-info}/PKG-INFO +3 -2
  6. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/SOURCES.txt +1 -1
  7. arguebuf-2.7.2/tests/test_preprocess_dialog.py +134 -0
  8. arguebuf-2.7.0/src/arguebuf/load/_load_aif.py +0 -106
  9. arguebuf-2.7.0/src/arguebuf/load/_preprocess_aif.py +0 -302
  10. {arguebuf-2.7.0 → arguebuf-2.7.2}/LICENSE +0 -0
  11. {arguebuf-2.7.0 → arguebuf-2.7.2}/README.md +0 -0
  12. {arguebuf-2.7.0 → arguebuf-2.7.2}/setup.cfg +0 -0
  13. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/__init__.py +0 -0
  14. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/__init__.py +0 -0
  15. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/__main__.py +0 -0
  16. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/app.py +0 -0
  17. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/graph.py +0 -0
  18. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/model.py +0 -0
  19. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/server.py +0 -0
  20. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/text.py +0 -0
  21. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/cli/translator.py +0 -0
  22. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dt.py +0 -0
  23. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/__init__.py +0 -0
  24. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_config.py +0 -0
  25. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_aif.py +0 -0
  26. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_d2.py +0 -0
  27. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_dict.py +0 -0
  28. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_graphviz.py +0 -0
  29. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_io.py +0 -0
  30. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_json.py +0 -0
  31. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_networkx.py +0 -0
  32. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_path.py +0 -0
  33. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_protobuf.py +0 -0
  34. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/dump/_dump_xaif.py +0 -0
  35. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/__init__.py +0 -0
  36. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_config.py +0 -0
  37. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_aml.py +0 -0
  38. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_argdown.py +0 -0
  39. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_brat.py +0 -0
  40. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_casebase.py +0 -0
  41. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_dict.py +0 -0
  42. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_io.py +0 -0
  43. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_json.py +0 -0
  44. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_kialo.py +0 -0
  45. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_microtexts.py +0 -0
  46. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_ova.py +0 -0
  47. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_path.py +0 -0
  48. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_protobuf.py +0 -0
  49. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_sadface.py +0 -0
  50. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_text.py +0 -0
  51. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/load/_load_xaif.py +0 -0
  52. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/__init__.py +0 -0
  53. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/analyst.py +0 -0
  54. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/edge.py +0 -0
  55. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/graph.py +0 -0
  56. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/metadata.py +0 -0
  57. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/node.py +0 -0
  58. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/participant.py +0 -0
  59. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/reference.py +0 -0
  60. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/resource.py +0 -0
  61. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/scheme.py +0 -0
  62. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/typing.py +0 -0
  63. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/userdata.py +0 -0
  64. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/model/utils.py +0 -0
  65. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/py.typed +0 -0
  66. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/__init__.py +0 -0
  67. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/render/_render_d2.py +0 -0
  68. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/__init__.py +0 -0
  69. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/aif.py +0 -0
  70. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/argdown.py +0 -0
  71. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/d2.py +0 -0
  72. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/graphviz.py +0 -0
  73. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/microtexts.py +0 -0
  74. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/ova.py +0 -0
  75. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/sadface.py +0 -0
  76. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/schemas/xaif.py +0 -0
  77. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/traverse.py +0 -0
  78. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf/utils.py +0 -0
  79. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/dependency_links.txt +0 -0
  80. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/entry_points.txt +0 -0
  81. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/requires.txt +0 -0
  82. {arguebuf-2.7.0 → arguebuf-2.7.2}/src/arguebuf.egg-info/top_level.txt +0 -0
  83. {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_create_graph.py +0 -0
  84. {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_casebase.py +0 -0
  85. {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_edge.py +0 -0
  86. {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_load_node.py +0 -0
  87. {arguebuf-2.7.0 → arguebuf-2.7.2}/tests/test_render_graph.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: arguebuf
3
- Version: 2.7.0
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arguebuf"
3
- version = "2.7.0"
3
+ version = "2.7.2"
4
4
  description = "Create and analyze argument graphs and serialize them via Protobuf"
5
5
  authors = [{ name = "Mirko Lenz", email = "mirko@mirkolenz.com" }]
6
6
  readme = "README.md"
@@ -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.2
1
+ Metadata-Version: 2.4
2
2
  Name: arguebuf
3
- Version: 2.7.0
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