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.

Files changed (52) hide show
  1. pyxecm/avts.py +4 -4
  2. pyxecm/coreshare.py +14 -15
  3. pyxecm/helper/data.py +2 -1
  4. pyxecm/helper/web.py +11 -11
  5. pyxecm/helper/xml.py +41 -10
  6. pyxecm/otac.py +1 -1
  7. pyxecm/otawp.py +19 -19
  8. pyxecm/otca.py +870 -67
  9. pyxecm/otcs.py +1567 -280
  10. pyxecm/otds.py +332 -153
  11. pyxecm/otkd.py +4 -4
  12. pyxecm/otmm.py +1 -1
  13. pyxecm/otpd.py +246 -30
  14. {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/METADATA +2 -1
  15. pyxecm-3.1.0.dist-info/RECORD +82 -0
  16. pyxecm_api/app.py +45 -35
  17. pyxecm_api/auth/functions.py +2 -2
  18. pyxecm_api/auth/router.py +2 -3
  19. pyxecm_api/common/functions.py +164 -12
  20. pyxecm_api/settings.py +0 -8
  21. pyxecm_api/terminal/router.py +1 -1
  22. pyxecm_api/v1_csai/router.py +33 -18
  23. pyxecm_customizer/browser_automation.py +98 -48
  24. pyxecm_customizer/customizer.py +43 -25
  25. pyxecm_customizer/guidewire.py +422 -8
  26. pyxecm_customizer/k8s.py +23 -27
  27. pyxecm_customizer/knowledge_graph.py +501 -20
  28. pyxecm_customizer/m365.py +45 -44
  29. pyxecm_customizer/payload.py +1684 -1159
  30. pyxecm_customizer/payload_list.py +3 -0
  31. pyxecm_customizer/salesforce.py +122 -79
  32. pyxecm_customizer/servicenow.py +27 -7
  33. pyxecm_customizer/settings.py +3 -1
  34. pyxecm_customizer/successfactors.py +2 -2
  35. pyxecm_customizer/translate.py +1 -1
  36. pyxecm-3.0.1.dist-info/RECORD +0 -96
  37. pyxecm_api/agents/__init__.py +0 -7
  38. pyxecm_api/agents/app.py +0 -13
  39. pyxecm_api/agents/functions.py +0 -119
  40. pyxecm_api/agents/models.py +0 -10
  41. pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
  42. pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
  43. pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
  44. pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
  45. pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
  46. pyxecm_api/agents/otcm_user_agent/models.py +0 -20
  47. pyxecm_api/agents/otcm_user_agent/router.py +0 -65
  48. pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
  49. pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
  50. pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
  51. {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/WHEEL +0 -0
  52. {pyxecm-3.0.1.dist-info → pyxecm-3.1.0.dist-info}/entry_points.txt +0 -0
@@ -2,11 +2,15 @@
2
2
 
3
3
  import logging
4
4
  import os
5
+ import time
6
+ from datetime import UTC, datetime
5
7
  from typing import Annotated
6
8
 
7
9
  from fastapi import Depends
10
+ from pyxecm.otca import OTCA
8
11
  from pyxecm.otcs import OTCS
9
12
  from pyxecm_customizer import K8s, PayloadList, Settings
13
+ from pyxecm_customizer.knowledge_graph import KnowledgeGraph
10
14
 
11
15
  from pyxecm_api.auth.functions import get_otcsticket
12
16
  from pyxecm_api.settings import CustomizerAPISettings, api_settings
@@ -18,6 +22,100 @@ LOGS_LOCK = {}
18
22
  # Initialize the globel Payloadlist object
19
23
  PAYLOAD_LIST = PayloadList(logger=logger)
20
24
 
25
+ # This object is initialized in the build_graph() function below.
26
+ KNOWLEDGEGRAPH_OBJECT: KnowledgeGraph = None
27
+
28
+ # The following ontology is fed into the knowledge graph tool description.
29
+ # This is currently hard-coded. Ideally this should be derived from OTCM
30
+ # or provided via a payload file:
31
+
32
+ KNOWLEDGEGRAPH_ONTOLOGY = {
33
+ ("Vendor", "Material", "child"): ["offers", "supplies", "provides"],
34
+ ("Vendor", "Purchase Order", "child"): ["supplies", "provides"],
35
+ ("Vendor", "Purchase Contract", "child"): ["signs", "owns"],
36
+ ("Material", "Vendor", "parent"): ["is supplied by"],
37
+ ("Purchase Order", "Material", "child"): ["includes", "is part of"],
38
+ ("Customer", "Sales Order", "child"): ["has ordered"],
39
+ ("Customer", "Sales Contract", "child"): ["signs", "owns"],
40
+ ("Sales Order", "Customer", "parent"): ["belongs to", "is initiated by"],
41
+ ("Sales Order", "Material", "child"): ["includes", "consists of"],
42
+ ("Sales Order", "Delivery", "child"): ["triggers", "is followed by"],
43
+ ("Sales Order", "Production Order", "child"): ["triggers", "is followed by"],
44
+ ("Sales Contract", "Material", "child"): ["includes", "consists of"],
45
+ ("Production Order", "Material", "child"): ["includes", "consists of"],
46
+ ("Production Order", "Delivery", "child"): ["triggers", "is followed by"],
47
+ ("Production Order", "Goods Movement", "child"): ["triggers", "is followed by"],
48
+ ("Delivery", "Goods Movement", "child"): ["triggers", "is followed by"],
49
+ ("Delivery", "Material", "child"): ["triggers", "is followed by"],
50
+ }
51
+
52
+
53
+ ### Functions
54
+
55
+
56
+ def get_ontology() -> dict:
57
+ """Get the ontology for the knowledge graph.
58
+
59
+ Returns:
60
+ dict: The ontology as a dictionary.
61
+
62
+ """
63
+
64
+ return KNOWLEDGEGRAPH_ONTOLOGY
65
+
66
+
67
+ def get_knowledgegraph_object() -> KnowledgeGraph:
68
+ """Get the Knowledge Graph object."""
69
+
70
+ global KNOWLEDGEGRAPH_OBJECT # noqa: PLW0603
71
+
72
+ if KNOWLEDGEGRAPH_OBJECT is None:
73
+ KNOWLEDGEGRAPH_OBJECT = KnowledgeGraph(otcs_object=get_otcs_object(), ontology=KNOWLEDGEGRAPH_ONTOLOGY)
74
+
75
+ return KNOWLEDGEGRAPH_OBJECT
76
+
77
+
78
+ def build_graph() -> None:
79
+ """Build the knowledge Graph. And keep it updated every hour."""
80
+
81
+ def build() -> None:
82
+ """Build the knowledge graph once."""
83
+
84
+ logger.info("Starting knowledge graph build...")
85
+ start_time = datetime.now(UTC)
86
+ result = get_knowledgegraph_object().build_graph(
87
+ workspace_type_exclusions=None,
88
+ workspace_type_inclusions=[
89
+ "Vendor",
90
+ "Purchase Contract",
91
+ "Purchase Order",
92
+ "Material",
93
+ "Customer",
94
+ "Sales Order",
95
+ "Sales Contract",
96
+ "Delivery",
97
+ "Goods Movement",
98
+ ],
99
+ workers=20, # for multi-threaded traversal
100
+ filter_at_traversal=True, # also filter for workspace types if following relationships
101
+ relationship_types=["child"], # only go from parent to child
102
+ strategy="BFS", # Breadth-First-Search
103
+ metadata=True, # don't include workspace metadata
104
+ )
105
+ end_time = datetime.now(UTC)
106
+ logger.info(
107
+ "Knowledge graph completed in %s. Processed %d workspace nodes and traversed %d workspace relationships.",
108
+ str(end_time - start_time),
109
+ result["processed"],
110
+ result["traversed"],
111
+ )
112
+
113
+ # Endless loop to build knowledge graph and update it every hour:
114
+ while True:
115
+ build()
116
+ logger.info("Waiting for 1 hour before rebuilding the knowledge graph...")
117
+ time.sleep(3600)
118
+
21
119
 
22
120
  def get_k8s_object() -> K8s:
23
121
  """Get an instance of a K8s object.
@@ -31,12 +129,14 @@ def get_k8s_object() -> K8s:
31
129
 
32
130
 
33
131
  def get_otcs_object() -> OTCS:
34
- """Get an instance of a K8s object.
132
+ """Get an instance of a Content Server (OTCS) object.
35
133
 
36
134
  Returns:
37
- K8s: Return a K8s object
135
+ OTCS:
136
+ Return a new OTCS object.
38
137
 
39
138
  """
139
+
40
140
  settings = Settings()
41
141
 
42
142
  otcs = OTCS(
@@ -52,27 +152,34 @@ def get_otcs_object() -> OTCS:
52
152
  support_path=settings.otcs.support_path,
53
153
  download_dir=settings.otcs.download_dir,
54
154
  feme_uri=settings.otcs.feme_uri,
55
- logger=logger.getChild("otcs"),
155
+ logger=logger,
56
156
  )
57
157
 
158
+ # Authenticate at Content Server:
58
159
  otcs.authenticate()
59
160
 
60
161
  return otcs
61
162
 
62
163
 
63
- def get_otcs_object_from_otcsticket(otcsticket: Annotated[str, Depends(get_otcsticket)]) -> OTCS:
64
- """Get an instance of a K8s object.
164
+ def get_otcs_object_from_otcsticket(otcs_ticket: Annotated[str, Depends(get_otcsticket)]) -> OTCS:
165
+ """Get an instance of a Content Server (OTCS) object.
65
166
 
66
167
  Returns:
67
- K8s: Return a K8s object
168
+ OTCS:
169
+ Return an OTCS object.
68
170
 
69
171
  """
172
+
70
173
  settings = Settings()
71
174
 
175
+ # Create an OTCS object without defining the username and password:
72
176
  otcs = OTCS(
73
- protocol=settings.otcs.url_backend.scheme,
74
- hostname=settings.otcs.url_backend.host,
75
- port=settings.otcs.url_backend.port,
177
+ # protocol=settings.otcs.url_backend.scheme,
178
+ # hostname=settings.otcs.url_backend.host,
179
+ # port=settings.otcs.url_backend.port,
180
+ protocol=settings.otcs.url_frontend.scheme,
181
+ hostname=settings.otcs.url_frontend.host,
182
+ port=settings.otcs.url_frontend.port,
76
183
  public_url=str(settings.otcs.url),
77
184
  user_partition=settings.otcs.partition,
78
185
  resource_name=settings.otcs.resource_name,
@@ -80,19 +187,62 @@ def get_otcs_object_from_otcsticket(otcsticket: Annotated[str, Depends(get_otcst
80
187
  support_path=settings.otcs.support_path,
81
188
  download_dir=settings.otcs.download_dir,
82
189
  feme_uri=settings.otcs.feme_uri,
83
- logger=logger.getChild("otcs"),
190
+ logger=logger,
84
191
  )
85
192
 
86
- otcs._otcs_ticket = otcsticket # noqa: SLF001
193
+ # Instead set the OTCS authentication ticket directly:
194
+ otcs._otcs_ticket = otcs_ticket # noqa: SLF001
87
195
 
88
196
  return otcs
89
197
 
90
198
 
199
+ def get_otca_object(otcs_object: OTCS | None = None) -> OTCA:
200
+ """Get the Content Aviator (OTCA) object.
201
+
202
+ Args:
203
+ otcs_object (OTCS | None, optional):
204
+ The Content Server (OTCS) object. Defaults to None.
205
+
206
+ Returns:
207
+ OTCA:
208
+ The new Content Aviator object.
209
+
210
+ """
211
+
212
+ settings = Settings()
213
+
214
+ # Get the Kubernetes object:
215
+ k8s_object = get_k8s_object()
216
+ content_system = {}
217
+ # Read the content system (e.g. OTCM) from the Kubernetes Config Map:
218
+ for service in ["chat", "embed"]:
219
+ cm = k8s_object.get_config_map(f"csai-{service}-svc")
220
+ if cm:
221
+ content_system[service] = cm.data.get("CONTENT_SYSTEM", "none")
222
+ logger.info("Set content system for '%s' to -> '%s'.", service, content_system[service])
223
+
224
+ # Create the Content Aviator object (OTCA class):
225
+ otca = OTCA(
226
+ chat_url=str(settings.aviator.chat_svc_url),
227
+ embed_url=str(settings.aviator.embed_svc_url),
228
+ studio_url=str(settings.aviator.studio_url),
229
+ otds_url=str(settings.otds.url_internal),
230
+ client_id=settings.aviator.oauth_client,
231
+ client_secret=settings.aviator.oauth_secret,
232
+ otcs_object=otcs_object,
233
+ content_system=content_system,
234
+ logger=logger.getChild("otca"),
235
+ )
236
+
237
+ return otca
238
+
239
+
91
240
  def get_settings() -> CustomizerAPISettings:
92
241
  """Get the API Settings object.
93
242
 
94
243
  Returns:
95
- CustomizerPISettings: Returns the API Settings
244
+ CustomizerPISettings:
245
+ Returns the API Settings.
96
246
 
97
247
  """
98
248
 
@@ -112,6 +262,7 @@ def get_otcs_logs_lock() -> dict:
112
262
 
113
263
  def list_files_in_directory(directory: str) -> dict:
114
264
  """Recursively list files in a directory and return a nested JSON structure with URLs."""
265
+
115
266
  result = {}
116
267
  for root, dirs, files in os.walk(directory):
117
268
  # Sort directories and files alphabetically
@@ -128,4 +279,5 @@ def list_files_in_directory(directory: str) -> dict:
128
279
  for file in files:
129
280
  file_path = os.path.join(relative_path, file)
130
281
  current_level[file] = file_path
282
+
131
283
  return result
pyxecm_api/settings.py CHANGED
@@ -56,9 +56,6 @@ class CustomizerAPISettings(BaseSettings):
56
56
  )
57
57
 
58
58
  loglevel: Literal["INFO", "DEBUG", "WARNING", "ERROR"] = "INFO"
59
- log_payload_processing: bool = Field(
60
- default=False, description="Print the customizer payload processing log messages to stdout"
61
- )
62
59
  logfolder: str = Field(
63
60
  default=os.path.join(tempfile.gettempdir(), "customizer"),
64
61
  description="Logfolder for Customizer logfiles",
@@ -122,11 +119,6 @@ class CustomizerAPISettings(BaseSettings):
122
119
  description="Prefix for the CSAI",
123
120
  )
124
121
 
125
- csai_studio_integration: bool = Field(
126
- default=False,
127
- description="Enable the CSAI Studio Integration",
128
- )
129
-
130
122
  upload_folder: str = Field(default=os.path.join(tempfile.gettempdir(), "upload"), description="Folder for uploads")
131
123
 
132
124
  upload_key: str = Field(default=str(uuid.uuid4()), description="Upload key for the Logs")
@@ -93,7 +93,7 @@ async def ws_terminal(
93
93
  read_task = asyncio.create_task(read_from_pty())
94
94
  write_task = asyncio.create_task(write_to_pty())
95
95
 
96
- done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED)
96
+ _done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED)
97
97
 
98
98
  # Cancel other task
99
99
  for task in pending:
@@ -4,15 +4,14 @@ import logging
4
4
  from http import HTTPStatus
5
5
  from typing import Annotated
6
6
 
7
- from fastapi import APIRouter, Body, Depends, Query
7
+ from fastapi import APIRouter, Body, Depends, HTTPException, Query
8
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
9
  from pyxecm.otcs import OTCS
10
10
  from pyxecm_customizer.k8s import K8s
11
11
 
12
- from pyxecm_api.agents.functions import get_otca_object
13
12
  from pyxecm_api.auth.functions import get_authorized_user
14
13
  from pyxecm_api.auth.models import User
15
- from pyxecm_api.common.functions import get_k8s_object, get_otcs_object, get_settings
14
+ from pyxecm_api.common.functions import get_k8s_object, get_otca_object, get_otcs_object, get_settings
16
15
  from pyxecm_api.settings import CustomizerAPISettings
17
16
 
18
17
  from .models import CSAIEmbedMetadata
@@ -31,12 +30,16 @@ def embed_metadata(
31
30
  """Embed the Metadata of the given objects.
32
31
 
33
32
  Args:
34
- user (Annotated[User, Depends): User required for authentication
35
- otcs_object (Annotated[OTCS, Depends(get_otcs_object)]): OTCS object to interact with OTCS
36
- body (Annotated[CSAIEmbedMetadata, Body): Request body
33
+ user (Annotated[User, Depends):
34
+ User required for authentication.
35
+ otcs_object (Annotated[OTCS, Depends(get_otcs_object)]):
36
+ The OTCS object to interact with OTCM (Content Server).
37
+ body (Annotated[CSAIEmbedMetadata, Body):
38
+ The request body.
37
39
 
38
40
  Returns:
39
- JSONResponse: JSONResponse with success=true/false
41
+ JSONResponse:
42
+ JSONResponse with success=true/false
40
43
 
41
44
  """
42
45
 
@@ -53,7 +56,7 @@ def get_csai_config_data(
53
56
  ) -> JSONResponse:
54
57
  """Get the csai config data."""
55
58
 
56
- logger.info("READ csai config data by user -> %s", user.id)
59
+ logger.info("Read CSAI config data by user -> %s", user.id)
57
60
 
58
61
  config_data = {}
59
62
 
@@ -79,9 +82,9 @@ def set_csai_config_data(
79
82
  k8s_object: Annotated[K8s, Depends(get_k8s_object)],
80
83
  config: Annotated[dict, Body()],
81
84
  ) -> JSONResponse:
82
- """Get the csai config data."""
85
+ """Set the CSAI config data."""
83
86
 
84
- logger.info("READ csai config data by user -> %s", user.id)
87
+ logger.info("Write CSAI config data by user -> %s", user.id)
85
88
 
86
89
  for config_map in config:
87
90
  if not config_map.startswith(settings.csai_prefix):
@@ -115,27 +118,39 @@ def set_csai_config_data(
115
118
 
116
119
 
117
120
  @router.get("/graph")
118
- def get_csai_graph(name: Annotated[str, Query(..., description="Name of the Graph")]) -> HTMLResponse:
121
+ def get_csai_graph(name: Annotated[str, Query(..., description="Name of the graph")]) -> HTMLResponse:
119
122
  """Display the graph of the given name.
120
123
 
121
124
  Args:
122
- otca (Annotated[OTCA, Depends): Generic OTCA Objec
123
- name (str): name of the graph
125
+ name (str):
126
+ The name of the CSAI graph.
124
127
 
125
128
  Returns:
126
- HTMLResponse: _description_
129
+ HTMLResponse: Visualization of the CSAI graph
127
130
 
128
131
  """
132
+
133
+ # Get the Content Aviator object:
129
134
  otca = get_otca_object(otcs_object=None)
130
135
 
136
+ # Get all graphs configured in Content Aviator:
131
137
  graphs = otca.get_graphs()
138
+ # Find the graph (LangGraph) with the given name:
132
139
  graph = [g for g in graphs if g["name"] == name]
133
140
 
134
- if graph:
141
+ if not graph:
142
+ logger.error("Couldn't find graph -> '%s' for visualization!", name)
143
+ raise HTTPException(status_code=404, detail="Graph -> '{}' not found!".format(name))
144
+
145
+ try:
135
146
  filename = otca.visualize_graph(graph[0]["id"])
136
147
 
137
- with open(filename) as f:
138
- file_content = f.read()
148
+ with open(filename) as f:
149
+ file_content = f.read()
150
+ except Exception as e:
151
+ logger.error("Error visualizing graph -> '%s': %s", name, str(e))
152
+ raise HTTPException(status_code=500, detail="Failed to visualize graph -> '{}'!".format(name)) from e
153
+
154
+ logger.info("Successfully visualized graph -> '%s'", name)
139
155
 
140
- logger.info("name: %s", name)
141
156
  return HTMLResponse(status_code=200, content=file_content)