pyxecm 3.0.1__py3-none-any.whl → 3.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/avts.py +4 -4
- pyxecm/coreshare.py +14 -15
- pyxecm/helper/data.py +2 -1
- pyxecm/helper/web.py +11 -11
- pyxecm/helper/xml.py +41 -10
- pyxecm/otac.py +1 -1
- pyxecm/otawp.py +19 -19
- pyxecm/otca.py +870 -67
- pyxecm/otcs.py +1567 -280
- pyxecm/otds.py +332 -153
- pyxecm/otkd.py +4 -4
- pyxecm/otmm.py +1 -1
- pyxecm/otpd.py +246 -30
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/METADATA +2 -1
- pyxecm-3.1.0.dist-info/RECORD +82 -0
- pyxecm_api/app.py +45 -35
- pyxecm_api/auth/functions.py +2 -2
- pyxecm_api/auth/router.py +2 -3
- pyxecm_api/common/functions.py +164 -12
- pyxecm_api/settings.py +0 -8
- pyxecm_api/terminal/router.py +1 -1
- pyxecm_api/v1_csai/router.py +33 -18
- pyxecm_customizer/browser_automation.py +98 -48
- pyxecm_customizer/customizer.py +43 -25
- pyxecm_customizer/guidewire.py +422 -8
- pyxecm_customizer/k8s.py +23 -27
- pyxecm_customizer/knowledge_graph.py +501 -20
- pyxecm_customizer/m365.py +45 -44
- pyxecm_customizer/payload.py +1684 -1159
- pyxecm_customizer/payload_list.py +3 -0
- pyxecm_customizer/salesforce.py +122 -79
- pyxecm_customizer/servicenow.py +27 -7
- pyxecm_customizer/settings.py +3 -1
- pyxecm_customizer/successfactors.py +2 -2
- pyxecm_customizer/translate.py +1 -1
- pyxecm-3.0.1.dist-info/RECORD +0 -96
- pyxecm_api/agents/__init__.py +0 -7
- pyxecm_api/agents/app.py +0 -13
- pyxecm_api/agents/functions.py +0 -119
- pyxecm_api/agents/models.py +0 -10
- pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
- pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
- pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
- pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
- pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
- pyxecm_api/agents/otcm_user_agent/models.py +0 -20
- pyxecm_api/agents/otcm_user_agent/router.py +0 -65
- pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
- pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
- pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/WHEEL +0 -0
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -30,6 +30,7 @@ class KnowledgeGraph:
|
|
|
30
30
|
|
|
31
31
|
WORKSPACE_ID_FIELD = "id"
|
|
32
32
|
WORKSPACE_NAME_FIELD = "name"
|
|
33
|
+
WORKSPACE_DESCRIPTION_FIELD = "description"
|
|
33
34
|
WORKSPACE_TYPE_FIELD = "wnf_wksp_type_id"
|
|
34
35
|
|
|
35
36
|
def __init__(self, otcs_object: OTCS, ontology: dict[tuple[str, str, str], list[str]] | None = None) -> None:
|
|
@@ -76,16 +77,35 @@ class KnowledgeGraph:
|
|
|
76
77
|
self._type_graph_inverted = self.invert_type_graph(self._type_graph)
|
|
77
78
|
|
|
78
79
|
# This include the pandas data frames for the node and edge INSTANCES:
|
|
79
|
-
self._nodes = Data() # columns=["id", "name", "type"])
|
|
80
|
-
self._edges = Data() # (columns=["
|
|
80
|
+
self._nodes = Data() # columns=["id", "name", "type", "description", "attributes"])
|
|
81
|
+
self._edges = Data() # (columns=["source_type", "source_id", "target_type", "target_id", "relationship_type", "relationship_semantics"])
|
|
81
82
|
|
|
82
83
|
# Create a simple dictionary with all workspace types to easily
|
|
83
84
|
# lookup the workspace type name (value) by the workspace type ID (key):
|
|
84
85
|
workspace_types = self._otcs.get_workspace_types()
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
if workspace_types:
|
|
87
|
+
self._workspace_types = {
|
|
88
|
+
wt["data"]["properties"]["wksp_type_id"]: wt["data"]["properties"]["wksp_type_name"]
|
|
89
|
+
for wt in workspace_types.get("results", [])
|
|
90
|
+
}
|
|
91
|
+
else:
|
|
92
|
+
self._workspace_types = {}
|
|
93
|
+
|
|
94
|
+
self._attribute_schemas = {}
|
|
95
|
+
self._graph_ready = False
|
|
96
|
+
|
|
97
|
+
# end method definition
|
|
98
|
+
|
|
99
|
+
def is_graph_ready(self) -> bool:
|
|
100
|
+
"""Return whether or not the graph has been built and is ready for queries.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
bool:
|
|
104
|
+
True if the graph is ready, False otherwise.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
return self._graph_ready
|
|
89
109
|
|
|
90
110
|
# end method definition
|
|
91
111
|
|
|
@@ -102,6 +122,28 @@ class KnowledgeGraph:
|
|
|
102
122
|
|
|
103
123
|
# end method definition
|
|
104
124
|
|
|
125
|
+
def get_node_by_id(self, node_id: int) -> dict | None:
|
|
126
|
+
"""Return the graph node with the given ID as a Data object.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
node_id (int):
|
|
130
|
+
ID of the node to retrieve.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict | None:
|
|
134
|
+
Dictionary representation of the node of the Knowledge Graph, or None if not found.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
data_frame = self._nodes.get_data_frame()
|
|
139
|
+
node_data = data_frame.loc[data_frame["id"] == node_id]
|
|
140
|
+
if not node_data.empty:
|
|
141
|
+
return node_data.to_dict(orient="records")[0]
|
|
142
|
+
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# end method definition
|
|
146
|
+
|
|
105
147
|
def get_edges(self) -> Data:
|
|
106
148
|
"""Return the graph edges as a Data object.
|
|
107
149
|
|
|
@@ -141,6 +183,241 @@ class KnowledgeGraph:
|
|
|
141
183
|
|
|
142
184
|
# end method definition
|
|
143
185
|
|
|
186
|
+
def get_workspace_types(self) -> dict:
|
|
187
|
+
"""Return the workspace types dictionary.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
dict:
|
|
191
|
+
Dictionary mapping workspace type IDs to workspace type names.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
return self._workspace_types
|
|
196
|
+
|
|
197
|
+
# end method definition
|
|
198
|
+
|
|
199
|
+
def get_workspace_type_names(self) -> list:
|
|
200
|
+
"""Return all workspace type names as a list.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
list:
|
|
204
|
+
All workspace type names.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
return list(self._workspace_types.values())
|
|
209
|
+
|
|
210
|
+
# end method definition
|
|
211
|
+
|
|
212
|
+
def get_workspace_type_name(self, workspace_type_id: str) -> str | None:
|
|
213
|
+
"""Return the workspace type name for a given workspace type ID.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
workspace_type_id (str):
|
|
217
|
+
The ID of the workspace type.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
str | None:
|
|
221
|
+
The name of the workspace type, or None if not found.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if self._workspace_types is None:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
return self._workspace_types.get(workspace_type_id, None)
|
|
229
|
+
|
|
230
|
+
# end method definition
|
|
231
|
+
|
|
232
|
+
def get_attribute_schemas(self) -> dict:
|
|
233
|
+
"""Return the attribute schemas dictionary.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
dict:
|
|
237
|
+
Dictionary mapping attribute names to their schemas.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
return self._attribute_schemas
|
|
242
|
+
|
|
243
|
+
# end method definition
|
|
244
|
+
|
|
245
|
+
def get_category_names(self) -> list:
|
|
246
|
+
"""Return all category names as a list.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
list:
|
|
250
|
+
All category item names.
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
return list(self._attribute_schemas.keys())
|
|
255
|
+
|
|
256
|
+
# end method definition
|
|
257
|
+
|
|
258
|
+
def get_category_attributes(self, category: str) -> list:
|
|
259
|
+
"""Return all attribute names as a list.
|
|
260
|
+
|
|
261
|
+
Attributes in a set use "<set_name>:<attribute_name>" as key.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
list:
|
|
265
|
+
All attribute names of a given category.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
if not self._attribute_schemas:
|
|
270
|
+
self.build_attributes()
|
|
271
|
+
|
|
272
|
+
return list(self._attribute_schemas.get(category, {}).get("attributes", {}).keys())
|
|
273
|
+
|
|
274
|
+
# end method definition
|
|
275
|
+
|
|
276
|
+
def get_category_id(self, category: str) -> str | None:
|
|
277
|
+
"""Return the attribute schemas dictionary.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
category (str):
|
|
281
|
+
The category of the attribute.
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
return self.get_attribute_id(category=category)
|
|
285
|
+
|
|
286
|
+
# end method definition
|
|
287
|
+
|
|
288
|
+
def get_attribute_id(
|
|
289
|
+
self, category: str, attribute: str | None = None, set_attribute: str | None = None, row: int | None = None
|
|
290
|
+
) -> str | None:
|
|
291
|
+
"""Return the attribute schemas dictionary.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
category (str):
|
|
295
|
+
The category of the attribute.
|
|
296
|
+
attribute (str):
|
|
297
|
+
The name of the attribute.
|
|
298
|
+
set_attribute (str | None, optional):
|
|
299
|
+
The set of the attribute.
|
|
300
|
+
row (int | None, optional):
|
|
301
|
+
The row number to retrieve (1-based index for set lines).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
str:
|
|
305
|
+
OTCS category ID or attribute ID.
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
if not self._attribute_schemas:
|
|
310
|
+
self.build_attributes()
|
|
311
|
+
|
|
312
|
+
# Lookup the category schema by the category name:
|
|
313
|
+
category = self._attribute_schemas.get(category, {})
|
|
314
|
+
if not category:
|
|
315
|
+
return None
|
|
316
|
+
if not attribute:
|
|
317
|
+
# If now specific attribute is requested return the category ID:
|
|
318
|
+
return category.get("id", None)
|
|
319
|
+
attributes = category.get("attributes", {})
|
|
320
|
+
# Construct the attribute lookup key. For set attributes we use the format "Set:Attribute".
|
|
321
|
+
lookup_key = attribute if not set_attribute else f"{set_attribute}:{attribute}"
|
|
322
|
+
if lookup_key not in attributes:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
if lookup_key not in attributes:
|
|
326
|
+
self.logger.error("Attribute '%s' not found in category '%s'!", lookup_key, category.get("id", "unknown"))
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
key = attributes[lookup_key]
|
|
330
|
+
|
|
331
|
+
if row is not None and "_x_" in key:
|
|
332
|
+
# We have to adjust for 0-based index:
|
|
333
|
+
key = key.replace("_x_", "_{}_".format(row))
|
|
334
|
+
elif row is not None:
|
|
335
|
+
self.logger.warning("Row specified for non-multi-line set attribute - ignoring row.")
|
|
336
|
+
|
|
337
|
+
return key
|
|
338
|
+
|
|
339
|
+
# end method definition
|
|
340
|
+
|
|
341
|
+
def get_result_values(self, response: dict, key: str, sub_keys: list[str] | None = None) -> list | None:
|
|
342
|
+
"""Read all values with a given key from the Knowledge Graph Query.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
response (dict):
|
|
346
|
+
Knowledge Graph query result. Example:
|
|
347
|
+
28038: {
|
|
348
|
+
'id': '28038',
|
|
349
|
+
'name': 'C.E.B. Berlin SE (10020)',
|
|
350
|
+
'attributes': {'Vendor': {...}}
|
|
351
|
+
'path': ['Material', 'Purchase Order', 'Vendor']
|
|
352
|
+
}
|
|
353
|
+
key (str):
|
|
354
|
+
Key to find (e.g., "name", "attributes").
|
|
355
|
+
sub_keys (list[str] | None, optional):
|
|
356
|
+
Sub keys to lookup data in a dictionary like "attributes".
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
list | None:
|
|
360
|
+
Value list of the item with the given key, or None if no value is found.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
# First do some sanity checks:
|
|
365
|
+
if not response:
|
|
366
|
+
self.logger.debug("Empty query response - returning None")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# Initialize results variable:
|
|
370
|
+
results = []
|
|
371
|
+
|
|
372
|
+
# Loop through all graph response values:
|
|
373
|
+
for value in response.values():
|
|
374
|
+
# Check if the key does actually exist in the current value:
|
|
375
|
+
if key not in value:
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
item = value[key]
|
|
379
|
+
|
|
380
|
+
# If sub-keys exist, loop through them and expand sub-dicts:
|
|
381
|
+
if sub_keys:
|
|
382
|
+
for sub_key in sub_keys:
|
|
383
|
+
if not isinstance(item, dict) or sub_key not in item:
|
|
384
|
+
item = None
|
|
385
|
+
break
|
|
386
|
+
item = item[sub_key]
|
|
387
|
+
|
|
388
|
+
# If a final item is found after expansion add it to the result list:
|
|
389
|
+
if item is not None:
|
|
390
|
+
results.append(item)
|
|
391
|
+
|
|
392
|
+
# We want to return None instead of an empty list:
|
|
393
|
+
return results or None
|
|
394
|
+
|
|
395
|
+
# end method definition
|
|
396
|
+
|
|
397
|
+
def get_result_values_iterator(
|
|
398
|
+
self,
|
|
399
|
+
response: dict,
|
|
400
|
+
) -> iter:
|
|
401
|
+
"""Get an iterator object that can be used to traverse through OTCS responses.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
response (dict):
|
|
405
|
+
REST API response object.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
list | None:
|
|
409
|
+
Value list of the item with the given key, or None if no value is found.
|
|
410
|
+
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
# First do some sanity checks:
|
|
414
|
+
if not response:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
yield from response.values()
|
|
418
|
+
|
|
419
|
+
# end method definition
|
|
420
|
+
|
|
144
421
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_type_graph")
|
|
145
422
|
def build_type_graph(self, directions: list | None = None) -> dict[str, set[str]]:
|
|
146
423
|
"""Construct a directed type-level graph from the ontology.
|
|
@@ -346,19 +623,30 @@ class KnowledgeGraph:
|
|
|
346
623
|
|
|
347
624
|
workspace_id = self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_ID_FIELD)
|
|
348
625
|
workspace_name = self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_NAME_FIELD)
|
|
349
|
-
|
|
626
|
+
workspace_description = self._otcs.get_result_value(
|
|
627
|
+
response=workspace_node, key=self.WORKSPACE_DESCRIPTION_FIELD
|
|
628
|
+
)
|
|
629
|
+
# We use the cached names of the workspace types:
|
|
630
|
+
workspace_type = self._workspace_types[
|
|
350
631
|
self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_TYPE_FIELD)
|
|
351
632
|
]
|
|
352
633
|
data = {
|
|
353
634
|
"id": workspace_id,
|
|
354
635
|
"name": workspace_name,
|
|
636
|
+
"description": workspace_description,
|
|
355
637
|
"type": workspace_type,
|
|
356
638
|
**kwargs, # ← allows adding more attributes from caller
|
|
357
639
|
}
|
|
358
640
|
if metadata:
|
|
359
|
-
response = self._otcs.get_workspace(node_id=workspace_id, fields="categories", metadata=True)
|
|
641
|
+
response = self._otcs.get_workspace(node_id=int(workspace_id), fields="categories", metadata=True)
|
|
360
642
|
if response:
|
|
361
643
|
data["attributes"] = self._otcs.extract_category_data(node=response)
|
|
644
|
+
else:
|
|
645
|
+
self.logger.warning(
|
|
646
|
+
"Workspace -> '%s' (%s) has no metadata! Cannot add it to the graph.",
|
|
647
|
+
workspace_name,
|
|
648
|
+
workspace_id,
|
|
649
|
+
)
|
|
362
650
|
with self._nodes.lock():
|
|
363
651
|
self._nodes.append(data)
|
|
364
652
|
return (True, True)
|
|
@@ -407,8 +695,8 @@ class KnowledgeGraph:
|
|
|
407
695
|
"target_id": workspace_target_id,
|
|
408
696
|
"relationship_type": rel_type,
|
|
409
697
|
"relationship_semantics": self.get_semantic_labels(
|
|
410
|
-
source_type=self.
|
|
411
|
-
target_type=self.
|
|
698
|
+
source_type=self._workspace_types.get(workspace_source_type, workspace_source_type),
|
|
699
|
+
target_type=self._workspace_types.get(workspace_target_type, workspace_target_type),
|
|
412
700
|
rel_type=rel_type,
|
|
413
701
|
),
|
|
414
702
|
**kwargs, # ← allows adding more attributes from caller
|
|
@@ -433,6 +721,9 @@ class KnowledgeGraph:
|
|
|
433
721
|
metadata=metadata,
|
|
434
722
|
)
|
|
435
723
|
|
|
724
|
+
# mark the graph as ready:
|
|
725
|
+
self._graph_ready = True
|
|
726
|
+
|
|
436
727
|
return result
|
|
437
728
|
|
|
438
729
|
# end method definition
|
|
@@ -474,7 +765,7 @@ class KnowledgeGraph:
|
|
|
474
765
|
intermediate_types: list[str] | None = None,
|
|
475
766
|
strict_intermediate_types: bool = False,
|
|
476
767
|
ordered_intermediate_types: bool = False,
|
|
477
|
-
) ->
|
|
768
|
+
) -> dict:
|
|
478
769
|
"""Find target entities by using the Knowledge Graph.
|
|
479
770
|
|
|
480
771
|
Given a source entity (like Material:M-789), find target entities (like Customer)
|
|
@@ -488,7 +779,7 @@ class KnowledgeGraph:
|
|
|
488
779
|
target_type (str):
|
|
489
780
|
Desired result type (e.g. "Customer").
|
|
490
781
|
target_value (str | int | None, optional):
|
|
491
|
-
The value or name of the
|
|
782
|
+
The value or name of the target node (e.g. "Global Trade AG").
|
|
492
783
|
max_hops (int | None, optional):
|
|
493
784
|
Limit on graph traversal depth. If None (default) there's no limit.
|
|
494
785
|
direction (str, optional):
|
|
@@ -502,8 +793,55 @@ class KnowledgeGraph:
|
|
|
502
793
|
Enforce order of intermediate types.
|
|
503
794
|
|
|
504
795
|
Returns:
|
|
505
|
-
|
|
506
|
-
|
|
796
|
+
dict:
|
|
797
|
+
Dictionary with node ID as key and values consisting of "name" and
|
|
798
|
+
optional "attributes" keys. "attributes" are just provided if the graph
|
|
799
|
+
was built with "metadata=True".
|
|
800
|
+
|
|
801
|
+
Example:
|
|
802
|
+
{
|
|
803
|
+
29023: {
|
|
804
|
+
'name': 'C.E.B. Berlin SE (10020)',
|
|
805
|
+
'attributes': {
|
|
806
|
+
'Vendor': {
|
|
807
|
+
'Locations': [
|
|
808
|
+
{
|
|
809
|
+
'Type': 'Stand. Address',
|
|
810
|
+
'Street': 'Potsdamer Platz 1',
|
|
811
|
+
'City': 'Berlin',
|
|
812
|
+
'Country': 'Germany',
|
|
813
|
+
'Postal code': '10785',
|
|
814
|
+
'Valid from': '2016-01-31T11:00:00',
|
|
815
|
+
'Valid to': '9999-12-31T11:00:00'
|
|
816
|
+
}
|
|
817
|
+
],
|
|
818
|
+
'Contacts': [
|
|
819
|
+
{
|
|
820
|
+
'BP No': '0000001101',
|
|
821
|
+
'Name': 'Matthias Schwarz',
|
|
822
|
+
'Department': '',
|
|
823
|
+
'Function': '',
|
|
824
|
+
'Phone': '',
|
|
825
|
+
...
|
|
826
|
+
}
|
|
827
|
+
],
|
|
828
|
+
'Name': 'C.E.B. Berlin SE',
|
|
829
|
+
'Street': 'Potsdamer Platz 1',
|
|
830
|
+
'Bank Accounts': [...],
|
|
831
|
+
'City': 'Berlin',
|
|
832
|
+
'Postal Code': '10785',
|
|
833
|
+
'Country': 'Germany',
|
|
834
|
+
'Purchasing Organization': ['1000 Innovate Germany'],
|
|
835
|
+
'Number': '10020',
|
|
836
|
+
'Key': '0000010020'
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
29140: {
|
|
841
|
+
'name': 'C.E.B. New York Inc. (30010)',
|
|
842
|
+
'attributes': {...}
|
|
843
|
+
},
|
|
844
|
+
...
|
|
507
845
|
|
|
508
846
|
"""
|
|
509
847
|
|
|
@@ -522,7 +860,7 @@ class KnowledgeGraph:
|
|
|
522
860
|
& (nodes_df["name"].astype(str).str.contains(str(source_value), case=False, na=False, regex=False))
|
|
523
861
|
]
|
|
524
862
|
if source_node.empty:
|
|
525
|
-
return
|
|
863
|
+
return {}
|
|
526
864
|
start_id = source_node.iloc[0]["id"]
|
|
527
865
|
|
|
528
866
|
#
|
|
@@ -530,7 +868,7 @@ class KnowledgeGraph:
|
|
|
530
868
|
#
|
|
531
869
|
visited = set()
|
|
532
870
|
queue = deque([(start_id, 0, [])]) # (node_id, depth, path_types)
|
|
533
|
-
results =
|
|
871
|
+
results: dict[int, dict[str, object]] = {}
|
|
534
872
|
# Cache of build_type_pathes results
|
|
535
873
|
type_path_cache = {}
|
|
536
874
|
|
|
@@ -555,6 +893,7 @@ class KnowledgeGraph:
|
|
|
555
893
|
|
|
556
894
|
current_type = node_row.iloc[0]["type"]
|
|
557
895
|
current_name = node_row.iloc[0]["name"]
|
|
896
|
+
current_attributes = node_row.iloc[0].get("attributes", {})
|
|
558
897
|
|
|
559
898
|
# Check if this node has been traversed before. If yes, skip.
|
|
560
899
|
if current_id in visited:
|
|
@@ -626,7 +965,7 @@ class KnowledgeGraph:
|
|
|
626
965
|
if (
|
|
627
966
|
target_value is not None
|
|
628
967
|
and target_value != current_name # exact match
|
|
629
|
-
and target_value.lower() not in current_name.lower() # partial match (substring)
|
|
968
|
+
and str(target_value).lower() not in current_name.lower() # partial match (substring)
|
|
630
969
|
):
|
|
631
970
|
self.logger.debug(
|
|
632
971
|
"Target node -> '%s' (%d) has the right type -> '%s' but not matching name or attributes (%s)",
|
|
@@ -637,7 +976,13 @@ class KnowledgeGraph:
|
|
|
637
976
|
)
|
|
638
977
|
continue
|
|
639
978
|
|
|
640
|
-
results
|
|
979
|
+
results[current_id] = {
|
|
980
|
+
"id": current_id,
|
|
981
|
+
"name": current_name,
|
|
982
|
+
"type": target_type,
|
|
983
|
+
"attributes": current_attributes,
|
|
984
|
+
"path": path_types,
|
|
985
|
+
}
|
|
641
986
|
self.logger.debug(
|
|
642
987
|
"Found node -> '%s' (%d) of desired target type -> '%s'%s%s",
|
|
643
988
|
current_name,
|
|
@@ -702,14 +1047,15 @@ class KnowledgeGraph:
|
|
|
702
1047
|
# Get neighbor nodes with their types
|
|
703
1048
|
neighbor_rows = nodes_df[nodes_df["id"].isin(neighbor_ids)][["id", "type"]]
|
|
704
1049
|
|
|
705
|
-
# Filter neighbors by allowed types
|
|
1050
|
+
# Filter neighbors by allowed types and not visited before:
|
|
706
1051
|
filtered_neighbors = [
|
|
707
1052
|
nid
|
|
708
1053
|
for nid in neighbor_rows.itertuples(index=False)
|
|
709
1054
|
if nid.type in allowed_types and nid.id not in visited
|
|
710
1055
|
]
|
|
711
1056
|
|
|
712
|
-
# Travers edges from current node to neighbors
|
|
1057
|
+
# Travers edges from current node to neighbors and
|
|
1058
|
+
# add the neighbors to the processing queue for traversal:
|
|
713
1059
|
for neighbor in filtered_neighbors:
|
|
714
1060
|
queue.append((neighbor.id, current_depth + 1, path_types))
|
|
715
1061
|
# end while queue
|
|
@@ -717,3 +1063,138 @@ class KnowledgeGraph:
|
|
|
717
1063
|
return results
|
|
718
1064
|
|
|
719
1065
|
# end method definition
|
|
1066
|
+
|
|
1067
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attribute")
|
|
1068
|
+
def build_attribute(self, node: dict, **kwargs: dict) -> tuple[bool, bool]: # noqa: ARG002
|
|
1069
|
+
"""Build a dictionary of all attribute schemas in OTCS.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
node (dict):
|
|
1073
|
+
The category node to process.
|
|
1074
|
+
path (list[str]):
|
|
1075
|
+
The current path in the category tree.
|
|
1076
|
+
kwargs (dict):
|
|
1077
|
+
Optional additional parameters.
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
bool:
|
|
1081
|
+
Whether or not the operation was successful.
|
|
1082
|
+
bool:
|
|
1083
|
+
Whether or not we require further traversal.
|
|
1084
|
+
|
|
1085
|
+
"""
|
|
1086
|
+
|
|
1087
|
+
if not node:
|
|
1088
|
+
return (False, False)
|
|
1089
|
+
|
|
1090
|
+
success = True
|
|
1091
|
+
traverse = True
|
|
1092
|
+
|
|
1093
|
+
node_id = self._otcs.get_result_value(response=node, key="id")
|
|
1094
|
+
node_name = self._otcs.get_result_value(response=node, key="name")
|
|
1095
|
+
node_type = self._otcs.get_result_value(response=node, key="type")
|
|
1096
|
+
if node_type != self._otcs.ITEM_TYPE_CATEGORY:
|
|
1097
|
+
success = False
|
|
1098
|
+
if node_type not in [self._otcs.VOLUME_TYPE_CATEGORIES_VOLUME, self._otcs.ITEM_TYPE_CATEGORY_FOLDER]:
|
|
1099
|
+
traverse = False
|
|
1100
|
+
|
|
1101
|
+
if success:
|
|
1102
|
+
# Initialize the entry on the category level:
|
|
1103
|
+
self._attribute_schemas[node_name] = {"id": node_id, "attributes": {}}
|
|
1104
|
+
personal_volume = self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_PERSONAL_WORKSPACE)
|
|
1105
|
+
personal_volume_id = self._otcs.get_result_value(response=personal_volume, key="id")
|
|
1106
|
+
# Retrieve the attribute schema for the given category.
|
|
1107
|
+
# We use the Enterprise Vault (2000) as node_id to have a predictable
|
|
1108
|
+
# node ID that is always available:
|
|
1109
|
+
attributes = self._otcs.get_node_category_form(
|
|
1110
|
+
node_id=personal_volume_id, category_id=node_id, operation="create"
|
|
1111
|
+
)
|
|
1112
|
+
if not attributes or "forms" not in attributes or not attributes["forms"]:
|
|
1113
|
+
self.logger.error(
|
|
1114
|
+
"Cannot retrieve attribute schema for category -> '%s' (%s)!",
|
|
1115
|
+
node_name,
|
|
1116
|
+
node_id,
|
|
1117
|
+
)
|
|
1118
|
+
return (False, False)
|
|
1119
|
+
attributes = attributes["forms"][0]["schema"]["properties"] if attributes else {}
|
|
1120
|
+
|
|
1121
|
+
for key, value in attributes.items():
|
|
1122
|
+
if not key[0].isdigit() or "title" not in value:
|
|
1123
|
+
continue
|
|
1124
|
+
self._attribute_schemas[node_name]["attributes"][value["title"]] = key
|
|
1125
|
+
# Process sub-attributes in single-row set:
|
|
1126
|
+
if "properties" in value:
|
|
1127
|
+
for sub_key, sub_value in value["properties"].items():
|
|
1128
|
+
if "title" not in sub_value:
|
|
1129
|
+
continue
|
|
1130
|
+
self._attribute_schemas[node_name]["attributes"][value["title"] + ":" + sub_value["title"]] = (
|
|
1131
|
+
sub_key
|
|
1132
|
+
)
|
|
1133
|
+
# Process sub-attributes in multi-row set:
|
|
1134
|
+
if "items" in value and "properties" in value["items"]:
|
|
1135
|
+
for sub_key, sub_value in value["items"]["properties"].items():
|
|
1136
|
+
if "title" not in sub_value:
|
|
1137
|
+
continue
|
|
1138
|
+
self._attribute_schemas[node_name]["attributes"][value["title"] + ":" + sub_value["title"]] = (
|
|
1139
|
+
sub_key
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
return (success, traverse)
|
|
1143
|
+
|
|
1144
|
+
# end method definition
|
|
1145
|
+
|
|
1146
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attributes")
|
|
1147
|
+
def build_attributes(self) -> dict:
|
|
1148
|
+
"""Build a dictionary of all attribute schemas in OTCS.
|
|
1149
|
+
|
|
1150
|
+
This method populates the `self._attribute_schemas` dictionary with
|
|
1151
|
+
attribute schemas for each category found in the OTCS categories volume.
|
|
1152
|
+
|
|
1153
|
+
It allows for fast lookup of IDs of categories and their attributes
|
|
1154
|
+
by their human-readable names that we use in the AttributesModel in
|
|
1155
|
+
the Aviator tools.
|
|
1156
|
+
|
|
1157
|
+
Each dictionary entry has the following format:
|
|
1158
|
+
{
|
|
1159
|
+
'Category Name': {
|
|
1160
|
+
'id': <category_id>,
|
|
1161
|
+
'attributes': {
|
|
1162
|
+
'Attribute Title': 'attribute_key',
|
|
1163
|
+
...
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
...
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
dict | None:
|
|
1171
|
+
The number of processed and traversed nodes. Format:
|
|
1172
|
+
{
|
|
1173
|
+
"processed": int,
|
|
1174
|
+
"traversed": int,
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
Example:
|
|
1179
|
+
{
|
|
1180
|
+
'Customer': {
|
|
1181
|
+
'id': 20739,
|
|
1182
|
+
'attributes': {}
|
|
1183
|
+
},
|
|
1184
|
+
...
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
"""
|
|
1188
|
+
|
|
1189
|
+
self.logger.info("Starting attribute data lookup build...")
|
|
1190
|
+
|
|
1191
|
+
result = self._otcs.traverse_node(
|
|
1192
|
+
node=self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_CATEGORIES_VOLUME),
|
|
1193
|
+
executables=[self.build_attribute],
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
self.logger.info("Attributes data lookup build completed.")
|
|
1197
|
+
|
|
1198
|
+
return result
|
|
1199
|
+
|
|
1200
|
+
# end class definition
|