Skip to content

Commit 8e9aace

Browse files
authored
Merge branch 'main' into release/v2.13.5
2 parents 1df3e58 + 0ada1cc commit 8e9aace

File tree

8 files changed

+392
-140
lines changed

8 files changed

+392
-140
lines changed

ads/aqua/constants.py

+25
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
HF_METADATA_FOLDER = ".cache/"
4141
HF_LOGIN_DEFAULT_TIMEOUT = 2
4242
MODEL_NAME_DELIMITER = ";"
43+
AQUA_TROUBLESHOOTING_LINK = "https://github.com/oracle-samples/oci-data-science-ai-samples/blob/main/ai-quick-actions/troubleshooting-tips.md"
4344

4445
TRAINING_METRICS_FINAL = "training_metrics_final"
4546
VALIDATION_METRICS_FINAL = "validation_metrics_final"
@@ -85,3 +86,27 @@
8586
"--host",
8687
}
8788
TEI_CONTAINER_DEFAULT_HOST = "8080"
89+
90+
OCI_OPERATION_FAILURES = {
91+
"list_model_deployments": "Unable to list model deployments. See tips for troubleshooting: ",
92+
"list_models": "Unable to list models. See tips for troubleshooting: ",
93+
"get_namespace": "Unable to access specified Object Storage Bucket. See tips for troubleshooting: ",
94+
"list_log_groups":"Unable to access logs. See tips for troubleshooting: " ,
95+
"list_buckets": "Unable to list Object Storage Bucket. See tips for troubleshooting: ",
96+
"put_object": "Unable to access or find Object Storage Bucket. See tips for troubleshooting: ",
97+
"list_model_version_sets": "Unable to create or fetch model version set. See tips for troubleshooting:",
98+
"update_model": "Unable to update model. See tips for troubleshooting: ",
99+
"list_data_science_private_endpoints": "Unable to access private endpoint. See tips for troubleshooting: ",
100+
"create_model" : "Unable to register model. See tips for troubleshooting: ",
101+
"create_deployment": "Unable to create deployment. See tips for troubleshooting: ",
102+
"create_model_version_sets" : "Unable to create model version set. See tips for troubleshooting: ",
103+
"create_job": "Unable to create job. See tips for troubleshooting: ",
104+
"create_job_run": "Unable to create job run. See tips for troubleshooting: ",
105+
}
106+
107+
STATUS_CODE_MESSAGES = {
108+
"400": "Could not process your request due to invalid input.",
109+
"403": "We're having trouble processing your request with the information provided.",
110+
"404": "Authorization Failed: The resource you're looking for isn't accessible.",
111+
"408": "Server is taking too long to respond, please try again.",
112+
}

ads/aqua/extension/aqua_ws_msg_handler.py

+6-36
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,18 @@
33
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
44
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
55

6-
import traceback
7-
import uuid
86
from abc import abstractmethod
9-
from http.client import responses
107
from typing import List
118

12-
from tornado.web import HTTPError
13-
14-
from ads.aqua import logger
159
from ads.aqua.common.decorator import handle_exceptions
16-
from ads.aqua.extension.base_handler import AquaAPIhandler
1710
from ads.aqua.extension.models.ws_models import (
1811
AquaWsError,
1912
BaseRequest,
2013
BaseResponse,
2114
ErrorResponse,
2215
RequestResponseType,
2316
)
17+
from ads.aqua.extension.utils import construct_error
2418
from ads.config import AQUA_TELEMETRY_BUCKET, AQUA_TELEMETRY_BUCKET_NS
2519
from ads.telemetry.client import TelemetryClient
2620

@@ -55,48 +49,24 @@ def process(self) -> BaseResponse:
5549

5650
def write_error(self, status_code, **kwargs):
5751
"""AquaWSMSGhandler errors are JSON, not human pages."""
58-
reason = kwargs.get("reason")
52+
5953
service_payload = kwargs.get("service_payload", {})
60-
default_msg = responses.get(status_code, "Unknown HTTP Error")
61-
message = AquaAPIhandler.get_default_error_messages(
62-
service_payload, str(status_code), kwargs.get("message", default_msg)
63-
)
64-
reply = {
65-
"status": status_code,
66-
"message": message,
67-
"service_payload": service_payload,
68-
"reason": reason,
69-
"request_id": str(uuid.uuid4()),
70-
}
71-
exc_info = kwargs.get("exc_info")
72-
if exc_info:
73-
logger.error(
74-
f"Error Request ID: {reply['request_id']}\n"
75-
f"Error: {''.join(traceback.format_exception(*exc_info))}"
76-
)
77-
e = exc_info[1]
78-
if isinstance(e, HTTPError):
79-
reply["message"] = e.log_message or message
80-
reply["reason"] = e.reason
54+
reply_details = construct_error(status_code, **kwargs)
8155

82-
logger.error(
83-
f"Error Request ID: {reply['request_id']}\n"
84-
f"Error: {reply['message']} {reply['reason']}"
85-
)
8656
# telemetry may not be present if there is an error while initializing
8757
if hasattr(self, "telemetry"):
8858
aqua_api_details = kwargs.get("aqua_api_details", {})
8959
self.telemetry.record_event_async(
9060
category="aqua/error",
9161
action=str(status_code),
92-
value=reason,
62+
value=reply_details.reason,
9363
**aqua_api_details,
9464
)
9565
response = AquaWsError(
9666
status=status_code,
97-
message=message,
67+
message=reply_details.message,
9868
service_payload=service_payload,
99-
reason=reason,
69+
reason=reply_details.reason,
10070
)
10171
base_message = BaseRequest.from_json(self.message, ignore_unknown=True)
10272
return ErrorResponse(

ads/aqua/extension/base_handler.py

+8-70
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@
22
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
44

5-
65
import json
7-
import traceback
8-
import uuid
96
from dataclasses import asdict, is_dataclass
10-
from http.client import responses
117
from typing import Any
128

139
from notebook.base.handlers import APIHandler
1410
from tornado import httputil
15-
from tornado.web import Application, HTTPError
11+
from tornado.web import Application
1612

17-
from ads.aqua import logger
1813
from ads.aqua.common.utils import is_pydantic_model
14+
from ads.aqua.extension.utils import construct_error
1915
from ads.config import AQUA_TELEMETRY_BUCKET, AQUA_TELEMETRY_BUCKET_NS
2016
from ads.telemetry.client import TelemetryClient
2117

@@ -75,78 +71,20 @@ def finish(self, payload=None): # pylint: disable=W0221
7571

7672
def write_error(self, status_code, **kwargs):
7773
"""AquaAPIhandler errors are JSON, not human pages."""
78-
self.set_header("Content-Type", "application/json")
79-
reason = kwargs.get("reason")
80-
self.set_status(status_code, reason=reason)
81-
service_payload = kwargs.get("service_payload", {})
82-
default_msg = responses.get(status_code, "Unknown HTTP Error")
83-
message = self.get_default_error_messages(
84-
service_payload, str(status_code), kwargs.get("message", default_msg)
85-
)
86-
87-
reply = {
88-
"status": status_code,
89-
"message": message,
90-
"service_payload": service_payload,
91-
"reason": reason,
92-
"request_id": str(uuid.uuid4()),
93-
}
94-
exc_info = kwargs.get("exc_info")
95-
if exc_info:
96-
logger.error(
97-
f"Error Request ID: {reply['request_id']}\n"
98-
f"Error: {''.join(traceback.format_exception(*exc_info))}"
99-
)
100-
e = exc_info[1]
101-
if isinstance(e, HTTPError):
102-
reply["message"] = e.log_message or message
103-
reply["reason"] = e.reason if e.reason else reply["reason"]
10474

105-
logger.error(
106-
f"Error Request ID: {reply['request_id']}\n"
107-
f"Error: {reply['message']} {reply['reason']}"
108-
)
75+
reply_details = construct_error(status_code, **kwargs)
76+
77+
self.set_header("Content-Type", "application/json")
78+
self.set_status(status_code, reason=reply_details.reason)
10979

11080
# telemetry may not be present if there is an error while initializing
11181
if hasattr(self, "telemetry"):
11282
aqua_api_details = kwargs.get("aqua_api_details", {})
11383
self.telemetry.record_event_async(
11484
category="aqua/error",
11585
action=str(status_code),
116-
value=reason,
86+
value=reply_details.reason,
11787
**aqua_api_details,
11888
)
11989

120-
self.finish(json.dumps(reply))
121-
122-
@staticmethod
123-
def get_default_error_messages(
124-
service_payload: dict,
125-
status_code: str,
126-
default_msg: str = "Unknown HTTP Error.",
127-
):
128-
"""Method that maps the error messages based on the operation performed or the status codes encountered."""
129-
130-
messages = {
131-
"400": "Something went wrong with your request.",
132-
"403": "We're having trouble processing your request with the information provided.",
133-
"404": "Authorization Failed: The resource you're looking for isn't accessible.",
134-
"408": "Server is taking too long to response, please try again.",
135-
"create": "Authorization Failed: Could not create resource.",
136-
"get": "Authorization Failed: The resource you're looking for isn't accessible.",
137-
}
138-
139-
if service_payload and "operation_name" in service_payload:
140-
operation_name = service_payload["operation_name"]
141-
if operation_name:
142-
if operation_name.startswith("create"):
143-
return messages["create"] + f" Operation Name: {operation_name}."
144-
elif operation_name.startswith("list") or operation_name.startswith(
145-
"get"
146-
):
147-
return messages["get"] + f" Operation Name: {operation_name}."
148-
149-
if status_code in messages:
150-
return messages[status_code]
151-
else:
152-
return default_msg
90+
self.finish(reply_details)

ads/aqua/extension/errors.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
#!/usr/bin/env python
22
# Copyright (c) 2024 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
4+
import uuid
5+
from typing import Any, Dict, List, Optional
46

7+
from pydantic import Field
8+
9+
from ads.aqua.config.utils.serializer import Serializable
10+
11+
from ads.aqua.constants import (
12+
AQUA_TROUBLESHOOTING_LINK
13+
)
514

615
class Errors(str):
716
INVALID_INPUT_DATA_FORMAT = "Invalid format of input data."
817
NO_INPUT_DATA = "No input data provided."
918
MISSING_REQUIRED_PARAMETER = "Missing required parameter: '{}'"
1019
MISSING_ONEOF_REQUIRED_PARAMETER = "Either '{}' or '{}' is required."
1120
INVALID_VALUE_OF_PARAMETER = "Invalid value of parameter: '{}'"
21+
22+
class ReplyDetails(Serializable):
23+
"""Structured reply to be returned to the client."""
24+
status: int
25+
troubleshooting_tips: str = Field(f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}",
26+
description="GitHub Link for troubleshooting documentation")
27+
message: str = Field("Unknown HTTP Error.", description="GitHub Link for troubleshooting documentation")
28+
service_payload: Optional[Dict[str, Any]] = Field(default_factory=dict)
29+
reason: str = Field("Unknown error", description="Reason for Error")
30+
request_id: str = Field(str(uuid.uuid4()), description="Unique ID for tracking the error.")

ads/aqua/extension/utils.py

+114-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
#!/usr/bin/env python
22
# Copyright (c) 2024 Oracle and/or its affiliates.
33
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
4+
5+
import re
6+
import traceback
7+
import uuid
48
from dataclasses import fields
59
from datetime import datetime, timedelta
10+
from http.client import responses
611
from typing import Dict, Optional
712

813
from cachetools import TTLCache, cached
914
from tornado.web import HTTPError
1015

11-
from ads.aqua import ODSC_MODEL_COMPARTMENT_OCID
16+
from ads.aqua import ODSC_MODEL_COMPARTMENT_OCID, logger
1217
from ads.aqua.common.utils import fetch_service_compartment
13-
from ads.aqua.extension.errors import Errors
18+
from ads.aqua.constants import (
19+
AQUA_TROUBLESHOOTING_LINK,
20+
OCI_OPERATION_FAILURES,
21+
STATUS_CODE_MESSAGES,
22+
)
23+
from ads.aqua.extension.errors import Errors, ReplyDetails
1424

1525

1626
def validate_function_parameters(data_class, input_data: Dict):
@@ -32,3 +42,105 @@ def ui_compatability_check():
3242
fetched from the configuration. The cached result is returned when multiple calls are made in quick succession
3343
from the UI to avoid multiple config file loads."""
3444
return ODSC_MODEL_COMPARTMENT_OCID or fetch_service_compartment()
45+
46+
47+
def get_default_error_messages(
48+
service_payload: dict,
49+
status_code: str,
50+
default_msg: str = "Unknown HTTP Error.",
51+
)-> str:
52+
"""Method that maps the error messages based on the operation performed or the status codes encountered."""
53+
54+
if service_payload and "operation_name" in service_payload:
55+
operation_name = service_payload.get("operation_name")
56+
57+
if operation_name and status_code in STATUS_CODE_MESSAGES:
58+
return f"{STATUS_CODE_MESSAGES[status_code]}\n{service_payload.get('message')}\nOperation Name: {operation_name}."
59+
60+
return STATUS_CODE_MESSAGES.get(status_code, default_msg)
61+
62+
63+
def get_documentation_link(key: str) -> str:
64+
"""Generates appropriate GitHub link to AQUA Troubleshooting Documentation per the user's error."""
65+
github_header = re.sub(r"_", "-", key)
66+
return f"{AQUA_TROUBLESHOOTING_LINK}#{github_header}"
67+
68+
69+
def get_troubleshooting_tips(service_payload: dict,
70+
status_code: str) -> str:
71+
"""Maps authorization errors to potential solutions on Troubleshooting Page per Aqua Documentation on oci-data-science-ai-samples"""
72+
73+
tip = f"For general tips on troubleshooting: {AQUA_TROUBLESHOOTING_LINK}"
74+
75+
if status_code in (404, 400):
76+
failed_operation = service_payload.get('operation_name')
77+
78+
if failed_operation in OCI_OPERATION_FAILURES:
79+
link = get_documentation_link(failed_operation)
80+
tip = OCI_OPERATION_FAILURES[failed_operation] + link
81+
82+
return tip
83+
84+
85+
def construct_error(status_code: int, **kwargs) -> ReplyDetails:
86+
"""
87+
Formats an error response based on the provided status code and optional details.
88+
89+
Args:
90+
status_code (int): The HTTP status code of the error.
91+
**kwargs: Additional optional parameters:
92+
- reason (str, optional): A brief reason for the error.
93+
- service_payload (dict, optional): Contextual error data from OCI SDK methods
94+
- message (str, optional): A custom error message, from error raised from failed AQUA methods calling OCI SDK methods
95+
- exc_info (tuple, optional): Exception information (e.g., from `sys.exc_info()`), used for logging.
96+
97+
Returns:
98+
ReplyDetails: A Pydantic object containing details about the formatted error response.
99+
kwargs:
100+
- "status" (int): The HTTP status code.
101+
- "troubleshooting_tips" (str): a GitHub link to AQUA troubleshooting docs, may be linked to a specific header.
102+
- "message" (str): error message.
103+
- "service_payload" (Dict[str, Any], optional) : Additional context from OCI Python SDK call.
104+
- "reason" (str): The reason for the error.
105+
- "request_id" (str): A unique identifier for tracking the error.
106+
107+
Logs:
108+
- Logs the error details with a unique request ID.
109+
- If `exc_info` is provided and contains an `HTTPError`, updates the response message and reason accordingly.
110+
111+
"""
112+
reason = kwargs.get("reason", "Unknown Error")
113+
service_payload = kwargs.get("service_payload", {})
114+
default_msg = responses.get(status_code, "Unknown HTTP Error")
115+
message = get_default_error_messages(
116+
service_payload, str(status_code), kwargs.get("message", default_msg)
117+
)
118+
119+
tips = get_troubleshooting_tips(service_payload, status_code)
120+
121+
122+
reply = ReplyDetails(
123+
status = status_code,
124+
troubleshooting_tips = tips,
125+
message = message,
126+
service_payload = service_payload,
127+
reason = reason,
128+
request_id = str(uuid.uuid4()),
129+
)
130+
131+
exc_info = kwargs.get("exc_info")
132+
if exc_info:
133+
logger.error(
134+
f"Error Request ID: {reply.request_id}\n"
135+
f"Error: {''.join(traceback.format_exception(*exc_info))}"
136+
)
137+
e = exc_info[1]
138+
if isinstance(e, HTTPError):
139+
reply.message = e.log_message or message
140+
reply.reason = e.reason if e.reason else reply.reason
141+
142+
logger.error(
143+
f"Error Request ID: {reply.request_id}\n"
144+
f"Error: {reply.message} {reply.reason}"
145+
)
146+
return reply

0 commit comments

Comments
 (0)