pyxecm 3.0.0__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 (53) 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.1.0.dist-info/METADATA +127 -0
  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.0.dist-info/METADATA +0 -48
  37. pyxecm-3.0.0.dist-info/RECORD +0 -96
  38. pyxecm_api/agents/__init__.py +0 -7
  39. pyxecm_api/agents/app.py +0 -13
  40. pyxecm_api/agents/functions.py +0 -119
  41. pyxecm_api/agents/models.py +0 -10
  42. pyxecm_api/agents/otcm_knowledgegraph/__init__.py +0 -1
  43. pyxecm_api/agents/otcm_knowledgegraph/functions.py +0 -85
  44. pyxecm_api/agents/otcm_knowledgegraph/models.py +0 -61
  45. pyxecm_api/agents/otcm_knowledgegraph/router.py +0 -74
  46. pyxecm_api/agents/otcm_user_agent/__init__.py +0 -1
  47. pyxecm_api/agents/otcm_user_agent/models.py +0 -20
  48. pyxecm_api/agents/otcm_user_agent/router.py +0 -65
  49. pyxecm_api/agents/otcm_workspace_agent/__init__.py +0 -1
  50. pyxecm_api/agents/otcm_workspace_agent/models.py +0 -40
  51. pyxecm_api/agents/otcm_workspace_agent/router.py +0 -200
  52. {pyxecm-3.0.0.dist-info → pyxecm-3.1.0.dist-info}/WHEEL +0 -0
  53. {pyxecm-3.0.0.dist-info → pyxecm-3.1.0.dist-info}/entry_points.txt +0 -0
pyxecm_api/app.py CHANGED
@@ -8,7 +8,6 @@ __email__ = "mdiefenb@opentext.com"
8
8
 
9
9
  import logging
10
10
  import os
11
- import threading
12
11
  from collections.abc import AsyncGenerator
13
12
  from contextlib import asynccontextmanager
14
13
  from datetime import UTC, datetime
@@ -23,9 +22,6 @@ from prometheus_fastapi_instrumentator import Instrumentator
23
22
  from pyxecm.helper.otel_config import tracer
24
23
  from pyxecm_maintenance_page import run_maintenance_page
25
24
 
26
- from .agents.app import agent_routers
27
- from .agents.functions import register_all
28
- from .agents.otcm_knowledgegraph.functions import build_graph
29
25
  from .auth.router import router as auth_router
30
26
  from .common.functions import PAYLOAD_LIST
31
27
  from .common.metrics import payload_logs_by_payload, payload_logs_total
@@ -50,31 +46,20 @@ if os.path.isfile(os.path.join(api_settings.logfolder, api_settings.logfile)):
50
46
  elif not os.path.exists(api_settings.logfolder):
51
47
  os.makedirs(api_settings.logfolder)
52
48
 
53
- handlers = [logging.FileHandler(os.path.join(api_settings.logfolder, api_settings.logfile))]
54
- if api_settings.log_payload_processing:
55
- handlers.append(logging.StreamHandler())
56
-
57
- logging.basicConfig(
58
- format="%(asctime)s %(levelname)s [%(name)s] [%(threadName)s] %(message)s",
59
- datefmt="%d-%b-%Y %H:%M:%S",
60
- level=api_settings.loglevel,
61
- handlers=handlers,
62
- )
63
-
64
49
 
65
50
  @asynccontextmanager
66
51
  async def lifespan(
67
52
  app: FastAPI, # noqa: ARG001
68
53
  ) -> AsyncGenerator:
69
- """Lifespan Method for FASTAPI to handle the startup and shutdown process.
54
+ """Lifespan function for FASTAPI to handle the startup and shutdown process.
70
55
 
71
56
  Args:
72
57
  app (FastAPI):
73
- The application.
58
+ The FastAPI application.
74
59
 
75
60
  """
76
61
 
77
- logger.debug("Settings -> %s", api_settings)
62
+ logger.debug("API settings -> %s", api_settings)
78
63
 
79
64
  with tracer.start_as_current_span("import_payloads"):
80
65
  if api_settings.import_payload:
@@ -89,20 +74,18 @@ async def lifespan(
89
74
  # Optional Payload
90
75
  import_payload(payload_dir=api_settings.payload_dir_optional)
91
76
 
92
- logger.info("Starting maintenance_page thread...")
77
+ logger.info("Starting maintenance page thread...")
93
78
  if api_settings.maintenance_page:
94
79
  run_maintenance_page()
95
80
 
96
- if api_settings.csai_studio_integration:
97
- logger.info("Registering Content Aviator tools...")
98
- register_all()
99
- threading.Thread(name="KnowledgeGraph", target=build_graph).start()
100
-
101
81
  yield
82
+
102
83
  logger.info("Shutdown")
103
84
  PAYLOAD_LIST.stop_payload_processing()
104
85
 
105
86
 
87
+ # end function lifespan
88
+
106
89
  app = FastAPI(
107
90
  docs_url="/api",
108
91
  title=api_settings.title,
@@ -114,7 +97,7 @@ app = FastAPI(
114
97
  openapi_tags=[
115
98
  {
116
99
  "name": "auth",
117
- "description": "Authentication Endpoint - Users are authenticated against Opentext Directory Services",
100
+ "description": "Authentication Endpoint - Users are authenticated against OpenText Directory Services",
118
101
  },
119
102
  {
120
103
  "name": "payload",
@@ -156,8 +139,6 @@ if api_settings.csai:
156
139
  )
157
140
 
158
141
  app.include_router(router=v1_csai_router)
159
- for agent_router in agent_routers:
160
- app.include_router(prefix="/agents", router=agent_router)
161
142
 
162
143
 
163
144
  ## Add Prometheus Instrumentator for /metrics,
@@ -170,7 +151,7 @@ if api_settings.metrics:
170
151
 
171
152
  ## Start the API Server
172
153
  def run_api() -> None:
173
- """Start the FASTAPI Webserver."""
154
+ """Start the FastAPI Webserver."""
174
155
 
175
156
  # Check if Temp and Log dir exists
176
157
  if not os.path.exists(api_settings.temp_dir):
@@ -183,27 +164,56 @@ def run_api() -> None:
183
164
  customizer_start_time = datetime.now(UTC).strftime(
184
165
  "%Y-%m-%d_%H-%M",
185
166
  )
186
- api_settings.logfile = f"customizer_{customizer_start_time}.log"
167
+ api_settings.logfile = "customizer_{}.log".format(customizer_start_time)
187
168
 
188
169
  # Configure Logging for uvicorn
189
170
  log_config = uvicorn.config.LOGGING_CONFIG
190
171
 
191
172
  # Stdout
192
- log_config["formatters"]["pyxecm"] = {
173
+ log_config["formatters"]["standard"] = {
193
174
  "()": "uvicorn.logging.DefaultFormatter",
194
175
  "fmt": "%(levelprefix)s [%(name)s] [%(threadName)s] %(message)s",
195
176
  "use_colors": True,
196
177
  }
197
- log_config["handlers"]["pyxecm"] = {
198
- "formatter": "pyxecm",
178
+
179
+ log_config["formatters"]["logfile"] = {
180
+ "()": "uvicorn.logging.DefaultFormatter",
181
+ "fmt": "%(asctime)s %(levelname)s [%(name)s] [%(threadName)s] %(message)s",
182
+ "datefmt": "%d-%b-%Y %H:%M:%S",
183
+ "use_colors": True,
184
+ }
185
+
186
+ log_config["formatters"]["accesslog"] = {
187
+ "()": "uvicorn.logging.AccessFormatter",
188
+ "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
189
+ "use_colors": False,
190
+ }
191
+
192
+ log_config["handlers"]["console"] = {
193
+ "formatter": "standard",
199
194
  "class": "logging.StreamHandler",
200
195
  "stream": "ext://sys.stdout",
201
196
  }
202
197
 
203
- log_config["loggers"]["pyxecm"] = {
204
- "handlers": ["pyxecm"],
198
+ log_config["handlers"]["file"] = {
199
+ "formatter": "logfile",
200
+ "class": "logging.FileHandler",
201
+ "filename": os.path.join(api_settings.logfolder, api_settings.logfile),
202
+ "mode": "a",
203
+ }
204
+
205
+ log_config["handlers"]["accesslog"] = {
206
+ "formatter": "accesslog",
207
+ "class": "logging.FileHandler",
208
+ "filename": os.path.join(api_settings.logfolder, "access.log"),
209
+ "mode": "a",
210
+ }
211
+
212
+ log_config["loggers"]["uvicorn.access"]["handlers"] = ["access", "accesslog"]
213
+
214
+ log_config["loggers"]["root"] = {
205
215
  "level": api_settings.loglevel,
206
- "propagate": False,
216
+ "handlers": ["console", "file"],
207
217
  }
208
218
 
209
219
  logger.info("Starting processing thread...")
@@ -27,8 +27,8 @@ def get_groups(response: dict, token: str) -> list:
27
27
  """Get the groups of the user.
28
28
 
29
29
  Args:
30
- response (_type_): _description_
31
- token (_type_): _description_
30
+ response (dict): _description_
31
+ token (str): _description_
32
32
 
33
33
  Returns:
34
34
  list: _description_
pyxecm_api/auth/router.py CHANGED
@@ -64,9 +64,8 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> J
64
64
  async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]) -> JSONResponse:
65
65
  """Get the current user.
66
66
 
67
- current_user:
68
- type: User
69
- description: The current user.
67
+ current_user (User):
68
+ The current user.
70
69
 
71
70
  """
72
71
 
@@ -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)