pyxecm 2.0.4__py3-none-any.whl → 3.0.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.

Files changed (94) hide show
  1. pyxecm/coreshare.py +5 -3
  2. pyxecm/helper/data.py +4 -4
  3. pyxecm/helper/otel_config.py +26 -0
  4. pyxecm/helper/web.py +1 -2
  5. pyxecm/otca.py +1356 -16
  6. pyxecm/otcs.py +2354 -593
  7. pyxecm/otds.py +1 -1
  8. pyxecm/otmm.py +4 -5
  9. pyxecm/py.typed +0 -0
  10. pyxecm-3.0.1.dist-info/METADATA +126 -0
  11. pyxecm-3.0.1.dist-info/RECORD +96 -0
  12. {pyxecm-2.0.4.dist-info → pyxecm-3.0.1.dist-info}/WHEEL +1 -2
  13. pyxecm-3.0.1.dist-info/entry_points.txt +4 -0
  14. {pyxecm/customizer/api → pyxecm_api}/__main__.py +1 -1
  15. pyxecm_api/agents/__init__.py +7 -0
  16. pyxecm_api/agents/app.py +13 -0
  17. pyxecm_api/agents/functions.py +119 -0
  18. pyxecm_api/agents/models.py +10 -0
  19. pyxecm_api/agents/otcm_knowledgegraph/functions.py +85 -0
  20. pyxecm_api/agents/otcm_knowledgegraph/models.py +61 -0
  21. pyxecm_api/agents/otcm_knowledgegraph/router.py +74 -0
  22. pyxecm_api/agents/otcm_user_agent/models.py +20 -0
  23. pyxecm_api/agents/otcm_user_agent/router.py +65 -0
  24. pyxecm_api/agents/otcm_workspace_agent/models.py +40 -0
  25. pyxecm_api/agents/otcm_workspace_agent/router.py +200 -0
  26. pyxecm_api/app.py +221 -0
  27. {pyxecm/customizer/api → pyxecm_api}/auth/functions.py +10 -2
  28. {pyxecm/customizer/api → pyxecm_api}/auth/router.py +4 -3
  29. {pyxecm/customizer/api → pyxecm_api}/common/functions.py +39 -9
  30. {pyxecm/customizer/api → pyxecm_api}/common/metrics.py +1 -2
  31. {pyxecm/customizer/api → pyxecm_api}/common/router.py +7 -8
  32. {pyxecm/customizer/api → pyxecm_api}/settings.py +21 -6
  33. {pyxecm/customizer/api → pyxecm_api}/terminal/router.py +1 -1
  34. {pyxecm/customizer/api → pyxecm_api}/v1_csai/router.py +39 -10
  35. pyxecm_api/v1_csai/statics/bindings/utils.js +189 -0
  36. pyxecm_api/v1_csai/statics/tom-select/tom-select.complete.min.js +356 -0
  37. pyxecm_api/v1_csai/statics/tom-select/tom-select.css +334 -0
  38. pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.css +1 -0
  39. pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.min.js +27 -0
  40. pyxecm_api/v1_maintenance/__init__.py +1 -0
  41. {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/functions.py +3 -3
  42. {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/router.py +8 -8
  43. pyxecm_api/v1_otcs/__init__.py +1 -0
  44. {pyxecm/customizer/api → pyxecm_api}/v1_otcs/functions.py +7 -5
  45. {pyxecm/customizer/api → pyxecm_api}/v1_otcs/router.py +8 -7
  46. pyxecm_api/v1_payload/__init__.py +1 -0
  47. {pyxecm/customizer/api → pyxecm_api}/v1_payload/functions.py +10 -7
  48. {pyxecm/customizer/api → pyxecm_api}/v1_payload/router.py +11 -10
  49. {pyxecm/customizer → pyxecm_customizer}/__init__.py +8 -0
  50. {pyxecm/customizer → pyxecm_customizer}/__main__.py +15 -21
  51. {pyxecm/customizer → pyxecm_customizer}/browser_automation.py +414 -103
  52. {pyxecm/customizer → pyxecm_customizer}/customizer.py +178 -116
  53. {pyxecm/customizer → pyxecm_customizer}/guidewire.py +60 -20
  54. {pyxecm/customizer → pyxecm_customizer}/k8s.py +4 -4
  55. pyxecm_customizer/knowledge_graph.py +719 -0
  56. pyxecm_customizer/log.py +35 -0
  57. {pyxecm/customizer → pyxecm_customizer}/m365.py +41 -33
  58. {pyxecm/customizer → pyxecm_customizer}/payload.py +2265 -1933
  59. {pyxecm/customizer/api/common → pyxecm_customizer}/payload_list.py +18 -55
  60. {pyxecm/customizer → pyxecm_customizer}/salesforce.py +1 -1
  61. {pyxecm/customizer → pyxecm_customizer}/sap.py +6 -2
  62. {pyxecm/customizer → pyxecm_customizer}/servicenow.py +2 -4
  63. {pyxecm/customizer → pyxecm_customizer}/settings.py +7 -6
  64. {pyxecm/customizer → pyxecm_customizer}/successfactors.py +40 -28
  65. {pyxecm/customizer → pyxecm_customizer}/translate.py +1 -1
  66. {pyxecm/maintenance_page → pyxecm_maintenance_page}/__main__.py +1 -1
  67. {pyxecm/maintenance_page → pyxecm_maintenance_page}/app.py +14 -8
  68. pyxecm/customizer/api/app.py +0 -157
  69. pyxecm/customizer/log.py +0 -107
  70. pyxecm/customizer/nhc.py +0 -1169
  71. pyxecm/customizer/openapi.py +0 -258
  72. pyxecm/customizer/pht.py +0 -1357
  73. pyxecm-2.0.4.dist-info/METADATA +0 -119
  74. pyxecm-2.0.4.dist-info/RECORD +0 -78
  75. pyxecm-2.0.4.dist-info/licenses/LICENSE +0 -202
  76. pyxecm-2.0.4.dist-info/top_level.txt +0 -1
  77. {pyxecm/customizer/api → pyxecm_api}/__init__.py +0 -0
  78. {pyxecm/customizer/api/auth → pyxecm_api/agents/otcm_knowledgegraph}/__init__.py +0 -0
  79. {pyxecm/customizer/api/common → pyxecm_api/agents/otcm_user_agent}/__init__.py +0 -0
  80. {pyxecm/customizer/api/v1_csai → pyxecm_api/agents/otcm_workspace_agent}/__init__.py +0 -0
  81. {pyxecm/customizer/api/v1_maintenance → pyxecm_api/auth}/__init__.py +0 -0
  82. {pyxecm/customizer/api → pyxecm_api}/auth/models.py +0 -0
  83. {pyxecm/customizer/api/v1_otcs → pyxecm_api/common}/__init__.py +0 -0
  84. {pyxecm/customizer/api → pyxecm_api}/common/models.py +0 -0
  85. {pyxecm/customizer/api → pyxecm_api}/terminal/__init__.py +0 -0
  86. {pyxecm/customizer/api/v1_payload → pyxecm_api/v1_csai}/__init__.py +0 -0
  87. {pyxecm/customizer/api → pyxecm_api}/v1_csai/models.py +0 -0
  88. {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/models.py +0 -0
  89. {pyxecm/customizer/api → pyxecm_api}/v1_payload/models.py +0 -0
  90. {pyxecm/customizer → pyxecm_customizer}/exceptions.py +0 -0
  91. {pyxecm/maintenance_page → pyxecm_maintenance_page}/__init__.py +0 -0
  92. {pyxecm/maintenance_page → pyxecm_maintenance_page}/settings.py +0 -0
  93. {pyxecm/maintenance_page → pyxecm_maintenance_page}/static/favicon.avif +0 -0
  94. {pyxecm/maintenance_page → pyxecm_maintenance_page}/templates/maintenance.html +0 -0
pyxecm/otca.py CHANGED
@@ -45,6 +45,16 @@ REQUEST_MAX_RETRIES = 2
45
45
 
46
46
  default_logger = logging.getLogger(MODULE_NAME)
47
47
 
48
+ try:
49
+ from pyvis.network import Network
50
+
51
+ pyvis_installed = True
52
+ except ModuleNotFoundError:
53
+ default_logger.warning(
54
+ "Module pyvis is not installed. Customizer will not support graph visualization.",
55
+ )
56
+ pyvis_installed = False
57
+
48
58
 
49
59
  class OTCA:
50
60
  """Interact with Content Aviator REST API."""
@@ -55,15 +65,19 @@ class OTCA:
55
65
  _context = ""
56
66
  _embed_token: str | None = None
57
67
  _chat_token: str | None = None
68
+ _chat_token_hashed: str | None = None
69
+ _node_dictionary: dict = {}
58
70
 
59
71
  def __init__(
60
72
  self,
61
73
  chat_url: str,
62
74
  embed_url: str,
75
+ studio_url: str,
63
76
  otds_url: str,
64
77
  client_id: str,
65
78
  client_secret: str,
66
- otcs_object: OTCS,
79
+ content_system: dict | None = None,
80
+ otcs_object: OTCS | None = None,
67
81
  synonyms: list | None = None,
68
82
  inline_citation: bool = True,
69
83
  logger: logging.Logger = default_logger,
@@ -75,14 +89,18 @@ class OTCA:
75
89
  The Content Aviator base URL for chat.
76
90
  embed_url (str):
77
91
  The Content Aviator base URL for embedding.
92
+ studio_url (str):
93
+ The base URL of Content Aviator Studio.
78
94
  otds_url (str):
79
95
  The OTDS URL.
80
96
  client_id (str):
81
97
  The Core Share Client ID.
82
98
  client_secret (str):
83
99
  The Core Share client secret.
100
+ content_system (dict | None):
101
+ The Content System configuration for the services which control the authentication.
84
102
  otcs_object (OTCS):
85
- The OTCS object.
103
+ The OTCS object..
86
104
  synonyms (list):
87
105
  List of synonyms that are used to generate a better response to the user.
88
106
  inline_citation (bool):
@@ -102,6 +120,13 @@ class OTCA:
102
120
  otca_config["chatUrl"] = chat_url + "/v1/chat"
103
121
  otca_config["searchUrl"] = chat_url + "/v1/context"
104
122
  otca_config["embedUrl"] = embed_url + "/v1/embeddings"
123
+ otca_config["studioGraphsUrl"] = studio_url + "/studio/v1/graphs"
124
+ otca_config["studioAgentsUrl"] = studio_url + "/studio/v1/agents"
125
+ otca_config["studioToolsUrl"] = studio_url + "/studio/v1/tools"
126
+ otca_config["studioRulesUrl"] = studio_url + "/studio/v1/rules"
127
+ otca_config["studioModelsUrl"] = studio_url + "/studio/v1/api/models"
128
+
129
+ otca_config["content_system"] = content_system if content_system else {"chat": "xecm", "embed": "xecm"}
105
130
  otca_config["clientId"] = client_id
106
131
  otca_config["clientSecret"] = client_secret
107
132
  otca_config["otdsUrl"] = otds_url
@@ -196,10 +221,24 @@ class OTCA:
196
221
  if content_type:
197
222
  request_header["Content-Type"] = content_type
198
223
 
199
- if service_type == "chat" and self._chat_token is not None:
200
- request_header["Authorization"] = "Bearer {}".format(self._chat_token)
224
+ # Configure default Content System
225
+ content_system = self.config()["content_system"].get(service_type, "none")
226
+
227
+ if content_system == "none":
228
+ return request_header
201
229
 
202
- elif service_type == "embed" and self._embed_token is not None:
230
+ if service_type == "chat":
231
+ if self._chat_token is None:
232
+ self.authenticate_chat()
233
+
234
+ if content_system == "xecm":
235
+ request_header["Authorization"] = "Bearer {}".format(self._chat_token_hashed)
236
+ elif content_system == "xecm-direct":
237
+ request_header["otcsticket"] = self._chat_token
238
+
239
+ elif service_type == "embed":
240
+ if self._embed_token is None:
241
+ self.authenticate_embed()
203
242
  request_header["Authorization"] = "Bearer {}".format(self._embed_token)
204
243
 
205
244
  return request_header
@@ -220,6 +259,7 @@ class OTCA:
220
259
  success_message: str = "",
221
260
  max_retries: int = REQUEST_MAX_RETRIES,
222
261
  retry_forever: bool = False,
262
+ parse_request_response: bool = True,
223
263
  ) -> dict | None:
224
264
  """Call an Content Aviator REST API in a safe way.
225
265
 
@@ -251,6 +291,9 @@ class OTCA:
251
291
  Number of retries on connection errors. Defaults to REQUEST_MAX_RETRIES.
252
292
  retry_forever (bool, optional):
253
293
  Whether to wait forever without timeout. Defaults to False.
294
+ parse_request_response (bool, optional):
295
+ Whether the response text should be interpreted as JSON and loaded
296
+ into a dictionary. Defaults to True.
254
297
 
255
298
  Returns:
256
299
  dict | None:
@@ -261,6 +304,17 @@ class OTCA:
261
304
  retries = 0
262
305
  while True:
263
306
  try:
307
+ self.logger.debug(
308
+ "Sending %s request ->\nurl: %s\nheaders: %s\ndata: %s\njson: %s\nfiles: %s\ntimeout: %s",
309
+ method,
310
+ url,
311
+ json.dumps(headers, indent=2),
312
+ json.dumps(data, indent=2),
313
+ json.dumps(json_data, indent=2),
314
+ files,
315
+ timeout,
316
+ )
317
+
264
318
  response = requests.request(
265
319
  method=method,
266
320
  url=url,
@@ -274,7 +328,10 @@ class OTCA:
274
328
  if response.ok:
275
329
  if success_message:
276
330
  self.logger.debug(success_message)
277
- return self.parse_request_response(response)
331
+ if parse_request_response:
332
+ return self.parse_request_response(response, show_error=show_error)
333
+ else:
334
+ return response
278
335
  # Check if Session has expired - then re-authenticate and try once more
279
336
  elif response.status_code == 401 and retries == 0:
280
337
  self.logger.debug("Session has expired - try to re-authenticate...")
@@ -404,7 +461,7 @@ class OTCA:
404
461
 
405
462
  # end method definition
406
463
 
407
- def authenticate_chat(self) -> str | None:
464
+ def authenticate_chat(self) -> str:
408
465
  """Authenticate for Chat service at Content Aviator / CSAI.
409
466
 
410
467
  Returns:
@@ -413,11 +470,20 @@ class OTCA:
413
470
 
414
471
  """
415
472
 
473
+ if self.otcs_object is None:
474
+ msg = "OTCS Object is not defined, authentication failed."
475
+ raise AttributeError(msg)
476
+
416
477
  token = self.otcs_object.otcs_ticket() or self.otcs_object.authenticate()
417
478
 
418
- if token and "otcsticket" in token:
479
+ if isinstance(token, dict) and "otcsticket" in token:
480
+ token = token["otcsticket"]
481
+
482
+ if token:
483
+ self._chat_token = token
484
+
419
485
  # Encode the input string before hashing
420
- encoded_string = token["otcsticket"].encode("utf-8")
486
+ encoded_string = token.encode("utf-8")
421
487
 
422
488
  # Create a new SHA-512 hash object
423
489
  sha512 = hashlib.sha512()
@@ -428,11 +494,14 @@ class OTCA:
428
494
  # Get the hexadecimal representation of the hash
429
495
  hashed_output = sha512.hexdigest()
430
496
 
431
- self._chat_token = hashed_output
497
+ self._chat_token_hashed = hashed_output
432
498
 
433
499
  return self._chat_token
434
500
 
435
- return None
501
+ else:
502
+ self.logger.error("Authentication failed. Token not found.")
503
+
504
+ return None
436
505
 
437
506
  # end method definition
438
507
 
@@ -591,7 +660,7 @@ class OTCA:
591
660
  ) -> dict:
592
661
  """Semantic search for text chunks.
593
662
 
594
- Search requests are meant to be called as end-users. This should involve
663
+ Search requests are meant to be called as end-users. This should involve
595
664
  passing the end-user's access token via the Authorization HTTP header.
596
665
  The chat service use OTDS's token endpoint to ensure that the token is valid.
597
666
 
@@ -683,18 +752,19 @@ class OTCA:
683
752
 
684
753
  Args:
685
754
  content (str | None):
686
- Content to be embedded. Can be empty for "delete" operations.
755
+ Content to be embedded. This is a document chunk. Can be empty for "delete" operations.
687
756
  operation (str):
688
757
  This can be either "add", "update" or "delete".
689
758
  document_id (int):
690
- The ID of the document the content originates from.
759
+ The ID of the document the content originates from. This becmes metadata in the vector store.
691
760
  workspace_id (int):
692
- The ID of the workspace the content originates from.
761
+ The ID of the workspace the content originates from. This becomes metadata in the vector store.
693
762
  additional_metadata (dict | None):
694
763
  Dictionary with additional metadata.
695
764
 
696
765
  Returns:
697
- dict: _description_
766
+ dict:
767
+ REST API response or None in case of an error.
698
768
 
699
769
  """
700
770
 
@@ -733,3 +803,1273 @@ class OTCA:
733
803
  )
734
804
 
735
805
  # end method definition
806
+
807
+ def get_graphs(self) -> list | None:
808
+ """Get all graphs.
809
+
810
+ Returns:
811
+ list:
812
+ A list of all graphs.
813
+
814
+ Example:
815
+ [
816
+ {
817
+ 'id': '60fa6f74-a0a6-4f95-abf4-80a3ae19913c',
818
+ 'name': 'supervisor',
819
+ 'description': None,
820
+ 'attributes': None,
821
+ 'createdAt': '2025-07-01T09:06:28.123Z',
822
+ 'updatedAt': '2025-07-01T09:06:28.123Z',
823
+ 'tenantId': '010bae82-7b31-4e52-9db4-00bde19aa398'
824
+ },
825
+ {
826
+ 'id': 'f287ef5e-0acf-47cf-91cb-64b3195ceeb8',
827
+ 'name': 'breakdown',
828
+ 'description': None,
829
+ 'attributes': None,
830
+ 'createdAt': '2025-07-01T09:06:28.123Z',
831
+ 'updatedAt': '2025-07-01T09:06:28.123Z',
832
+ 'tenantId': '010bae82-7b31-4e52-9db4-00bde19aa398'
833
+ },
834
+ {
835
+ 'id': '378a6369-6f78-4ccc-a1f1-8b070973be24',
836
+ 'name': 'root',
837
+ 'description': None,
838
+ 'attributes': None,
839
+ 'createdAt': '2025-07-01T09:06:28.123Z',
840
+ 'updatedAt': '2025-07-01T09:06:28.123Z',
841
+ 'tenantId': '010bae82-7b31-4e52-9db4-00bde19aa398'
842
+ },
843
+ {
844
+ 'id': '6925e805-eaea-4054-a07f-e3e48c7bab15',
845
+ 'name': 'answer',
846
+ 'description': None,
847
+ 'attributes': None,
848
+ 'createdAt': '2025-07-01T09:06:28.123Z',
849
+ 'updatedAt': '2025-07-01T09:06:28.123Z',
850
+ 'tenantId': '010bae82-7b31-4e52-9db4-00bde19aa398'
851
+ }
852
+ ]
853
+
854
+ """
855
+
856
+ request_url = self.config()["studioGraphsUrl"]
857
+ request_header = self.request_header(service_type="studio")
858
+
859
+ response = self.do_request(
860
+ url=request_url,
861
+ method="GET",
862
+ headers=request_header,
863
+ timeout=None,
864
+ show_error=True,
865
+ failure_message="Failed get graphs",
866
+ )
867
+
868
+ if response is None:
869
+ return None
870
+
871
+ return response.get("results", [])
872
+
873
+ # end method definition
874
+
875
+ def get_graphs_iterator(self) -> iter:
876
+ """Get an iterator object that can be used to traverse graphs.
877
+
878
+ Returns:
879
+ iter:
880
+ A generator yielding one graph per iteration.
881
+ If the REST API fails, returns no value.
882
+
883
+ Yields:
884
+ Iterator[iter]:
885
+ One graph at a time.
886
+
887
+ """
888
+
889
+ graphs: list = self.get_graphs()
890
+
891
+ yield from graphs
892
+
893
+ # end method definition
894
+
895
+ def get_graph(self, graph_id: str) -> dict | None:
896
+ """Get a graph by its ID.
897
+
898
+ Args:
899
+ graph_id (str):
900
+ The ID of the graph.
901
+
902
+ Returns:
903
+ dict | None:
904
+ Graph data or none in case of an error.
905
+
906
+ Example:
907
+ {
908
+ 'id': 'a245ddcb-2df0-465a-abab-b21222245ba9',
909
+ 'name': 'supervisor',
910
+ 'description': None,
911
+ 'attributes': None,
912
+ 'createdAt': '2025-07-01T16:51:56.703Z',
913
+ 'updatedAt': '2025-07-01T16:51:56.703Z',
914
+ 'tenantId': '05f43f12-5865-46cd-8954-1af3dc575e88'
915
+ }
916
+
917
+ """
918
+
919
+ request_url = self.config()["studioGraphsUrl"] + "/" + graph_id
920
+ request_header = self.request_header(service_type="studio")
921
+
922
+ return self.do_request(
923
+ url=request_url,
924
+ method="GET",
925
+ headers=request_header,
926
+ timeout=None,
927
+ show_error=True,
928
+ failure_message="Failed get graphs",
929
+ )
930
+
931
+ # end method definition
932
+
933
+ def get_graph_nodes(self, graph_id: str) -> list | None:
934
+ """Get all nodes of a graph.
935
+
936
+ Args:
937
+ graph_id (str):
938
+ The ID of the Graph to retrieve the nodes for.
939
+
940
+ Returns:
941
+ list | None:
942
+ A list of all nodes of the graph.
943
+
944
+ Example:
945
+ [
946
+ {
947
+ 'id': '1b99d09f-9e36-4da9-8fe0-8ebe0652fef3',
948
+ 'name': 'decision',
949
+ 'description': None,
950
+ 'discriminator': 1,
951
+ 'createdAt': '2025-07-01T09:06:28.140Z',
952
+ 'updatedAt': '2025-07-01T09:06:28.140Z',
953
+ 'version': 0,
954
+ 'status': 0,
955
+ 'graphId': '60fa6f74-a0a6-4f95-abf4-80a3ae19913c',
956
+ 'attributes': {
957
+ 'studio': 'routerAgent'
958
+ },
959
+ 'klassId': '20470453-2179-4392-aa0e-bc46ae3f3e80'
960
+ }
961
+ ]
962
+
963
+ """
964
+
965
+ request_url = self.config()["studioGraphsUrl"] + "/" + graph_id + "/nodes"
966
+ request_header = self.request_header(service_type="studio")
967
+
968
+ response = self.do_request(
969
+ url=request_url,
970
+ method="GET",
971
+ headers=request_header,
972
+ timeout=None,
973
+ show_error=True,
974
+ failure_message="Failed get list of graph nodes!",
975
+ )
976
+
977
+ if response is None:
978
+ return None
979
+
980
+ return response.get("results", [])
981
+
982
+ # end method definition
983
+
984
+ def get_graph_nodes_iterator(self, graph_id: str) -> iter:
985
+ """Get an iterator object that can be used to traverse graph nodes.
986
+
987
+ Returns:
988
+ iter:
989
+ A generator yielding one node per iteration.
990
+ If the REST API fails, returns no value.
991
+
992
+ Yields:
993
+ Iterator[iter]:
994
+ One node at a time.
995
+
996
+ """
997
+
998
+ nodes: list = self.get_graph_nodes(graph_id=graph_id)
999
+
1000
+ yield from nodes
1001
+
1002
+ # end method definition
1003
+
1004
+ def get_graph_nodes_by_name(self, name: str) -> list | None:
1005
+ """Get all nodes of a graph by name.
1006
+
1007
+ Args:
1008
+ name (str):
1009
+ The Name of the Graph to retrieve the nodes for.
1010
+
1011
+ Returns:
1012
+ list | None:
1013
+ A list of all nodes of the graph.
1014
+
1015
+ """
1016
+
1017
+ graphs = self.get_graphs()
1018
+
1019
+ if graphs is None:
1020
+ return None
1021
+
1022
+ graph = next((g for g in graphs if g["name"] == name), None)
1023
+
1024
+ if graph is None:
1025
+ self.logger.error("Graph -> '%s' not found!", name)
1026
+ return None
1027
+
1028
+ request_url = self.config()["studioGraphsUrl"] + "/" + graph["id"] + "/nodes"
1029
+ request_header = self.request_header(service_type="studio")
1030
+
1031
+ response = self.do_request(
1032
+ url=request_url,
1033
+ method="GET",
1034
+ headers=request_header,
1035
+ timeout=None,
1036
+ show_error=True,
1037
+ failure_message="Failed get list of graphs!",
1038
+ )
1039
+
1040
+ if response is None:
1041
+ return None
1042
+
1043
+ return response.get("results", [])
1044
+
1045
+ # end method definition
1046
+
1047
+ def get_graph_edges(self, graph_id: str) -> list | None:
1048
+ """Get all edges of a graph.
1049
+
1050
+ Args:
1051
+ graph_id (str):
1052
+ The ID of the Graph to retrieve the edges for.
1053
+
1054
+ Returns:
1055
+ list | None:
1056
+ A list of all edges of the graph.
1057
+
1058
+ Example:
1059
+ [
1060
+ {
1061
+ 'id': '420610ae-68d0-47d2-9807-6e5a5f75a02d',
1062
+ 'sourceId': '8cec788a-23eb-4480-a004-1f3c7edd1054',
1063
+ 'targetId': '233db9e1-1f5a-4fb6-8f70-fca46199c224',
1064
+ 'type': 0,
1065
+ 'graphId': '60fa6f74-a0a6-4f95-abf4-80a3ae19913c',
1066
+ 'createdAt': '2025-07-01T09:06:28.192Z',
1067
+ 'updatedAt': '2025-07-01T09:06:28.192Z',
1068
+ 'version': 0,
1069
+ 'status': 0,
1070
+ 'attributes': None
1071
+ }
1072
+ ]
1073
+
1074
+ """
1075
+
1076
+ request_url = self.config()["studioGraphsUrl"] + "/" + graph_id + "/edges"
1077
+ request_header = self.request_header(service_type="studio")
1078
+
1079
+ response = self.do_request(
1080
+ url=request_url,
1081
+ method="GET",
1082
+ headers=request_header,
1083
+ timeout=None,
1084
+ show_error=True,
1085
+ failure_message="Failed get list of graph edges!",
1086
+ )
1087
+
1088
+ if response is None:
1089
+ return None
1090
+
1091
+ return response.get("results", [])
1092
+
1093
+ # end method definition
1094
+
1095
+ def get_graph_edges_iterator(self, graph_id: str) -> iter:
1096
+ """Get an iterator object that can be used to traverse graph edges.
1097
+
1098
+ Returns:
1099
+ iter:
1100
+ A generator yielding one edge per iteration.
1101
+ If the REST API fails, returns no value.
1102
+
1103
+ Yields:
1104
+ Iterator[iter]:
1105
+ One edge at a time.
1106
+
1107
+ """
1108
+
1109
+ edges: list = self.get_graph_edges(graph_id=graph_id)
1110
+
1111
+ yield from edges
1112
+
1113
+ # end method definition
1114
+
1115
+ def visualize_graph(self, graph_id: str) -> str:
1116
+ """Visualize a graph.
1117
+
1118
+ Args:
1119
+ graph_id (str):
1120
+ The ID of the graph.
1121
+
1122
+ Returns:
1123
+ str: Filename of the generated html file
1124
+
1125
+ """
1126
+
1127
+ if not pyvis_installed:
1128
+ self.logger.warning("Cannot visualize graph. Python module pyvis not installed!")
1129
+ return None
1130
+
1131
+ graph = self.get_graph(graph_id=graph_id)
1132
+ graph_id = graph["id"]
1133
+ graph_name = graph["name"]
1134
+ net = Network(notebook=False, directed=True, height="1000px", width="100%", filter_menu=True, select_menu=True)
1135
+ net.heading = f"Aviator Studio graph: {graph_name}"
1136
+ nodes = self.get_graph_nodes_iterator(graph_id=graph_id)
1137
+ for node in nodes:
1138
+ node_id = node["id"]
1139
+ node_name = node["name"]
1140
+ node_attributes = node["attributes"]
1141
+ self._node_dictionary[(graph_id, node_id)] = node_name
1142
+ if node_attributes and "APISchema" in node_attributes:
1143
+ net.add_node(n_id=node_id, label=node_name, title=json.dumps(node, indent=2), color="green")
1144
+ else:
1145
+ net.add_node(n_id=node_id, label=node_name, title=json.dumps(node, indent=2))
1146
+ relationships = self.get_graph_node_relationships_iterator(
1147
+ graph_id=graph_id, node_id=node_id, relation_type="rules"
1148
+ )
1149
+ for relationship in relationships:
1150
+ for rule in relationship["rules"] or []:
1151
+ net.add_node(
1152
+ n_id=rule["id"], label=rule["name"], title=json.dumps(rule, indent=2), shape="box", color="red"
1153
+ )
1154
+ net.add_edge(source=node_id, to=rule["id"])
1155
+ relationships = self.get_graph_node_relationships_iterator(
1156
+ graph_id=graph_id, node_id=node_id, relation_type="prompts"
1157
+ )
1158
+ for relationship in relationships:
1159
+ for prompt in relationship["prompts"] or []:
1160
+ net.add_node(
1161
+ n_id=prompt["id"],
1162
+ label=prompt["name"],
1163
+ title=json.dumps(prompt, indent=2),
1164
+ shape="oval",
1165
+ color="green",
1166
+ )
1167
+ net.add_edge(source=node_id, to=prompt["id"])
1168
+ edges = self.get_graph_edges_iterator(graph_id=graph_id)
1169
+ for edge in edges:
1170
+ edge_source_id = edge["sourceId"]
1171
+ edge_target_id = edge["targetId"]
1172
+ net.add_edge(source=edge_source_id, to=edge_target_id)
1173
+
1174
+ html_file = "{}.html".format(graph_name)
1175
+ net.save_graph(html_file)
1176
+ self.logger.info("Graph visualization saved to -> %s", html_file)
1177
+
1178
+ return html_file
1179
+
1180
+ # end method definition
1181
+
1182
+ def get_model_types(self) -> list:
1183
+ """Get a list of all model types. Hardcoded.
1184
+
1185
+ Returns:
1186
+ list:
1187
+ Model types.
1188
+
1189
+ """
1190
+
1191
+ return ["tenants", "graphs", "nodes", "edges", "actions", "tools", "prompts", "rules", "klasses"]
1192
+
1193
+ # end method definition
1194
+
1195
+ def get_models(self, model_type: str) -> list | None:
1196
+ """Get all model details by type.
1197
+
1198
+ Args:
1199
+ model_type (str):
1200
+ The type of the model. Possible model types:
1201
+ * tenants
1202
+ * graphs
1203
+ * nodes
1204
+ * edges
1205
+ * actions
1206
+ * tools
1207
+ * prompts
1208
+ * rules
1209
+ * klasses
1210
+
1211
+ Returns:
1212
+ list | None:
1213
+ A list of all models of a given type.
1214
+
1215
+ """
1216
+
1217
+ request_url = self.config()["studioModelsUrl"] + "/" + model_type
1218
+ request_header = self.request_header(service_type="studio")
1219
+
1220
+ response = self.do_request(
1221
+ url=request_url,
1222
+ method="GET",
1223
+ headers=request_header,
1224
+ timeout=None,
1225
+ show_error=True,
1226
+ failure_message="Failed to get list of models!",
1227
+ )
1228
+
1229
+ if response is None:
1230
+ return None
1231
+
1232
+ return response.get("results", [])
1233
+
1234
+ # end method definition
1235
+
1236
+ def get_models_iterator(self, model_type: str) -> iter:
1237
+ """Get an iterator object that can be used to traverse models.
1238
+
1239
+ Returns:
1240
+ iter:
1241
+ A generator yielding one model per iteration.
1242
+ If the REST API fails, returns no value.
1243
+
1244
+ Yields:
1245
+ Iterator[iter]:
1246
+ One edge at a time.
1247
+
1248
+ """
1249
+
1250
+ models: list = self.get_models(model_type=model_type)
1251
+
1252
+ yield from models
1253
+
1254
+ # end method definition
1255
+
1256
+ def get_model(self, model_type: str, model_id: str) -> dict | None:
1257
+ """Get a specific model based on its type and ID.
1258
+
1259
+ Args:
1260
+ model_type (str):
1261
+ The type of the model. Possible model types:
1262
+ * tenants
1263
+ * graphs
1264
+ * nodes
1265
+ * edges
1266
+ * actions
1267
+ * tools
1268
+ * prompts
1269
+ * rules
1270
+ * klasses
1271
+ model_id (str):
1272
+ The ID of the model.
1273
+
1274
+ Returns:
1275
+ dict | None:
1276
+ The model data.
1277
+
1278
+ """
1279
+
1280
+ request_url = self.config()["studioModelsUrl"] + "/" + model_type + "/" + model_id
1281
+ request_header = self.request_header(service_type="studio")
1282
+
1283
+ return self.do_request(
1284
+ url=request_url,
1285
+ method="GET",
1286
+ headers=request_header,
1287
+ timeout=None,
1288
+ show_error=True,
1289
+ failure_message="Failed to get models with type -> '{}' and ID -> {}!".format(model_type, model_id),
1290
+ )
1291
+
1292
+ # end method definition
1293
+
1294
+ def get_model_by_type_and_name(self, model_type: str, name: str) -> dict | None:
1295
+ """Get model details by model type and name.
1296
+
1297
+ Args:
1298
+ model_type (str):
1299
+ The type of the model.
1300
+ name (str):
1301
+ The name of the model.
1302
+
1303
+ Returns:
1304
+ dict:
1305
+ Model details or None in case of an error.
1306
+
1307
+ """
1308
+
1309
+ models = self.get_models(model_type=model_type)
1310
+ if models:
1311
+ return next((model for model in models if model["name"] == name), None)
1312
+
1313
+ return None
1314
+
1315
+ # end method definition
1316
+
1317
+ # end method definition
1318
+
1319
+ def delete_model(self, model_type: str, model_id: str) -> dict | None:
1320
+ """Delete a model by type and id.
1321
+
1322
+ Args:
1323
+ model_type (str):
1324
+ The type of the model.
1325
+ model_id (str):
1326
+ The model name.
1327
+
1328
+ Returns:
1329
+ dict | None:
1330
+ Dict with the model details
1331
+
1332
+ """
1333
+
1334
+ self.logger.info("Deleting existing model -> '%s' (%s)", model_type, model_id)
1335
+
1336
+ request_header = self.request_header(service_type="studio")
1337
+ request_url = self.config()["studioModelsUrl"] + "/" + model_type + "/" + model_id
1338
+ return self.do_request(
1339
+ url=request_url,
1340
+ method="DELETE",
1341
+ headers=request_header,
1342
+ timeout=None,
1343
+ show_error=True,
1344
+ failure_message="Failed to delete model -> '{}' ({})!".format(model_type, model_id),
1345
+ )
1346
+
1347
+ # end method definition
1348
+
1349
+ def update_model(self, model_type: str, model_id: str, request_body: dict) -> dict | None:
1350
+ """Update a model with a given type and ID.
1351
+
1352
+ Args:
1353
+ model_type (str):
1354
+ The type of the model.
1355
+ model_id (str):
1356
+ The ID of the model.
1357
+ request_body (dict):
1358
+ Data to update the model.
1359
+
1360
+ Returns:
1361
+ dict | None:
1362
+ Dict with the model details or None in case of an error.
1363
+
1364
+ """
1365
+
1366
+ self.logger.info("Updating existing model -> '%s' (%s)", model_type, model_id)
1367
+
1368
+ request_header = self.request_header(service_type="studio")
1369
+ request_url = self.config()["studioModelsUrl"] + "/" + model_type + "/" + model_id
1370
+ return self.do_request(
1371
+ url=request_url,
1372
+ method="PUT",
1373
+ headers=request_header,
1374
+ json_data=request_body,
1375
+ timeout=None,
1376
+ show_error=True,
1377
+ failure_message="Failed to update model -> '{}' ({}).".format(model_type, model_id),
1378
+ )
1379
+
1380
+ # end method definition
1381
+
1382
+ def get_tools(self) -> list | None:
1383
+ """Get all tools.
1384
+
1385
+ Returns:
1386
+ list:
1387
+ A list of all tools.
1388
+
1389
+ Example:
1390
+ [
1391
+ {
1392
+ 'id': '305353d8-7391-497e-9f3f-8a1fe11ceac0',
1393
+ 'attributes': {
1394
+ 'conditions': [{'messageLength': 2}],
1395
+ 'maxHistory': -1,
1396
+ 'showInContext': True
1397
+ },
1398
+ 'klassId': 'b033b1d5-8182-4883-ba8a-16f0048b01b0',
1399
+ 'name': 'rephrase_search',
1400
+ 'description': 'Used for creating a standalone search query for retrieving documents. Input should be a dependent user message',
1401
+ 'discriminator': 0,
1402
+ 'createdAt': '2025-07-01T22:57:13.135Z',
1403
+ 'updatedAt': '2025-07-01T22:57:13.135Z',
1404
+ 'version': 0,
1405
+ 'status': 0,
1406
+ 'graphId': '93897862-d999-4fe0-82fc-3f9d03474545'
1407
+ }
1408
+ ]
1409
+
1410
+ """
1411
+
1412
+ return self.get_models(model_type="tools")
1413
+
1414
+ # end method definition
1415
+
1416
+ def get_tools_iterator(self) -> iter:
1417
+ """Get an iterator object that can be used to traverse tools.
1418
+
1419
+ Returns:
1420
+ iter:
1421
+ A generator yielding one tool per iteration.
1422
+ If the REST API fails, returns no value.
1423
+
1424
+ Yields:
1425
+ Iterator[iter]:
1426
+ One tool at a time.
1427
+
1428
+ """
1429
+
1430
+ tools: list = self.get_models(model_type="tools")
1431
+
1432
+ yield from tools
1433
+
1434
+ # end method definition
1435
+
1436
+ def get_tool(self, tool_id: str) -> dict | None:
1437
+ r"""Get a tool by its ID.
1438
+
1439
+ Args:
1440
+ tool_id (str):
1441
+ The ID of the tool.
1442
+
1443
+ Returns:
1444
+ dict | None:
1445
+ Tool data or none in case of an error.
1446
+
1447
+ Example:
1448
+ {
1449
+ 'id': '49e230ef-024a-4dd8-beeb-70210cec0564',
1450
+ 'attributes': {'APISchema': {...}},
1451
+ 'klassId': '275843d9-c39f-43e2-ad00-b02ac42b5dd6',
1452
+ 'name': 'otcm_workspace_agent_lookup_workspace',
1453
+ 'description': 'Lookup a workspace based on its type and a value of one of the workspace attributes.\n\nUse this tool if the workspace name is _not_ specified but the user asks for a specific\nworkspace attribute value like cities, products, or other attributes.\n\nReturn the workspace data if it is found. If it is not found confirm with the user if the workspace should be created or not.\nIf it should be created call the tool: otcm_workspace_agent_create_workspace',
1454
+ 'discriminator': 0,
1455
+ 'createdAt': '2025-07-01T22:57:13.535Z',
1456
+ 'updatedAt': '2025-07-01T22:57:13.535Z',
1457
+ 'version': 0,
1458
+ 'status': 0,
1459
+ 'graphId': '93897862-d999-4fe0-82fc-3f9d03474545'
1460
+ }
1461
+
1462
+ """
1463
+
1464
+ tool = self.get_model(model_type="tools", model_id=tool_id)
1465
+
1466
+ return tool
1467
+
1468
+ # end method definition
1469
+
1470
+ def register_tool(
1471
+ self,
1472
+ request_body: dict,
1473
+ ) -> dict:
1474
+ r"""Register a Tool in Content Aviator.
1475
+
1476
+ Requests are meant to be called as a service user. This would involve passing a service user's access token
1477
+ (token from a particular OAuth confidential client, using client credentials grant).
1478
+
1479
+ Args:
1480
+ request_body (dict):
1481
+ Body for the request. Needs to look like:
1482
+ example:
1483
+ {
1484
+ "name": "tool name",
1485
+ "description": "description of the tool",
1486
+ "APISchema": {} # dict of the APISchema, compliant with openapi 3.0.0
1487
+ "requestTemplate": {
1488
+ "data": {
1489
+ "context": {
1490
+ "where": "memory.input.where",
1491
+ "query": "memory.input.query"
1492
+ }
1493
+ },
1494
+ },
1495
+ "responseTemplate": {},
1496
+ "agents": ["retrieverAgent"],
1497
+ }
1498
+
1499
+ Returns:
1500
+ dict: Tool details or None in case of an error.
1501
+
1502
+ Example:
1503
+ {
1504
+ 'id': '27ce608f-41ea-4128-aff9-91facc66bcfa',
1505
+ 'attributes': {
1506
+ 'APISchema': {
1507
+ 'openapi': '3.0.0',
1508
+ 'info': {
1509
+ 'title': 'otcm_workspace_agent_find_workspace',
1510
+ 'version': '0.0.0'
1511
+ },
1512
+ 'servers': [{'url': 'http://customizer:8000'}],
1513
+ 'paths': {
1514
+ '/agents/otcm_workspace_agent/find_workspace': {
1515
+ 'post': {
1516
+ 'tags': [...],
1517
+ 'summary': 'Find the markdown link to a workspace by workspace name and workspace type and display the link.',
1518
+ 'description': 'Find a workspace by workspace name and workspace type.\n\nThe returned workspace is an OTCS workspace object. Show the markdown link in the chat response, sothat the user can click on it.',
1519
+ 'operationId': 'otcm_workspace_agent_find_workspace_agents_otcm_workspace_agent_find_workspace_post',
1520
+ 'requestBody': {...},
1521
+ 'responses': {
1522
+ '200': {
1523
+ 'description': 'Workspace found',
1524
+ 'content': {
1525
+ 'application/json': {
1526
+ 'schema': {'$ref': '#/components/schemas/WorkspaceModel'}
1527
+ }
1528
+ }
1529
+ },
1530
+ '403': {
1531
+ 'description': 'Invalid credentials'
1532
+ },
1533
+ '404': {
1534
+ 'description': 'Workspace not found'
1535
+ },
1536
+ '422': {
1537
+ 'description': 'Validation Error',
1538
+ 'content': {...}
1539
+ }
1540
+ },
1541
+ 'security': [...]
1542
+ }
1543
+ }
1544
+ },
1545
+ 'components': {
1546
+ 'schemas': {
1547
+ 'Body_otcm_workspace_agent_find_workspace_agents_otcm_workspace_agent_find_workspace_post': {
1548
+ 'properties': {...},
1549
+ 'type': 'object',
1550
+ 'required': [...],
1551
+ 'title': 'Body_otcm_workspace_agent_find_workspace_agents_otcm_workspace_agent_find_workspace_post'
1552
+ },
1553
+ 'Context': {
1554
+ 'properties': {...},
1555
+ 'type': 'object',
1556
+ 'required': [...],
1557
+ 'title': 'Context',
1558
+ 'description': 'Define Model that is used to provide static context information for tools.'
1559
+ },
1560
+ 'HTTPValidationError': {
1561
+ 'properties': {...},
1562
+ 'type': 'object',
1563
+ 'title': 'HTTPValidationError'
1564
+ },
1565
+ 'ValidationError': {
1566
+ 'properties': {...},
1567
+ 'type': 'object',
1568
+ 'required': [...],
1569
+ 'title': 'ValidationError'
1570
+ },
1571
+ 'WorkspaceModel': {
1572
+ 'properties': {...},
1573
+ 'type': 'object',
1574
+ 'title': 'WorkspaceModel',
1575
+ 'description': 'Defines Model for describing workspaces in OTCM (Opentext Content Management).\n\nTo display an instance of this model, please display the link.'
1576
+ }
1577
+ },
1578
+ 'securitySchemes': {...}
1579
+ }
1580
+ },
1581
+ 'showInContext': True,
1582
+ 'responseFormat': 'content_and_artifact',
1583
+ 'requestTemplate': {
1584
+ 'data': {
1585
+ 'context': {
1586
+ 'query': 'memory.input.query',
1587
+ 'where': 'memory.input.where'
1588
+ }
1589
+ }
1590
+ },
1591
+ 'responseTemplate': {}
1592
+ },
1593
+ 'klassId': '1d8dbd52-5dee-4645-841d-2889fac74b13',
1594
+ 'name': 'otcm_workspace_agent_find_workspace',
1595
+ 'description': 'Find a workspace by workspace name and workspace type.\n\nThe returned workspace is an OTCS workspace object. Show the markdown link in the chat response, sothat the user can click on it.',
1596
+ 'discriminator': 0,
1597
+ 'createdAt': '2025-07-09T12:35:26.464Z',
1598
+ 'updatedAt': '2025-07-09T12:35:26.464Z',
1599
+ 'version': 0,
1600
+ 'status': 0,
1601
+ 'graphId': '440aae89-8942-4bb0-8107-291227f8ad92'
1602
+ }
1603
+
1604
+ """
1605
+
1606
+ # Validations:
1607
+ for key in ["name", "description", "APISchema", "agents"]:
1608
+ if key not in request_body:
1609
+ self.logger.error("%s is missing in provided request body for tool registration!", key)
1610
+ return None
1611
+
1612
+ # Check if the tool already exists and need to be updated only:
1613
+ self.logger.debug("Check if tool -> '%s' already exists...", request_body["name"])
1614
+ model = self.get_model_by_type_and_name(model_type="tools", name=request_body["name"])
1615
+ if model:
1616
+ self.logger.info("Updating existing tool -> '%s'...", request_body["name"])
1617
+
1618
+ update_body = {
1619
+ "description": request_body["description"],
1620
+ "attributes": {**model.get("attributes", {}), "APISchema": request_body["APISchema"]},
1621
+ }
1622
+ response = self.update_model(model_type="tools", model_id=model["id"], request_body=update_body)
1623
+ if not response:
1624
+ self.logger.error("Failed to update model -> '%s' (%s)", request_body["name"], model["id"])
1625
+ else:
1626
+ self.logger.info("Registering new tool -> '%s'...", request_body["name"])
1627
+ request_header = self.request_header(service_type="studio")
1628
+ request_url = self.config()["studioToolsUrl"]
1629
+ response = self.do_request(
1630
+ url=request_url,
1631
+ method="POST",
1632
+ headers=request_header,
1633
+ json_data=request_body,
1634
+ timeout=None,
1635
+ show_error=True,
1636
+ failure_message="Failed to register tool -> '{}'!".format(request_body["name"]),
1637
+ )
1638
+
1639
+ return response
1640
+
1641
+ # end method definition
1642
+
1643
+ def get_rules(self) -> list | None:
1644
+ r"""Get all rules.
1645
+
1646
+ Returns:
1647
+ list:
1648
+ A list of all rules.
1649
+
1650
+ Example:
1651
+ [
1652
+ {
1653
+ 'id': '4d089d1e-205d-4ff4-8128-7c2a83bd2462',
1654
+ 'name': 'evaluateLastToolResult',
1655
+ 'description': 'Equivalent of previous "toolResult" check. Evaluates the result of last tool executed and compares it with a given string. Returns `true` or `false`. This is the equivalent of \n ```const lastTool = memory.response.called[memory.response.called.length - 1];\nreturn lastTool?.result === (andConditions[condition]);```',
1656
+ 'createdAt': '2025-07-01T16:51:56.703Z',
1657
+ 'updatedAt': '2025-07-01T16:51:56.703Z',
1658
+ 'rule': {
1659
+ '===': [
1660
+ '<<nodeResult>>',
1661
+ {
1662
+ 'get': [
1663
+ {...}, 'result'
1664
+ ]
1665
+ }
1666
+ ]
1667
+ },
1668
+ 'status': 0,
1669
+ 'tenantId': '05f43f12-5865-46cd-8954-1af3dc575e88'
1670
+ }
1671
+ ]
1672
+
1673
+ """
1674
+
1675
+ request_url = self.config()["studioRulesUrl"]
1676
+ request_header = self.request_header(service_type="studio")
1677
+
1678
+ response = self.do_request(
1679
+ url=request_url,
1680
+ method="GET",
1681
+ headers=request_header,
1682
+ timeout=None,
1683
+ show_error=True,
1684
+ failure_message="Failed get rules!",
1685
+ )
1686
+
1687
+ if response is None:
1688
+ return None
1689
+
1690
+ return response.get("results", [])
1691
+
1692
+ # end method definition
1693
+
1694
+ def get_rules_iterator(self) -> iter:
1695
+ """Get an iterator object that can be used to traverse rules.
1696
+
1697
+ Returns:
1698
+ iter:
1699
+ A generator yielding one rule per iteration.
1700
+ If the REST API fails, returns no value.
1701
+
1702
+ Yields:
1703
+ Iterator[iter]:
1704
+ One rule at a time.
1705
+
1706
+ """
1707
+
1708
+ rules: list = self.get_rules()
1709
+
1710
+ yield from rules
1711
+
1712
+ # end method definition
1713
+
1714
+ def get_rule(self, rule_id: str) -> dict | None:
1715
+ r"""Get a rule by its ID.
1716
+
1717
+ Args:
1718
+ rule_id (str):
1719
+ The ID of the rule.
1720
+
1721
+ Returns:
1722
+ dict | None:
1723
+ Rule data or none in case of an error.
1724
+
1725
+ Example:
1726
+ {
1727
+ 'id': '4d089d1e-205d-4ff4-8128-7c2a83bd2462',
1728
+ 'name': 'evaluateLastToolResult',
1729
+ 'description': 'Equivalent of previous "toolResult" check. Evaluates the result of last tool executed and compares it with a given string. Returns `true` or `false`. This is the equivalent of \n ```const lastTool = memory.response.called[memory.response.called.length - 1];\nreturn lastTool?.result === (andConditions[condition]);```',
1730
+ 'createdAt': '2025-07-01T16:51:56.703Z',
1731
+ 'updatedAt': '2025-07-01T16:51:56.703Z',
1732
+ 'rule': {
1733
+ '===': [
1734
+ '<<nodeResult>>',
1735
+ {
1736
+ 'get': [
1737
+ {...}, 'result'
1738
+ ]
1739
+ }
1740
+ ]
1741
+ },
1742
+ 'status': 0,
1743
+ 'tenantId': '05f43f12-5865-46cd-8954-1af3dc575e88'
1744
+ }
1745
+
1746
+ """
1747
+
1748
+ rule = self.get_model(model_type="rules", model_id=rule_id)
1749
+
1750
+ return rule
1751
+
1752
+ # end method definition
1753
+
1754
+ def get_prompts(self) -> list | None:
1755
+ r"""Get all prompts.
1756
+
1757
+ Returns:
1758
+ list:
1759
+ A list of all prompts.
1760
+
1761
+ Example:
1762
+ [
1763
+ {
1764
+ 'id': '1aeb9fa1-cb26-4b07-a736-20d25a4ab939',
1765
+ 'name': 'general_system',
1766
+ 'template': "Your name is Aviator and you are a friendly chatbot assisting users with their queries about documents. The DOCUMENTS contains text of tool calls, arguments and their responses in the following format:\n Tool '[test]' called with arguments '[args]' and returned: [tool response]. \n When responding: \n 1. If one or more tool responses are present, answer directly using the information in the tool response. Do not refer to the tool call or tool response explicitly. \n 2. If no tool response is present, reply that you do not know. \n 3. If the information is out of the scope of the document or you are unsure of the answer, reply that you do not know. If the user explicitly requests to provide, show, display, generate a specific output format like a table, a list or a code block, please prioritize that format, when providing an answer. \nDOCUMENTS: {context}",
1767
+ 'type': 0,
1768
+ 'createdAt': '2025-07-01T16:51:56.703Z',
1769
+ 'updatedAt': '2025-07-01T16:51:56.703Z',
1770
+ 'version': None,
1771
+ 'status': 0,
1772
+ 'tenantId': '05f43f12-5865-46cd-8954-1af3dc575e88',
1773
+ 'attributes': None,
1774
+ 'description': None
1775
+ },
1776
+ ...
1777
+ ]
1778
+
1779
+ """
1780
+
1781
+ return self.get_models(model_type="prompts")
1782
+
1783
+ # end method definition
1784
+
1785
+ def get_prompts_iterator(self) -> iter:
1786
+ """Get an iterator object that can be used to traverse prompts.
1787
+
1788
+ Returns:
1789
+ iter:
1790
+ A generator yielding one prompt per iteration.
1791
+ If the REST API fails, returns no value.
1792
+
1793
+ Yields:
1794
+ Iterator[iter]:
1795
+ One tool at a time.
1796
+
1797
+ """
1798
+
1799
+ prompts: list = self.get_models(model_type="prompts")
1800
+
1801
+ yield from prompts
1802
+
1803
+ # end method definition
1804
+
1805
+ def get_prompt(self, prompt_id: str) -> dict | None:
1806
+ r"""Get a rule by its ID.
1807
+
1808
+ Args:
1809
+ prompt_id (str):
1810
+ The ID of the prompt.
1811
+
1812
+ Returns:
1813
+ dict | None:
1814
+ Prompt data or none in case of an error.
1815
+
1816
+ Example:
1817
+ {
1818
+ 'id': '1aeb9fa1-cb26-4b07-a736-20d25a4ab939',
1819
+ 'name': 'general_system',
1820
+ 'template': "Your name is Aviator and you are a friendly chatbot assisting users with their queries about documents. The DOCUMENTS contains text of tool calls, arguments and their responses in the following format:\n Tool '[test]' called with arguments '[args]' and returned: [tool response]. \n When responding: \n 1. If one or more tool responses are present, answer directly using the information in the tool response. Do not refer to the tool call or tool response explicitly. \n 2. If no tool response is present, reply that you do not know. \n 3. If the information is out of the scope of the document or you are unsure of the answer, reply that you do not know. If the user explicitly requests to provide, show, display, generate a specific output format like a table, a list or a code block, please prioritize that format, when providing an answer. \nDOCUMENTS: {context}",
1821
+ 'type': 0,
1822
+ 'createdAt': '2025-07-01T16:51:56.703Z',
1823
+ 'updatedAt': '2025-07-01T16:51:56.703Z',
1824
+ 'version': None,
1825
+ 'status': 0,
1826
+ 'tenantId': '05f43f12-5865-46cd-8954-1af3dc575e88',
1827
+ 'attributes': None,
1828
+ 'description': None
1829
+ },
1830
+
1831
+ """
1832
+
1833
+ prompt = self.get_model(model_type="prompts", model_id=prompt_id)
1834
+
1835
+ return prompt
1836
+
1837
+ # end method definition
1838
+
1839
+ def get_actions(self) -> list | None:
1840
+ r"""Get all actions.
1841
+
1842
+ Returns:
1843
+ list:
1844
+ A list of all actions.
1845
+
1846
+ Example:
1847
+ [
1848
+ {
1849
+ 'id': '98dec337-8284-4d30-8a6d-0da099aa025a',
1850
+ 'attributes': {'studio': 'routerAgent'},
1851
+ 'klassId': '3d2d2500-483a-4af6-9103-79da80994852',
1852
+ 'name': 'decision',
1853
+ 'description': None,
1854
+ 'discriminator': 1,
1855
+ 'createdAt': '2025-07-02T06:45:04.117Z',
1856
+ 'updatedAt': '2025-07-02T06:45:04.117Z',
1857
+ 'version': 0,
1858
+ 'status': 0,
1859
+ 'graphId': '02a6ae86-dbf5-4007-ad66-090a145bc81a'
1860
+ },
1861
+ ...
1862
+ ]
1863
+
1864
+ """
1865
+
1866
+ return self.get_models(model_type="actions")
1867
+
1868
+ # end method definition
1869
+
1870
+ def get_actions_iterator(self) -> iter:
1871
+ """Get an iterator object that can be used to traverse actions.
1872
+
1873
+ Returns:
1874
+ iter:
1875
+ A generator yielding one action per iteration.
1876
+ If the REST API fails, returns no value.
1877
+
1878
+ Yields:
1879
+ Iterator[iter]:
1880
+ One action at a time.
1881
+
1882
+ """
1883
+
1884
+ actions: list = self.get_models(model_type="actions")
1885
+
1886
+ yield from actions
1887
+
1888
+ # end method definition
1889
+
1890
+ def get_action(self, action_id: str) -> dict | None:
1891
+ r"""Get a action by its ID.
1892
+
1893
+ Args:
1894
+ action_id (str):
1895
+ The ID of the action.
1896
+
1897
+ Returns:
1898
+ dict | None:
1899
+ Action data or none in case of an error.
1900
+
1901
+ Example:
1902
+ {
1903
+ 'id': '98dec337-8284-4d30-8a6d-0da099aa025a',
1904
+ 'attributes': {'studio': 'routerAgent'},
1905
+ 'klassId': '3d2d2500-483a-4af6-9103-79da80994852',
1906
+ 'name': 'decision',
1907
+ 'description': None,
1908
+ 'discriminator': 1,
1909
+ 'createdAt': '2025-07-02T06:45:04.117Z',
1910
+ 'updatedAt': '2025-07-02T06:45:04.117Z',
1911
+ 'version': 0,
1912
+ 'status': 0,
1913
+ 'graphId': '02a6ae86-dbf5-4007-ad66-090a145bc81a'
1914
+ },
1915
+
1916
+ """
1917
+
1918
+ action = self.get_model(model_type="actions", model_id=action_id)
1919
+
1920
+ return action
1921
+
1922
+ # end method definition
1923
+
1924
+ def get_klasses(self) -> list | None:
1925
+ r"""Get all klasses.
1926
+
1927
+ Returns:
1928
+ list:
1929
+ A list of all klasses.
1930
+
1931
+ Example:
1932
+ [
1933
+ {
1934
+ 'id': '20cfe232-cf03-4b77-a4e6-bc9339371a37',
1935
+ 'name': 'RephraseSearch',
1936
+ 'tenantId': 'eb6fee1e-da08-4046-9867-e96ac0ec5bdf',
1937
+ 'path': '../langchain_tools/tools/rephraseSearch',
1938
+ 'type': 8,
1939
+ 'createdAt': '2025-07-02T06:45:04.099Z',
1940
+ 'updatedAt': '2025-07-02T06:45:04.099Z',
1941
+ 'description': None
1942
+ },
1943
+ ...
1944
+ ]
1945
+
1946
+ """
1947
+
1948
+ return self.get_models(model_type="klasses")
1949
+
1950
+ # end method definition
1951
+
1952
+ def get_klasses_iterator(self) -> iter:
1953
+ """Get an iterator object that can be used to traverse klasses.
1954
+
1955
+ Returns:
1956
+ iter:
1957
+ A generator yielding one klass per iteration.
1958
+ If the REST API fails, returns no value.
1959
+
1960
+ Yields:
1961
+ Iterator[iter]:
1962
+ One klass at a time.
1963
+
1964
+ """
1965
+
1966
+ klasses: list = self.get_models(model_type="klasses")
1967
+
1968
+ yield from klasses
1969
+
1970
+ # end method definition
1971
+
1972
+ def get_klass(self, klass_id: str) -> dict | None:
1973
+ r"""Get a klass by its ID.
1974
+
1975
+ Args:
1976
+ klass_id (str):
1977
+ The ID of the klass.
1978
+
1979
+ Returns:
1980
+ dict | None:
1981
+ Klass data or none in case of an error.
1982
+
1983
+ Example:
1984
+ {
1985
+ 'id': '20cfe232-cf03-4b77-a4e6-bc9339371a37',
1986
+ 'name': 'RephraseSearch',
1987
+ 'tenantId': 'eb6fee1e-da08-4046-9867-e96ac0ec5bdf',
1988
+ 'path': '../langchain_tools/tools/rephraseSearch',
1989
+ 'type': 8,
1990
+ 'createdAt': '2025-07-02T06:45:04.099Z',
1991
+ 'updatedAt': '2025-07-02T06:45:04.099Z',
1992
+ 'description': None
1993
+ }
1994
+
1995
+ """
1996
+
1997
+ klass = self.get_model(model_type="klasses", model_id=klass_id)
1998
+
1999
+ return klass
2000
+
2001
+ # end method definition
2002
+
2003
+ def get_graph_node_relationships(self, graph_id: str, node_id: str, relation_type: str) -> list | None:
2004
+ """Get all relations to prompts or rules for a graph node.
2005
+
2006
+ Args:
2007
+ graph_id (str):
2008
+ The ID of the Graph to retrieve the relationships for.
2009
+ node_id (str):
2010
+ The ID of the Graph node to retrieve the relationships for.
2011
+ relation_type (str):
2012
+ This can either be "prompts" or "rules".
2013
+
2014
+ Returns:
2015
+ list | None:
2016
+ A list of relationships for the node.
2017
+
2018
+ Example:
2019
+ [
2020
+ ]
2021
+
2022
+ """
2023
+
2024
+ request_url = self.config()["studioGraphsUrl"] + "/" + graph_id + "/nodes/" + node_id + "/" + relation_type
2025
+ request_header = self.request_header(service_type="studio")
2026
+
2027
+ response = self.do_request(
2028
+ url=request_url,
2029
+ method="GET",
2030
+ headers=request_header,
2031
+ timeout=None,
2032
+ show_error=True,
2033
+ failure_message="Failed get list of graph node relationships!",
2034
+ )
2035
+
2036
+ if response is None:
2037
+ return None
2038
+
2039
+ return response.get("results", [])
2040
+
2041
+ # end method definition
2042
+
2043
+ def get_graph_node_relationships_iterator(
2044
+ self, graph_id: str, node_id: str, relation_type: str | list = "prompts"
2045
+ ) -> iter:
2046
+ """Get an iterator object that can be used to traverse prompts.
2047
+
2048
+ Args:
2049
+ graph_id (str):
2050
+ The ID of the Graph to retrieve the relationships for.
2051
+ node_id (str):
2052
+ The ID of the Graph node to retrieve the relationships for.
2053
+ relation_type (str):
2054
+ This can either be "prompts" or "rules".
2055
+
2056
+ Returns:
2057
+ iter:
2058
+ A generator yielding one relationship per iteration.
2059
+ If the REST API fails, returns no value.
2060
+
2061
+ Yields:
2062
+ Iterator[iter]:
2063
+ One relationship at a time.
2064
+
2065
+ """
2066
+
2067
+ relationships: list = self.get_graph_node_relationships(
2068
+ graph_id=graph_id, node_id=node_id, relation_type=relation_type
2069
+ )
2070
+ if not relationships:
2071
+ return
2072
+
2073
+ yield from relationships
2074
+
2075
+ # end method definition