pyxecm 3.0.1__py3-none-any.whl → 3.1.1__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 +878 -70
- pyxecm/otcs.py +1716 -349
- 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.1.dist-info}/METADATA +2 -1
- pyxecm-3.1.1.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 +67 -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 +161 -79
- pyxecm_customizer/customizer.py +43 -25
- pyxecm_customizer/guidewire.py +422 -8
- pyxecm_customizer/k8s.py +23 -27
- pyxecm_customizer/knowledge_graph.py +498 -20
- pyxecm_customizer/m365.py +45 -44
- pyxecm_customizer/payload.py +1723 -1188
- 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.1.dist-info}/WHEEL +0 -0
- {pyxecm-3.0.1.dist-info → pyxecm-3.1.1.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,238 @@ 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_categories()
|
|
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 category ID for a category name.
|
|
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 ID (or category ID if attribute is None).
|
|
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_categories()
|
|
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 no 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
|
+
self.logger.error("Attribute '%s' not found in category '%s'!", lookup_key, category.get("id", "unknown"))
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
key = attributes[lookup_key]
|
|
327
|
+
|
|
328
|
+
if row is not None and "_x_" in key:
|
|
329
|
+
# We have to adjust for 0-based index:
|
|
330
|
+
key = key.replace("_x_", "_{}_".format(row))
|
|
331
|
+
elif row is not None:
|
|
332
|
+
self.logger.warning("Row specified for non-multi-line set attribute - ignoring row.")
|
|
333
|
+
|
|
334
|
+
return key
|
|
335
|
+
|
|
336
|
+
# end method definition
|
|
337
|
+
|
|
338
|
+
def get_result_values(self, response: dict, key: str, sub_keys: list[str] | None = None) -> list | None:
|
|
339
|
+
"""Read all values with a given key from the Knowledge Graph Query.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
response (dict):
|
|
343
|
+
Knowledge Graph query result. Example:
|
|
344
|
+
28038: {
|
|
345
|
+
'id': '28038',
|
|
346
|
+
'name': 'C.E.B. Berlin SE (10020)',
|
|
347
|
+
'attributes': {'Vendor': {...}}
|
|
348
|
+
'path': ['Material', 'Purchase Order', 'Vendor']
|
|
349
|
+
}
|
|
350
|
+
key (str):
|
|
351
|
+
Key to find (e.g., "name", "attributes").
|
|
352
|
+
sub_keys (list[str] | None, optional):
|
|
353
|
+
Sub keys to lookup data in a dictionary like "attributes".
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
list | None:
|
|
357
|
+
Value list of the item with the given key, or None if no value is found.
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
# First do some sanity checks:
|
|
362
|
+
if not response:
|
|
363
|
+
self.logger.debug("Empty query response - returning None")
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
# Initialize results variable:
|
|
367
|
+
results = []
|
|
368
|
+
|
|
369
|
+
# Loop through all graph response values:
|
|
370
|
+
for value in response.values():
|
|
371
|
+
# Check if the key does actually exist in the current value:
|
|
372
|
+
if key not in value:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
item = value[key]
|
|
376
|
+
|
|
377
|
+
# If sub-keys exist, loop through them and expand sub-dicts:
|
|
378
|
+
if sub_keys:
|
|
379
|
+
for sub_key in sub_keys:
|
|
380
|
+
if not isinstance(item, dict) or sub_key not in item:
|
|
381
|
+
item = None
|
|
382
|
+
break
|
|
383
|
+
item = item[sub_key]
|
|
384
|
+
|
|
385
|
+
# If a final item is found after expansion add it to the result list:
|
|
386
|
+
if item is not None:
|
|
387
|
+
results.append(item)
|
|
388
|
+
|
|
389
|
+
# We want to return None instead of an empty list:
|
|
390
|
+
return results or None
|
|
391
|
+
|
|
392
|
+
# end method definition
|
|
393
|
+
|
|
394
|
+
def get_result_values_iterator(
|
|
395
|
+
self,
|
|
396
|
+
response: dict,
|
|
397
|
+
) -> iter:
|
|
398
|
+
"""Get an iterator object that can be used to traverse through OTCS responses.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
response (dict):
|
|
402
|
+
REST API response object.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
list | None:
|
|
406
|
+
Value list of the item with the given key, or None if no value is found.
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
# First do some sanity checks:
|
|
411
|
+
if not response:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
yield from response.values()
|
|
415
|
+
|
|
416
|
+
# end method definition
|
|
417
|
+
|
|
144
418
|
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_type_graph")
|
|
145
419
|
def build_type_graph(self, directions: list | None = None) -> dict[str, set[str]]:
|
|
146
420
|
"""Construct a directed type-level graph from the ontology.
|
|
@@ -346,19 +620,30 @@ class KnowledgeGraph:
|
|
|
346
620
|
|
|
347
621
|
workspace_id = self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_ID_FIELD)
|
|
348
622
|
workspace_name = self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_NAME_FIELD)
|
|
349
|
-
|
|
623
|
+
workspace_description = self._otcs.get_result_value(
|
|
624
|
+
response=workspace_node, key=self.WORKSPACE_DESCRIPTION_FIELD
|
|
625
|
+
)
|
|
626
|
+
# We use the cached names of the workspace types:
|
|
627
|
+
workspace_type = self._workspace_types[
|
|
350
628
|
self._otcs.get_result_value(response=workspace_node, key=self.WORKSPACE_TYPE_FIELD)
|
|
351
629
|
]
|
|
352
630
|
data = {
|
|
353
631
|
"id": workspace_id,
|
|
354
632
|
"name": workspace_name,
|
|
633
|
+
"description": workspace_description,
|
|
355
634
|
"type": workspace_type,
|
|
356
635
|
**kwargs, # ← allows adding more attributes from caller
|
|
357
636
|
}
|
|
358
637
|
if metadata:
|
|
359
|
-
response = self._otcs.get_workspace(node_id=workspace_id, fields="categories", metadata=True)
|
|
638
|
+
response = self._otcs.get_workspace(node_id=int(workspace_id), fields="categories", metadata=True)
|
|
360
639
|
if response:
|
|
361
640
|
data["attributes"] = self._otcs.extract_category_data(node=response)
|
|
641
|
+
else:
|
|
642
|
+
self.logger.warning(
|
|
643
|
+
"Workspace -> '%s' (%s) has no metadata! Cannot add it to the graph.",
|
|
644
|
+
workspace_name,
|
|
645
|
+
workspace_id,
|
|
646
|
+
)
|
|
362
647
|
with self._nodes.lock():
|
|
363
648
|
self._nodes.append(data)
|
|
364
649
|
return (True, True)
|
|
@@ -407,8 +692,8 @@ class KnowledgeGraph:
|
|
|
407
692
|
"target_id": workspace_target_id,
|
|
408
693
|
"relationship_type": rel_type,
|
|
409
694
|
"relationship_semantics": self.get_semantic_labels(
|
|
410
|
-
source_type=self.
|
|
411
|
-
target_type=self.
|
|
695
|
+
source_type=self._workspace_types.get(workspace_source_type, workspace_source_type),
|
|
696
|
+
target_type=self._workspace_types.get(workspace_target_type, workspace_target_type),
|
|
412
697
|
rel_type=rel_type,
|
|
413
698
|
),
|
|
414
699
|
**kwargs, # ← allows adding more attributes from caller
|
|
@@ -433,6 +718,9 @@ class KnowledgeGraph:
|
|
|
433
718
|
metadata=metadata,
|
|
434
719
|
)
|
|
435
720
|
|
|
721
|
+
# mark the graph as ready:
|
|
722
|
+
self._graph_ready = True
|
|
723
|
+
|
|
436
724
|
return result
|
|
437
725
|
|
|
438
726
|
# end method definition
|
|
@@ -474,7 +762,7 @@ class KnowledgeGraph:
|
|
|
474
762
|
intermediate_types: list[str] | None = None,
|
|
475
763
|
strict_intermediate_types: bool = False,
|
|
476
764
|
ordered_intermediate_types: bool = False,
|
|
477
|
-
) ->
|
|
765
|
+
) -> dict:
|
|
478
766
|
"""Find target entities by using the Knowledge Graph.
|
|
479
767
|
|
|
480
768
|
Given a source entity (like Material:M-789), find target entities (like Customer)
|
|
@@ -488,7 +776,7 @@ class KnowledgeGraph:
|
|
|
488
776
|
target_type (str):
|
|
489
777
|
Desired result type (e.g. "Customer").
|
|
490
778
|
target_value (str | int | None, optional):
|
|
491
|
-
The value or name of the
|
|
779
|
+
The value or name of the target node (e.g. "Global Trade AG").
|
|
492
780
|
max_hops (int | None, optional):
|
|
493
781
|
Limit on graph traversal depth. If None (default) there's no limit.
|
|
494
782
|
direction (str, optional):
|
|
@@ -502,8 +790,55 @@ class KnowledgeGraph:
|
|
|
502
790
|
Enforce order of intermediate types.
|
|
503
791
|
|
|
504
792
|
Returns:
|
|
505
|
-
|
|
506
|
-
|
|
793
|
+
dict:
|
|
794
|
+
Dictionary with node ID as key and values consisting of "name" and
|
|
795
|
+
optional "attributes" keys. "attributes" are just provided if the graph
|
|
796
|
+
was built with "metadata=True".
|
|
797
|
+
|
|
798
|
+
Example:
|
|
799
|
+
{
|
|
800
|
+
29023: {
|
|
801
|
+
'name': 'C.E.B. Berlin SE (10020)',
|
|
802
|
+
'attributes': {
|
|
803
|
+
'Vendor': {
|
|
804
|
+
'Locations': [
|
|
805
|
+
{
|
|
806
|
+
'Type': 'Stand. Address',
|
|
807
|
+
'Street': 'Potsdamer Platz 1',
|
|
808
|
+
'City': 'Berlin',
|
|
809
|
+
'Country': 'Germany',
|
|
810
|
+
'Postal code': '10785',
|
|
811
|
+
'Valid from': '2016-01-31T11:00:00',
|
|
812
|
+
'Valid to': '9999-12-31T11:00:00'
|
|
813
|
+
}
|
|
814
|
+
],
|
|
815
|
+
'Contacts': [
|
|
816
|
+
{
|
|
817
|
+
'BP No': '0000001101',
|
|
818
|
+
'Name': 'Matthias Schwarz',
|
|
819
|
+
'Department': '',
|
|
820
|
+
'Function': '',
|
|
821
|
+
'Phone': '',
|
|
822
|
+
...
|
|
823
|
+
}
|
|
824
|
+
],
|
|
825
|
+
'Name': 'C.E.B. Berlin SE',
|
|
826
|
+
'Street': 'Potsdamer Platz 1',
|
|
827
|
+
'Bank Accounts': [...],
|
|
828
|
+
'City': 'Berlin',
|
|
829
|
+
'Postal Code': '10785',
|
|
830
|
+
'Country': 'Germany',
|
|
831
|
+
'Purchasing Organization': ['1000 Innovate Germany'],
|
|
832
|
+
'Number': '10020',
|
|
833
|
+
'Key': '0000010020'
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
29140: {
|
|
838
|
+
'name': 'C.E.B. New York Inc. (30010)',
|
|
839
|
+
'attributes': {...}
|
|
840
|
+
},
|
|
841
|
+
...
|
|
507
842
|
|
|
508
843
|
"""
|
|
509
844
|
|
|
@@ -522,7 +857,7 @@ class KnowledgeGraph:
|
|
|
522
857
|
& (nodes_df["name"].astype(str).str.contains(str(source_value), case=False, na=False, regex=False))
|
|
523
858
|
]
|
|
524
859
|
if source_node.empty:
|
|
525
|
-
return
|
|
860
|
+
return {}
|
|
526
861
|
start_id = source_node.iloc[0]["id"]
|
|
527
862
|
|
|
528
863
|
#
|
|
@@ -530,7 +865,7 @@ class KnowledgeGraph:
|
|
|
530
865
|
#
|
|
531
866
|
visited = set()
|
|
532
867
|
queue = deque([(start_id, 0, [])]) # (node_id, depth, path_types)
|
|
533
|
-
results =
|
|
868
|
+
results: dict[int, dict[str, object]] = {}
|
|
534
869
|
# Cache of build_type_pathes results
|
|
535
870
|
type_path_cache = {}
|
|
536
871
|
|
|
@@ -555,6 +890,7 @@ class KnowledgeGraph:
|
|
|
555
890
|
|
|
556
891
|
current_type = node_row.iloc[0]["type"]
|
|
557
892
|
current_name = node_row.iloc[0]["name"]
|
|
893
|
+
current_attributes = node_row.iloc[0].get("attributes", {})
|
|
558
894
|
|
|
559
895
|
# Check if this node has been traversed before. If yes, skip.
|
|
560
896
|
if current_id in visited:
|
|
@@ -626,7 +962,7 @@ class KnowledgeGraph:
|
|
|
626
962
|
if (
|
|
627
963
|
target_value is not None
|
|
628
964
|
and target_value != current_name # exact match
|
|
629
|
-
and target_value.lower() not in current_name.lower() # partial match (substring)
|
|
965
|
+
and str(target_value).lower() not in current_name.lower() # partial match (substring)
|
|
630
966
|
):
|
|
631
967
|
self.logger.debug(
|
|
632
968
|
"Target node -> '%s' (%d) has the right type -> '%s' but not matching name or attributes (%s)",
|
|
@@ -637,7 +973,13 @@ class KnowledgeGraph:
|
|
|
637
973
|
)
|
|
638
974
|
continue
|
|
639
975
|
|
|
640
|
-
results
|
|
976
|
+
results[current_id] = {
|
|
977
|
+
"id": current_id,
|
|
978
|
+
"name": current_name,
|
|
979
|
+
"type": target_type,
|
|
980
|
+
"attributes": current_attributes,
|
|
981
|
+
"path": path_types,
|
|
982
|
+
}
|
|
641
983
|
self.logger.debug(
|
|
642
984
|
"Found node -> '%s' (%d) of desired target type -> '%s'%s%s",
|
|
643
985
|
current_name,
|
|
@@ -702,14 +1044,15 @@ class KnowledgeGraph:
|
|
|
702
1044
|
# Get neighbor nodes with their types
|
|
703
1045
|
neighbor_rows = nodes_df[nodes_df["id"].isin(neighbor_ids)][["id", "type"]]
|
|
704
1046
|
|
|
705
|
-
# Filter neighbors by allowed types
|
|
1047
|
+
# Filter neighbors by allowed types and not visited before:
|
|
706
1048
|
filtered_neighbors = [
|
|
707
1049
|
nid
|
|
708
1050
|
for nid in neighbor_rows.itertuples(index=False)
|
|
709
1051
|
if nid.type in allowed_types and nid.id not in visited
|
|
710
1052
|
]
|
|
711
1053
|
|
|
712
|
-
# Travers edges from current node to neighbors
|
|
1054
|
+
# Travers edges from current node to neighbors and
|
|
1055
|
+
# add the neighbors to the processing queue for traversal:
|
|
713
1056
|
for neighbor in filtered_neighbors:
|
|
714
1057
|
queue.append((neighbor.id, current_depth + 1, path_types))
|
|
715
1058
|
# end while queue
|
|
@@ -717,3 +1060,138 @@ class KnowledgeGraph:
|
|
|
717
1060
|
return results
|
|
718
1061
|
|
|
719
1062
|
# end method definition
|
|
1063
|
+
|
|
1064
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attribute")
|
|
1065
|
+
def build_attributes(self, node: dict, **kwargs: dict) -> tuple[bool, bool]: # noqa: ARG002
|
|
1066
|
+
"""Build a dictionary of all attributes in a given category in OTCS.
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
node (dict):
|
|
1070
|
+
The category node to process.
|
|
1071
|
+
path (list[str]):
|
|
1072
|
+
The current path in the category tree.
|
|
1073
|
+
kwargs (dict):
|
|
1074
|
+
Optional additional parameters.
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
bool:
|
|
1078
|
+
Whether or not the operation was successful.
|
|
1079
|
+
bool:
|
|
1080
|
+
Whether or not we require further traversal.
|
|
1081
|
+
|
|
1082
|
+
"""
|
|
1083
|
+
|
|
1084
|
+
if not node:
|
|
1085
|
+
return (False, False)
|
|
1086
|
+
|
|
1087
|
+
success = True
|
|
1088
|
+
traverse = True
|
|
1089
|
+
|
|
1090
|
+
node_id = self._otcs.get_result_value(response=node, key="id")
|
|
1091
|
+
node_name = self._otcs.get_result_value(response=node, key="name")
|
|
1092
|
+
node_type = self._otcs.get_result_value(response=node, key="type")
|
|
1093
|
+
if node_type != self._otcs.ITEM_TYPE_CATEGORY:
|
|
1094
|
+
success = False
|
|
1095
|
+
if node_type not in [self._otcs.VOLUME_TYPE_CATEGORIES_VOLUME, self._otcs.ITEM_TYPE_CATEGORY_FOLDER]:
|
|
1096
|
+
traverse = False
|
|
1097
|
+
|
|
1098
|
+
if success:
|
|
1099
|
+
# Initialize the entry on the category level:
|
|
1100
|
+
self._attribute_schemas[node_name] = {"id": node_id, "attributes": {}}
|
|
1101
|
+
# Retrieve the attribute schema for the given category.
|
|
1102
|
+
# We use the Personal Volume as node_id to have a predictable
|
|
1103
|
+
# node ID that is always available:
|
|
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
|
+
attributes = self._otcs.get_node_category_form(
|
|
1107
|
+
node_id=personal_volume_id, category_id=node_id, operation="create"
|
|
1108
|
+
)
|
|
1109
|
+
if not attributes or "forms" not in attributes or not attributes["forms"]:
|
|
1110
|
+
self.logger.error(
|
|
1111
|
+
"Cannot retrieve attribute schema for category -> '%s' (%s)!",
|
|
1112
|
+
node_name,
|
|
1113
|
+
node_id,
|
|
1114
|
+
)
|
|
1115
|
+
return (False, False)
|
|
1116
|
+
attributes = attributes["forms"][0]["schema"]["properties"] if attributes else {}
|
|
1117
|
+
|
|
1118
|
+
for key, value in attributes.items():
|
|
1119
|
+
if not key[0].isdigit() or "title" not in value:
|
|
1120
|
+
continue
|
|
1121
|
+
self._attribute_schemas[node_name]["attributes"][value["title"]] = key
|
|
1122
|
+
# Process sub-attributes in single-row set:
|
|
1123
|
+
if "properties" in value:
|
|
1124
|
+
for sub_key, sub_value in value["properties"].items():
|
|
1125
|
+
if "title" not in sub_value:
|
|
1126
|
+
continue
|
|
1127
|
+
self._attribute_schemas[node_name]["attributes"][value["title"] + ":" + sub_value["title"]] = (
|
|
1128
|
+
sub_key
|
|
1129
|
+
)
|
|
1130
|
+
# Process sub-attributes in multi-row set:
|
|
1131
|
+
if "items" in value and "properties" in value["items"]:
|
|
1132
|
+
for sub_key, sub_value in value["items"]["properties"].items():
|
|
1133
|
+
if "title" not in sub_value:
|
|
1134
|
+
continue
|
|
1135
|
+
self._attribute_schemas[node_name]["attributes"][value["title"] + ":" + sub_value["title"]] = (
|
|
1136
|
+
sub_key
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
return (success, traverse)
|
|
1140
|
+
|
|
1141
|
+
# end method definition
|
|
1142
|
+
|
|
1143
|
+
@tracer.start_as_current_span(attributes=OTEL_TRACING_ATTRIBUTES, name="build_attributes")
|
|
1144
|
+
def build_categories(self) -> dict:
|
|
1145
|
+
"""Build a dictionary of all attribute schemas in OTCS.
|
|
1146
|
+
|
|
1147
|
+
This method populates the `self._attribute_schemas` dictionary with
|
|
1148
|
+
attribute schemas for each category found in the OTCS categories volume.
|
|
1149
|
+
|
|
1150
|
+
It allows for fast lookup of IDs of categories and their attributes
|
|
1151
|
+
by their human-readable names that we use in the AttributesModel in
|
|
1152
|
+
the Aviator tools.
|
|
1153
|
+
|
|
1154
|
+
Each dictionary entry has the following format:
|
|
1155
|
+
{
|
|
1156
|
+
'Category Name': {
|
|
1157
|
+
'id': <category_id>,
|
|
1158
|
+
'attributes': {
|
|
1159
|
+
'Attribute Title': 'attribute_key',
|
|
1160
|
+
...
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
...
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
dict | None:
|
|
1168
|
+
The number of processed and traversed nodes. Format:
|
|
1169
|
+
{
|
|
1170
|
+
"processed": int,
|
|
1171
|
+
"traversed": int,
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
Example:
|
|
1176
|
+
{
|
|
1177
|
+
'Customer': {
|
|
1178
|
+
'id': 20739,
|
|
1179
|
+
'attributes': {}
|
|
1180
|
+
},
|
|
1181
|
+
...
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
"""
|
|
1185
|
+
|
|
1186
|
+
self.logger.debug("Start building attribute data lookup...")
|
|
1187
|
+
|
|
1188
|
+
result = self._otcs.traverse_node(
|
|
1189
|
+
node=self._otcs.get_volume(volume_type=self._otcs.VOLUME_TYPE_CATEGORIES_VOLUME),
|
|
1190
|
+
executables=[self.build_attributes],
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
self.logger.debug("Attribute data lookup completed.")
|
|
1194
|
+
|
|
1195
|
+
return result
|
|
1196
|
+
|
|
1197
|
+
# end class definition
|