oracle-ads 2.12.8__py3-none-any.whl → 2.12.10__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.
- ads/aqua/__init__.py +4 -3
- ads/aqua/app.py +40 -18
- ads/aqua/client/__init__.py +3 -0
- ads/aqua/client/client.py +799 -0
- ads/aqua/common/enums.py +3 -0
- ads/aqua/common/utils.py +62 -2
- ads/aqua/data.py +2 -19
- ads/aqua/evaluation/entities.py +6 -0
- ads/aqua/evaluation/evaluation.py +45 -15
- ads/aqua/extension/aqua_ws_msg_handler.py +14 -7
- ads/aqua/extension/base_handler.py +12 -9
- ads/aqua/extension/deployment_handler.py +8 -4
- ads/aqua/extension/finetune_handler.py +8 -14
- ads/aqua/extension/model_handler.py +30 -6
- ads/aqua/extension/ui_handler.py +13 -1
- ads/aqua/finetuning/constants.py +5 -2
- ads/aqua/finetuning/entities.py +73 -17
- ads/aqua/finetuning/finetuning.py +110 -82
- ads/aqua/model/entities.py +5 -1
- ads/aqua/model/model.py +230 -104
- ads/aqua/modeldeployment/deployment.py +35 -11
- ads/aqua/modeldeployment/entities.py +7 -4
- ads/aqua/ui.py +24 -2
- ads/cli.py +16 -8
- ads/common/auth.py +9 -9
- ads/llm/autogen/__init__.py +2 -0
- ads/llm/autogen/constants.py +15 -0
- ads/llm/autogen/reports/__init__.py +2 -0
- ads/llm/autogen/reports/base.py +67 -0
- ads/llm/autogen/reports/data.py +103 -0
- ads/llm/autogen/reports/session.py +526 -0
- ads/llm/autogen/reports/templates/chat_box.html +13 -0
- ads/llm/autogen/reports/templates/chat_box_lt.html +5 -0
- ads/llm/autogen/reports/templates/chat_box_rt.html +6 -0
- ads/llm/autogen/reports/utils.py +56 -0
- ads/llm/autogen/v02/__init__.py +4 -0
- ads/llm/autogen/{client_v02.py → v02/client.py} +23 -10
- ads/llm/autogen/v02/log_handlers/__init__.py +2 -0
- ads/llm/autogen/v02/log_handlers/oci_file_handler.py +83 -0
- ads/llm/autogen/v02/loggers/__init__.py +6 -0
- ads/llm/autogen/v02/loggers/metric_logger.py +320 -0
- ads/llm/autogen/v02/loggers/session_logger.py +580 -0
- ads/llm/autogen/v02/loggers/utils.py +86 -0
- ads/llm/autogen/v02/runtime_logging.py +163 -0
- ads/llm/guardrails/base.py +6 -5
- ads/llm/langchain/plugins/chat_models/oci_data_science.py +46 -20
- ads/llm/langchain/plugins/llms/oci_data_science_model_deployment_endpoint.py +38 -11
- ads/model/__init__.py +11 -13
- ads/model/artifact.py +47 -8
- ads/model/extractor/embedding_onnx_extractor.py +80 -0
- ads/model/framework/embedding_onnx_model.py +438 -0
- ads/model/generic_model.py +26 -24
- ads/model/model_metadata.py +8 -7
- ads/opctl/config/merger.py +13 -14
- ads/opctl/operator/common/operator_config.py +4 -4
- ads/opctl/operator/lowcode/common/transformations.py +50 -8
- ads/opctl/operator/lowcode/common/utils.py +22 -6
- ads/opctl/operator/lowcode/forecast/__main__.py +10 -0
- ads/opctl/operator/lowcode/forecast/const.py +3 -0
- ads/opctl/operator/lowcode/forecast/model/arima.py +19 -13
- ads/opctl/operator/lowcode/forecast/model/automlx.py +129 -36
- ads/opctl/operator/lowcode/forecast/model/autots.py +1 -0
- ads/opctl/operator/lowcode/forecast/model/base_model.py +58 -17
- ads/opctl/operator/lowcode/forecast/model/forecast_datasets.py +1 -1
- ads/opctl/operator/lowcode/forecast/model/neuralprophet.py +10 -3
- ads/opctl/operator/lowcode/forecast/model/prophet.py +25 -18
- ads/opctl/operator/lowcode/forecast/model_evaluator.py +3 -2
- ads/opctl/operator/lowcode/forecast/operator_config.py +31 -0
- ads/opctl/operator/lowcode/forecast/schema.yaml +76 -0
- ads/opctl/operator/lowcode/forecast/utils.py +8 -6
- ads/opctl/operator/lowcode/forecast/whatifserve/__init__.py +7 -0
- ads/opctl/operator/lowcode/forecast/whatifserve/deployment_manager.py +233 -0
- ads/opctl/operator/lowcode/forecast/whatifserve/score.py +238 -0
- ads/telemetry/base.py +18 -11
- ads/telemetry/client.py +33 -13
- ads/templates/schemas/openapi.json +1740 -0
- ads/templates/score_embedding_onnx.jinja2 +202 -0
- {oracle_ads-2.12.8.dist-info → oracle_ads-2.12.10.dist-info}/METADATA +11 -10
- {oracle_ads-2.12.8.dist-info → oracle_ads-2.12.10.dist-info}/RECORD +82 -56
- {oracle_ads-2.12.8.dist-info → oracle_ads-2.12.10.dist-info}/LICENSE.txt +0 -0
- {oracle_ads-2.12.8.dist-info → oracle_ads-2.12.10.dist-info}/WHEEL +0 -0
- {oracle_ads-2.12.8.dist-info → oracle_ads-2.12.10.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
import json
|
3
|
+
# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
|
+
|
6
|
+
import os
|
7
|
+
import pickle
|
8
|
+
import shutil
|
9
|
+
import sys
|
10
|
+
import tempfile
|
11
|
+
import oci
|
12
|
+
|
13
|
+
import pandas as pd
|
14
|
+
import cloudpickle
|
15
|
+
|
16
|
+
from ads.opctl import logger
|
17
|
+
from ads.common.model_export_util import prepare_generic_model
|
18
|
+
from ads.opctl.operator.lowcode.common.utils import write_data, write_simple_json
|
19
|
+
from ads.opctl.operator.lowcode.common.utils import default_signer
|
20
|
+
from ..model.forecast_datasets import AdditionalData
|
21
|
+
from ..operator_config import ForecastOperatorSpec
|
22
|
+
|
23
|
+
from oci.data_science import DataScienceClient, DataScienceClientCompositeOperations
|
24
|
+
|
25
|
+
from oci.data_science.models import ModelConfigurationDetails, InstanceConfiguration, \
|
26
|
+
FixedSizeScalingPolicy, CategoryLogDetails, LogDetails, \
|
27
|
+
SingleModelDeploymentConfigurationDetails, CreateModelDeploymentDetails
|
28
|
+
from ads.common.object_storage_details import ObjectStorageDetails
|
29
|
+
|
30
|
+
|
31
|
+
class ModelDeploymentManager:
|
32
|
+
def __init__(self, spec: ForecastOperatorSpec, additional_data: AdditionalData, previous_model_version=None):
|
33
|
+
self.spec = spec
|
34
|
+
self.model_name = spec.model
|
35
|
+
self.horizon = spec.horizon
|
36
|
+
self.additional_data = additional_data.get_dict_by_series()
|
37
|
+
self.model_obj = {}
|
38
|
+
self.display_name = spec.what_if_analysis.model_display_name
|
39
|
+
self.project_id = spec.what_if_analysis.project_id if spec.what_if_analysis.project_id \
|
40
|
+
else os.environ.get('PROJECT_OCID')
|
41
|
+
self.compartment_id = spec.what_if_analysis.compartment_id if spec.what_if_analysis.compartment_id \
|
42
|
+
else os.environ.get('NB_SESSION_COMPARTMENT_OCID')
|
43
|
+
if self.project_id is None or self.compartment_id is None:
|
44
|
+
raise ValueError("Either project_id or compartment_id cannot be None.")
|
45
|
+
self.path_to_artifact = f"{self.spec.output_directory.url}/artifacts/"
|
46
|
+
self.pickle_file_path = f"{self.spec.output_directory.url}/model.pkl"
|
47
|
+
self.model_version = previous_model_version + 1 if previous_model_version else 1
|
48
|
+
self.catalog_id = None
|
49
|
+
self.test_mode = os.environ.get("TEST_MODE", False)
|
50
|
+
self.deployment_info = {}
|
51
|
+
|
52
|
+
def _sanity_test(self):
|
53
|
+
"""
|
54
|
+
Function perform sanity test for saved artifact
|
55
|
+
"""
|
56
|
+
org_sys_path = sys.path[:]
|
57
|
+
try:
|
58
|
+
sys.path.insert(0, f"{self.path_to_artifact}")
|
59
|
+
from score import load_model, predict
|
60
|
+
_ = load_model()
|
61
|
+
|
62
|
+
# Write additional data to tmp file and perform sanity check
|
63
|
+
with tempfile.NamedTemporaryFile(suffix='.csv') as temp_file:
|
64
|
+
one_series = next(iter(self.additional_data))
|
65
|
+
sample_prediction_data = self.additional_data[one_series].tail(self.horizon)
|
66
|
+
sample_prediction_data[self.spec.target_category_columns[0]] = one_series
|
67
|
+
date_col_name = self.spec.datetime_column.name
|
68
|
+
date_col_format = self.spec.datetime_column.format
|
69
|
+
sample_prediction_data[date_col_name] = sample_prediction_data[date_col_name].dt.strftime(
|
70
|
+
date_col_format)
|
71
|
+
sample_prediction_data.to_csv(temp_file.name, index=False)
|
72
|
+
input_data = {"additional_data": {"url": temp_file.name}}
|
73
|
+
prediction_test = predict(input_data, _)
|
74
|
+
logger.info(f"prediction test completed with result :{prediction_test}")
|
75
|
+
except Exception as e:
|
76
|
+
logger.error(f"An error occurred during the sanity test: {e}")
|
77
|
+
raise
|
78
|
+
finally:
|
79
|
+
sys.path = org_sys_path
|
80
|
+
|
81
|
+
def _copy_score_file(self):
|
82
|
+
"""
|
83
|
+
Copies the score.py to the artifact_path.
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
87
|
+
score_file = os.path.join(current_dir, "score.py")
|
88
|
+
destination_file = os.path.join(self.path_to_artifact, os.path.basename(score_file))
|
89
|
+
shutil.copy2(score_file, destination_file)
|
90
|
+
logger.info(f"score.py copied successfully to {self.path_to_artifact}")
|
91
|
+
except Exception as e:
|
92
|
+
logger.warn(f"Error copying file: {e}")
|
93
|
+
raise e
|
94
|
+
|
95
|
+
def save_to_catalog(self):
|
96
|
+
"""Save the model to a model catalog"""
|
97
|
+
with open(self.pickle_file_path, 'rb') as file:
|
98
|
+
self.model_obj = pickle.load(file)
|
99
|
+
|
100
|
+
if not os.path.exists(self.path_to_artifact):
|
101
|
+
os.mkdir(self.path_to_artifact)
|
102
|
+
|
103
|
+
artifact_dict = {"spec": self.spec.to_dict(), "models": self.model_obj}
|
104
|
+
with open(f"{self.path_to_artifact}/models.pickle", "wb") as f:
|
105
|
+
cloudpickle.dump(artifact_dict, f)
|
106
|
+
artifact = prepare_generic_model(
|
107
|
+
self.path_to_artifact,
|
108
|
+
function_artifacts=False,
|
109
|
+
force_overwrite=True,
|
110
|
+
data_science_env=True)
|
111
|
+
|
112
|
+
self._copy_score_file()
|
113
|
+
self._sanity_test()
|
114
|
+
|
115
|
+
if isinstance(self.model_obj, dict):
|
116
|
+
series = self.model_obj.keys()
|
117
|
+
else:
|
118
|
+
series = self.additional_data.keys()
|
119
|
+
description = f"The object contains {len(series)} {self.model_name} models"
|
120
|
+
|
121
|
+
if not self.test_mode:
|
122
|
+
catalog_entry = artifact.save(
|
123
|
+
display_name=self.display_name,
|
124
|
+
compartment_id=self.compartment_id,
|
125
|
+
project_id=self.project_id,
|
126
|
+
description=description)
|
127
|
+
self.catalog_id = catalog_entry.id
|
128
|
+
|
129
|
+
logger.info(f"Saved {self.model_name} version-v{self.model_version} to model catalog"
|
130
|
+
f" with model ocid : {self.catalog_id}")
|
131
|
+
|
132
|
+
self.deployment_info = {"model_ocid": self.catalog_id, "series": list(series)}
|
133
|
+
|
134
|
+
def create_deployment(self):
|
135
|
+
"""Create a model deployment serving"""
|
136
|
+
|
137
|
+
# create new model deployment
|
138
|
+
initial_shape = self.spec.what_if_analysis.model_deployment.initial_shape
|
139
|
+
name = self.spec.what_if_analysis.model_deployment.display_name
|
140
|
+
description = self.spec.what_if_analysis.model_deployment.description
|
141
|
+
auto_scaling_config = self.spec.what_if_analysis.model_deployment.auto_scaling
|
142
|
+
|
143
|
+
# if auto_scaling_config is defined
|
144
|
+
if auto_scaling_config:
|
145
|
+
scaling_policy = oci.data_science.models.AutoScalingPolicy(
|
146
|
+
policy_type="AUTOSCALING",
|
147
|
+
auto_scaling_policies=[
|
148
|
+
oci.data_science.models.ThresholdBasedAutoScalingPolicyDetails(
|
149
|
+
auto_scaling_policy_type="THRESHOLD",
|
150
|
+
rules=[
|
151
|
+
oci.data_science.models.PredefinedMetricExpressionRule(
|
152
|
+
metric_expression_rule_type="PREDEFINED_EXPRESSION",
|
153
|
+
metric_type=auto_scaling_config.scaling_metric,
|
154
|
+
scale_in_configuration=oci.data_science.models.PredefinedExpressionThresholdScalingConfiguration(
|
155
|
+
scaling_configuration_type="THRESHOLD",
|
156
|
+
threshold=auto_scaling_config.scale_in_threshold
|
157
|
+
),
|
158
|
+
scale_out_configuration=oci.data_science.models.PredefinedExpressionThresholdScalingConfiguration(
|
159
|
+
scaling_configuration_type="THRESHOLD",
|
160
|
+
threshold=auto_scaling_config.scale_out_threshold
|
161
|
+
)
|
162
|
+
)],
|
163
|
+
maximum_instance_count=auto_scaling_config.maximum_instance,
|
164
|
+
minimum_instance_count=auto_scaling_config.minimum_instance,
|
165
|
+
initial_instance_count=auto_scaling_config.minimum_instance)],
|
166
|
+
cool_down_in_seconds=auto_scaling_config.cool_down_in_seconds,
|
167
|
+
is_enabled=True)
|
168
|
+
logger.info(f"Using autoscaling {auto_scaling_config.scaling_metric} for creating MD")
|
169
|
+
else:
|
170
|
+
scaling_policy = FixedSizeScalingPolicy(instance_count=1)
|
171
|
+
logger.info("Using fixed size policy for creating MD")
|
172
|
+
|
173
|
+
model_configuration_details_object = ModelConfigurationDetails(
|
174
|
+
model_id=self.catalog_id,
|
175
|
+
instance_configuration=InstanceConfiguration(
|
176
|
+
instance_shape_name=initial_shape),
|
177
|
+
scaling_policy=scaling_policy,
|
178
|
+
bandwidth_mbps=20)
|
179
|
+
|
180
|
+
single_model_config = SingleModelDeploymentConfigurationDetails(
|
181
|
+
deployment_type='SINGLE_MODEL',
|
182
|
+
model_configuration_details=model_configuration_details_object
|
183
|
+
)
|
184
|
+
|
185
|
+
log_group = self.spec.what_if_analysis.model_deployment.log_group
|
186
|
+
log_id = self.spec.what_if_analysis.model_deployment.log_id
|
187
|
+
|
188
|
+
logs_configuration_details_object = CategoryLogDetails(
|
189
|
+
access=LogDetails(log_group_id=log_group,
|
190
|
+
log_id=log_id),
|
191
|
+
predict=LogDetails(log_group_id=log_group,
|
192
|
+
log_id=log_id))
|
193
|
+
|
194
|
+
model_deploy_configuration = CreateModelDeploymentDetails(
|
195
|
+
display_name=name,
|
196
|
+
description=description,
|
197
|
+
project_id=self.project_id,
|
198
|
+
compartment_id=self.compartment_id,
|
199
|
+
model_deployment_configuration_details=single_model_config,
|
200
|
+
category_log_details=logs_configuration_details_object)
|
201
|
+
|
202
|
+
if not self.test_mode:
|
203
|
+
auth = oci.auth.signers.get_resource_principals_signer()
|
204
|
+
data_science = DataScienceClient({}, signer=auth)
|
205
|
+
data_science_composite = DataScienceClientCompositeOperations(data_science)
|
206
|
+
model_deployment = data_science_composite.create_model_deployment_and_wait_for_state(
|
207
|
+
model_deploy_configuration,
|
208
|
+
wait_for_states=[
|
209
|
+
"SUCCEEDED", "FAILED"])
|
210
|
+
self.deployment_info['work_request'] = model_deployment.data.id
|
211
|
+
logger.info(f"deployment metadata :{model_deployment.data}")
|
212
|
+
md = data_science.get_model_deployment(model_deployment_id=model_deployment.data.resources[0].identifier)
|
213
|
+
self.deployment_info['model_deployment_ocid'] = md.data.id
|
214
|
+
endpoint_url = md.data.model_deployment_url
|
215
|
+
self.deployment_info['model_deployment_endpoint'] = f"{endpoint_url}/predict"
|
216
|
+
|
217
|
+
def save_deployment_info(self):
|
218
|
+
output_dir = self.spec.output_directory.url
|
219
|
+
if ObjectStorageDetails.is_oci_path(output_dir):
|
220
|
+
storage_options = default_signer()
|
221
|
+
else:
|
222
|
+
storage_options = {}
|
223
|
+
write_data(
|
224
|
+
data=pd.DataFrame.from_dict(self.deployment_info),
|
225
|
+
filename=os.path.join(output_dir, "deployment_info.json"),
|
226
|
+
format="json",
|
227
|
+
storage_options=storage_options,
|
228
|
+
index=False,
|
229
|
+
indent=4,
|
230
|
+
orient="records"
|
231
|
+
)
|
232
|
+
write_simple_json(self.deployment_info, os.path.join(output_dir, "deployment_info.json"))
|
233
|
+
logger.info(f"Saved deployment info to {output_dir}")
|
@@ -0,0 +1,238 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
# Copyright (c) 2023, 2024 Oracle and/or its affiliates.
|
4
|
+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
|
+
|
6
|
+
import json
|
7
|
+
import os
|
8
|
+
from joblib import load
|
9
|
+
import pandas as pd
|
10
|
+
import numpy as np
|
11
|
+
from functools import lru_cache
|
12
|
+
import logging
|
13
|
+
import ads
|
14
|
+
from ads.opctl.operator.lowcode.common.utils import load_data
|
15
|
+
from ads.opctl.operator.common.operator_config import InputData
|
16
|
+
from ads.opctl.operator.lowcode.forecast.const import SupportedModels
|
17
|
+
|
18
|
+
ads.set_auth("resource_principal")
|
19
|
+
|
20
|
+
logging.basicConfig(format='%(name)s - %(levelname)s - %(message)s', level=logging.INFO)
|
21
|
+
logger_pred = logging.getLogger('model-prediction')
|
22
|
+
logger_pred.setLevel(logging.INFO)
|
23
|
+
logger_feat = logging.getLogger('input-features')
|
24
|
+
logger_feat.setLevel(logging.INFO)
|
25
|
+
|
26
|
+
"""
|
27
|
+
Inference script. This script is used for prediction by scoring server when schema is known.
|
28
|
+
"""
|
29
|
+
|
30
|
+
|
31
|
+
@lru_cache(maxsize=10)
|
32
|
+
def load_model():
|
33
|
+
"""
|
34
|
+
Loads model from the serialized format
|
35
|
+
|
36
|
+
Returns
|
37
|
+
-------
|
38
|
+
model: a model instance on which predict API can be invoked
|
39
|
+
"""
|
40
|
+
model_dir = os.path.dirname(os.path.realpath(__file__))
|
41
|
+
contents = os.listdir(model_dir)
|
42
|
+
model_file_name = "models.pickle"
|
43
|
+
if model_file_name in contents:
|
44
|
+
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), model_file_name), "rb") as file:
|
45
|
+
model = load(file)
|
46
|
+
else:
|
47
|
+
raise Exception('{0} is not found in model directory {1}'.format(model_file_name, model_dir))
|
48
|
+
return model
|
49
|
+
|
50
|
+
|
51
|
+
@lru_cache(maxsize=1)
|
52
|
+
def fetch_data_type_from_schema(
|
53
|
+
input_schema_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "input_schema.json")):
|
54
|
+
"""
|
55
|
+
Returns data type information fetch from input_schema.json.
|
56
|
+
|
57
|
+
Parameters
|
58
|
+
----------
|
59
|
+
input_schema_path: path of input schema.
|
60
|
+
|
61
|
+
Returns
|
62
|
+
-------
|
63
|
+
data_type: data type fetch from input_schema.json.
|
64
|
+
|
65
|
+
"""
|
66
|
+
data_type = {}
|
67
|
+
if os.path.exists(input_schema_path):
|
68
|
+
schema = json.load(open(input_schema_path))
|
69
|
+
for col in schema['schema']:
|
70
|
+
data_type[col['name']] = col['dtype']
|
71
|
+
else:
|
72
|
+
print(
|
73
|
+
"input_schema has to be passed in in order to recover the same data type. pass `X_sample` in `ads.model.framework.sklearn_model.SklearnModel.prepare` function to generate the input_schema. Otherwise, the data type might be changed after serialization/deserialization.")
|
74
|
+
return data_type
|
75
|
+
|
76
|
+
|
77
|
+
def deserialize(data, input_schema_path):
|
78
|
+
"""
|
79
|
+
Deserialize json serialization data to data in original type when sent to predict.
|
80
|
+
|
81
|
+
Parameters
|
82
|
+
----------
|
83
|
+
data: serialized input data.
|
84
|
+
input_schema_path: path of input schema.
|
85
|
+
|
86
|
+
Returns
|
87
|
+
-------
|
88
|
+
data: deserialized input data.
|
89
|
+
|
90
|
+
"""
|
91
|
+
|
92
|
+
# Add further data deserialization if needed
|
93
|
+
return data
|
94
|
+
|
95
|
+
|
96
|
+
def pre_inference(data, input_schema_path):
|
97
|
+
"""
|
98
|
+
Preprocess data
|
99
|
+
|
100
|
+
Parameters
|
101
|
+
----------
|
102
|
+
data: Data format as expected by the predict API of the core estimator.
|
103
|
+
input_schema_path: path of input schema.
|
104
|
+
|
105
|
+
Returns
|
106
|
+
-------
|
107
|
+
data: Data format after any processing.
|
108
|
+
|
109
|
+
"""
|
110
|
+
return deserialize(data, input_schema_path)
|
111
|
+
|
112
|
+
|
113
|
+
def post_inference(yhat):
|
114
|
+
"""
|
115
|
+
Post-process the model results
|
116
|
+
|
117
|
+
Parameters
|
118
|
+
----------
|
119
|
+
yhat: Data format after calling model.predict.
|
120
|
+
|
121
|
+
Returns
|
122
|
+
-------
|
123
|
+
yhat: Data format after any processing.
|
124
|
+
|
125
|
+
"""
|
126
|
+
if isinstance(yhat, pd.core.frame.DataFrame):
|
127
|
+
yhat = yhat.values
|
128
|
+
if isinstance(yhat, np.ndarray):
|
129
|
+
yhat = yhat.tolist()
|
130
|
+
return yhat
|
131
|
+
|
132
|
+
|
133
|
+
def get_forecast(future_df, model_name, series_id, model_object, date_col, target_column, target_cat_col, horizon):
|
134
|
+
date_col_name = date_col["name"]
|
135
|
+
date_col_format = date_col["format"]
|
136
|
+
future_df[target_cat_col] = future_df[target_cat_col].astype("str")
|
137
|
+
future_df[date_col_name] = pd.to_datetime(
|
138
|
+
future_df[date_col_name], format=date_col_format
|
139
|
+
)
|
140
|
+
if model_name == SupportedModels.AutoTS:
|
141
|
+
series_id_col = "Series"
|
142
|
+
full_data_indexed = future_df.rename(columns={target_cat_col: series_id_col})
|
143
|
+
additional_regressors = list(
|
144
|
+
set(full_data_indexed.columns) - {target_column, series_id_col, date_col_name}
|
145
|
+
)
|
146
|
+
future_reg = full_data_indexed.reset_index().pivot(
|
147
|
+
index=date_col_name,
|
148
|
+
columns=series_id_col,
|
149
|
+
values=additional_regressors,
|
150
|
+
)
|
151
|
+
pred_obj = model_object.predict(future_regressor=future_reg)
|
152
|
+
return pred_obj.forecast[series_id].tolist()
|
153
|
+
elif model_name == SupportedModels.Prophet and series_id in model_object:
|
154
|
+
model = model_object[series_id]
|
155
|
+
processed = future_df.rename(columns={date_col_name: 'ds', target_column: 'y'})
|
156
|
+
forecast = model.predict(processed)
|
157
|
+
return forecast['yhat'].tolist()
|
158
|
+
elif model_name == SupportedModels.NeuralProphet and series_id in model_object:
|
159
|
+
model = model_object[series_id]
|
160
|
+
model.restore_trainer()
|
161
|
+
accepted_regressors = list(model.config_regressors.regressors.keys())
|
162
|
+
data = future_df.rename(columns={date_col_name: 'ds', target_column: 'y'})
|
163
|
+
future = data[accepted_regressors + ["ds"]].reset_index(drop=True)
|
164
|
+
future["y"] = None
|
165
|
+
forecast = model.predict(future)
|
166
|
+
return forecast['yhat1'].tolist()
|
167
|
+
elif model_name == SupportedModels.Arima and series_id in model_object:
|
168
|
+
model = model_object[series_id]
|
169
|
+
future_df = future_df.set_index(date_col_name)
|
170
|
+
x_pred = future_df.drop(target_cat_col, axis=1)
|
171
|
+
yhat, conf_int = model.predict(
|
172
|
+
n_periods=horizon,
|
173
|
+
X=x_pred,
|
174
|
+
return_conf_int=True
|
175
|
+
)
|
176
|
+
yhat_clean = pd.DataFrame(yhat, index=yhat.index, columns=["yhat"])
|
177
|
+
return yhat_clean['yhat'].tolist()
|
178
|
+
elif model_name == SupportedModels.AutoMLX and series_id in model_object:
|
179
|
+
# automlx model
|
180
|
+
model = model_object[series_id]
|
181
|
+
x_pred = future_df.drop(target_cat_col, axis=1)
|
182
|
+
x_pred = x_pred.set_index(date_col_name)
|
183
|
+
forecast = model.forecast(
|
184
|
+
X=x_pred,
|
185
|
+
periods=horizon
|
186
|
+
)
|
187
|
+
return forecast[target_column].tolist()
|
188
|
+
else:
|
189
|
+
raise Exception(f"Invalid model object type: {type(model_object).__name__}.")
|
190
|
+
|
191
|
+
|
192
|
+
def predict(data, model=load_model()) -> dict:
|
193
|
+
"""
|
194
|
+
Returns prediction given the model and data to predict
|
195
|
+
|
196
|
+
Parameters
|
197
|
+
----------
|
198
|
+
model: Model instance returned by load_model API
|
199
|
+
data: Data format as expected by the predict API of the core estimator. For eg. in case of sckit models it could be numpy array/List of list/Panda DataFrame
|
200
|
+
|
201
|
+
Returns
|
202
|
+
-------
|
203
|
+
predictions: Output from scoring server
|
204
|
+
Format: { 'prediction': output from `model.predict` method }
|
205
|
+
|
206
|
+
"""
|
207
|
+
assert model is not None, "Model is not loaded"
|
208
|
+
|
209
|
+
models = model["models"]
|
210
|
+
specs = model["spec"]
|
211
|
+
horizon = specs["horizon"]
|
212
|
+
model_name = specs["model"]
|
213
|
+
date_col = specs["datetime_column"]
|
214
|
+
target_column = specs["target_column"]
|
215
|
+
target_category_column = specs["target_category_columns"][0]
|
216
|
+
|
217
|
+
try:
|
218
|
+
input_data = InputData(**data["additional_data"])
|
219
|
+
except TypeError as e:
|
220
|
+
raise ValueError(f"Validation error: {e}")
|
221
|
+
additional_data = load_data(input_data)
|
222
|
+
|
223
|
+
unique_values = additional_data[target_category_column].unique()
|
224
|
+
forecasts = {}
|
225
|
+
for key in unique_values:
|
226
|
+
try:
|
227
|
+
s_id = str(key)
|
228
|
+
filtered = additional_data[additional_data[target_category_column] == key]
|
229
|
+
future = filtered.tail(horizon)
|
230
|
+
forecast = get_forecast(future, model_name, s_id, models, date_col,
|
231
|
+
target_column, target_category_column, horizon)
|
232
|
+
forecasts[s_id] = json.dumps(forecast)
|
233
|
+
except Exception as e:
|
234
|
+
raise RuntimeError(
|
235
|
+
f"An error occurred during prediction: {e}."
|
236
|
+
f" Please ensure the input data matches the format and structure of the data used during training.")
|
237
|
+
|
238
|
+
return {'prediction': json.dumps(forecasts)}
|
ads/telemetry/base.py
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
|
-
#
|
3
|
-
# Copyright (c) 2024 Oracle and/or its affiliates.
|
2
|
+
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
|
4
3
|
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
4
|
|
6
5
|
import logging
|
7
6
|
|
8
|
-
|
7
|
+
import oci
|
8
|
+
|
9
9
|
from ads.common import oci_client as oc
|
10
|
-
from ads.common.auth import default_signer
|
10
|
+
from ads.common.auth import default_signer, resource_principal
|
11
11
|
from ads.config import OCI_RESOURCE_PRINCIPAL_VERSION
|
12
12
|
|
13
|
-
|
14
13
|
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
15
16
|
class TelemetryBase:
|
16
17
|
"""Base class for Telemetry Client."""
|
17
18
|
|
@@ -25,15 +26,21 @@ class TelemetryBase:
|
|
25
26
|
namespace : str, optional
|
26
27
|
Namespace of the OCI object storage bucket, by default None.
|
27
28
|
"""
|
29
|
+
# Use resource principal as authentication method if available,
|
30
|
+
# however, do not change the ADS authentication if user configured it by set_auth.
|
28
31
|
if OCI_RESOURCE_PRINCIPAL_VERSION:
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
self._auth = resource_principal()
|
33
|
+
else:
|
34
|
+
self._auth = default_signer()
|
35
|
+
self.os_client: oci.object_storage.ObjectStorageClient = oc.OCIClientFactory(
|
36
|
+
**self._auth
|
37
|
+
).object_storage
|
32
38
|
self.bucket = bucket
|
33
39
|
self._namespace = namespace
|
34
40
|
self._service_endpoint = None
|
35
|
-
logger.debug(
|
36
|
-
|
41
|
+
logger.debug(
|
42
|
+
f"Initialized Telemetry. Namespace: {self.namespace}, Bucket: {self.bucket}"
|
43
|
+
)
|
37
44
|
|
38
45
|
@property
|
39
46
|
def namespace(self) -> str:
|
@@ -58,5 +65,5 @@ class TelemetryBase:
|
|
58
65
|
Tenancy-specific endpoint.
|
59
66
|
"""
|
60
67
|
if not self._service_endpoint:
|
61
|
-
self._service_endpoint = self.os_client.base_client.endpoint
|
68
|
+
self._service_endpoint = str(self.os_client.base_client.endpoint)
|
62
69
|
return self._service_endpoint
|
ads/telemetry/client.py
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
#!/usr/bin/env python
|
2
|
-
#
|
3
|
-
# Copyright (c) 2024 Oracle and/or its affiliates.
|
2
|
+
# Copyright (c) 2024, 2025 Oracle and/or its affiliates.
|
4
3
|
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
5
4
|
|
6
5
|
|
7
6
|
import logging
|
8
7
|
import threading
|
8
|
+
import traceback
|
9
9
|
import urllib.parse
|
10
|
-
import
|
11
|
-
|
12
|
-
|
10
|
+
from typing import Optional
|
11
|
+
|
12
|
+
import oci
|
13
|
+
|
13
14
|
from ads.config import DEBUG_TELEMETRY
|
14
15
|
|
16
|
+
from .base import TelemetryBase
|
15
17
|
|
16
18
|
logger = logging.getLogger(__name__)
|
17
19
|
|
@@ -32,7 +34,7 @@ class TelemetryClient(TelemetryBase):
|
|
32
34
|
>>> import traceback
|
33
35
|
>>> from ads.telemetry.client import TelemetryClient
|
34
36
|
>>> AQUA_BUCKET = os.environ.get("AQUA_BUCKET", "service-managed-models")
|
35
|
-
>>> AQUA_BUCKET_NS = os.environ.get("AQUA_BUCKET_NS", "
|
37
|
+
>>> AQUA_BUCKET_NS = os.environ.get("AQUA_BUCKET_NS", "namespace")
|
36
38
|
>>> telemetry = TelemetryClient(bucket=AQUA_BUCKET, namespace=AQUA_BUCKET_NS)
|
37
39
|
>>> telemetry.record_event_async(category="aqua/service/model", action="create") # records create action
|
38
40
|
>>> telemetry.record_event_async(category="aqua/service/model/create", action="shape", detail="VM.GPU.A10.1")
|
@@ -45,7 +47,7 @@ class TelemetryClient(TelemetryBase):
|
|
45
47
|
|
46
48
|
def record_event(
|
47
49
|
self, category: str = None, action: str = None, detail: str = None, **kwargs
|
48
|
-
) ->
|
50
|
+
) -> Optional[int]:
|
49
51
|
"""Send a head request to generate an event record.
|
50
52
|
|
51
53
|
Parameters
|
@@ -62,23 +64,41 @@ class TelemetryClient(TelemetryBase):
|
|
62
64
|
|
63
65
|
Returns
|
64
66
|
-------
|
65
|
-
|
67
|
+
int
|
68
|
+
The status code for the telemetry request.
|
69
|
+
200: The the object exists for the telemetry request
|
70
|
+
404: The the object does not exist for the telemetry request.
|
71
|
+
Note that for telemetry purpose, the object does not need to be exist.
|
72
|
+
`None` will be returned if the telemetry request failed.
|
66
73
|
"""
|
67
74
|
try:
|
68
75
|
if not category or not action:
|
69
76
|
raise ValueError("Please specify the category and the action.")
|
70
77
|
if detail:
|
71
78
|
category, action = f"{category}/{action}", detail
|
79
|
+
# Here `endpoint`` is for debugging purpose
|
80
|
+
# For some federated/domain users, the `endpoint` may not be a valid URL
|
72
81
|
endpoint = f"{self.service_endpoint}/n/{self.namespace}/b/{self.bucket}/o/telemetry/{category}/{action}"
|
73
|
-
headers = {"User-Agent": self._encode_user_agent(**kwargs)}
|
74
82
|
logger.debug(f"Sending telemetry to endpoint: {endpoint}")
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
83
|
+
|
84
|
+
self.os_client.base_client.user_agent = self._encode_user_agent(**kwargs)
|
85
|
+
try:
|
86
|
+
response: oci.response.Response = self.os_client.head_object(
|
87
|
+
namespace_name=self.namespace,
|
88
|
+
bucket_name=self.bucket,
|
89
|
+
object_name=f"telemetry/{category}/{action}",
|
90
|
+
)
|
91
|
+
logger.debug(f"Telemetry status: {response.status}")
|
92
|
+
return response.status
|
93
|
+
except oci.exceptions.ServiceError as ex:
|
94
|
+
if ex.status == 404:
|
95
|
+
return ex.status
|
96
|
+
raise ex
|
79
97
|
except Exception as e:
|
80
98
|
if DEBUG_TELEMETRY:
|
81
99
|
logger.error(f"There is an error recording telemetry: {e}")
|
100
|
+
traceback.print_exc()
|
101
|
+
return None
|
82
102
|
|
83
103
|
def record_event_async(
|
84
104
|
self, category: str = None, action: str = None, detail: str = None, **kwargs
|