snowflake-ml-python 1.7.0__py3-none-any.whl → 1.7.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.
Files changed (49) hide show
  1. snowflake/cortex/__init__.py +4 -0
  2. snowflake/cortex/_complete.py +107 -64
  3. snowflake/cortex/_finetune.py +273 -0
  4. snowflake/cortex/_sse_client.py +91 -28
  5. snowflake/cortex/_util.py +30 -1
  6. snowflake/ml/_internal/type_utils.py +3 -3
  7. snowflake/ml/data/__init__.py +5 -0
  8. snowflake/ml/model/_client/model/model_version_impl.py +7 -7
  9. snowflake/ml/model/_client/ops/model_ops.py +51 -30
  10. snowflake/ml/model/_client/ops/service_ops.py +13 -2
  11. snowflake/ml/model/_client/sql/model.py +0 -14
  12. snowflake/ml/model/_client/sql/service.py +25 -1
  13. snowflake/ml/model/_model_composer/model_method/infer_function.py_template +2 -1
  14. snowflake/ml/model/_packager/model_env/model_env.py +12 -0
  15. snowflake/ml/model/_packager/model_handlers/_utils.py +1 -1
  16. snowflake/ml/model/_packager/model_handlers/catboost.py +1 -1
  17. snowflake/ml/model/_packager/model_handlers/custom.py +3 -1
  18. snowflake/ml/model/_packager/model_handlers/lightgbm.py +2 -1
  19. snowflake/ml/model/_packager/model_handlers/sklearn.py +48 -1
  20. snowflake/ml/model/_packager/model_handlers/snowmlmodel.py +1 -1
  21. snowflake/ml/model/_packager/model_handlers/tensorflow.py +23 -6
  22. snowflake/ml/model/_packager/model_handlers/torchscript.py +14 -14
  23. snowflake/ml/model/_packager/model_meta/_packaging_requirements.py +2 -3
  24. snowflake/ml/model/_packager/model_meta/model_meta_schema.py +5 -0
  25. snowflake/ml/model/_packager/model_runtime/_snowml_inference_alternative_requirements.py +2 -10
  26. snowflake/ml/model/_packager/model_runtime/model_runtime.py +4 -9
  27. snowflake/ml/model/_packager/model_task/model_task_utils.py +1 -1
  28. snowflake/ml/model/_signatures/core.py +63 -16
  29. snowflake/ml/model/_signatures/pandas_handler.py +71 -27
  30. snowflake/ml/model/_signatures/pytorch_handler.py +2 -2
  31. snowflake/ml/model/_signatures/snowpark_handler.py +2 -1
  32. snowflake/ml/model/_signatures/tensorflow_handler.py +2 -2
  33. snowflake/ml/model/_signatures/utils.py +4 -0
  34. snowflake/ml/model/model_signature.py +38 -9
  35. snowflake/ml/model/type_hints.py +1 -1
  36. snowflake/ml/modeling/lightgbm/lgbm_classifier.py +2 -4
  37. snowflake/ml/modeling/lightgbm/lgbm_regressor.py +2 -4
  38. snowflake/ml/monitoring/_client/model_monitor_sql_client.py +158 -1045
  39. snowflake/ml/monitoring/_manager/model_monitor_manager.py +106 -230
  40. snowflake/ml/monitoring/entities/model_monitor_config.py +10 -10
  41. snowflake/ml/monitoring/model_monitor.py +7 -96
  42. snowflake/ml/registry/registry.py +17 -29
  43. snowflake/ml/version.py +1 -1
  44. {snowflake_ml_python-1.7.0.dist-info → snowflake_ml_python-1.7.1.dist-info}/METADATA +31 -5
  45. {snowflake_ml_python-1.7.0.dist-info → snowflake_ml_python-1.7.1.dist-info}/RECORD +48 -47
  46. {snowflake_ml_python-1.7.0.dist-info → snowflake_ml_python-1.7.1.dist-info}/WHEEL +1 -1
  47. snowflake/ml/monitoring/entities/model_monitor_interval.py +0 -46
  48. {snowflake_ml_python-1.7.0.dist-info → snowflake_ml_python-1.7.1.dist-info}/LICENSE.txt +0 -0
  49. {snowflake_ml_python-1.7.0.dist-info → snowflake_ml_python-1.7.1.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from snowflake.cortex._complete import Complete, CompleteOptions
3
3
  from snowflake.cortex._embed_text_768 import EmbedText768
4
4
  from snowflake.cortex._embed_text_1024 import EmbedText1024
5
5
  from snowflake.cortex._extract_answer import ExtractAnswer
6
+ from snowflake.cortex._finetune import Finetune, FinetuneJob, FinetuneStatus
6
7
  from snowflake.cortex._sentiment import Sentiment
7
8
  from snowflake.cortex._summarize import Summarize
8
9
  from snowflake.cortex._translate import Translate
@@ -14,6 +15,9 @@ __all__ = [
14
15
  "EmbedText768",
15
16
  "EmbedText1024",
16
17
  "ExtractAnswer",
18
+ "Finetune",
19
+ "FinetuneJob",
20
+ "FinetuneStatus",
17
21
  "Sentiment",
18
22
  "Summarize",
19
23
  "Translate",
@@ -1,7 +1,8 @@
1
1
  import json
2
2
  import logging
3
3
  import time
4
- from typing import Any, Callable, Iterator, List, Optional, TypedDict, Union, cast
4
+ from io import BytesIO
5
+ from typing import Any, Callable, Dict, Iterator, List, Optional, TypedDict, Union, cast
5
6
  from urllib.parse import urlunparse
6
7
 
7
8
  import requests
@@ -16,8 +17,10 @@ from snowflake.cortex._util import (
16
17
  )
17
18
  from snowflake.ml._internal import telemetry
18
19
  from snowflake.snowpark import context, functions
20
+ from snowflake.snowpark._internal.utils import is_in_stored_procedure
19
21
 
20
22
  logger = logging.getLogger(__name__)
23
+ _REST_COMPLETE_URL = "/api/v2/cortex/inference:complete"
21
24
 
22
25
 
23
26
  class ConversationMessage(TypedDict):
@@ -84,6 +87,76 @@ def retry(func: Callable[..., requests.Response]) -> Callable[..., requests.Resp
84
87
  return inner
85
88
 
86
89
 
90
+ def _make_common_request_headers() -> Dict[str, str]:
91
+ headers = {
92
+ "Content-Type": "application/json",
93
+ "Accept": "application/json, text/event-stream",
94
+ }
95
+ return headers
96
+
97
+
98
+ def _make_request_body(
99
+ model: str,
100
+ prompt: Union[str, List[ConversationMessage]],
101
+ options: Optional[CompleteOptions] = None,
102
+ ) -> Dict[str, Any]:
103
+ data = {
104
+ "model": model,
105
+ "stream": True,
106
+ }
107
+ if isinstance(prompt, List):
108
+ data["messages"] = prompt
109
+ else:
110
+ data["messages"] = [{"content": prompt}]
111
+
112
+ if options:
113
+ if "max_tokens" in options:
114
+ data["max_tokens"] = options["max_tokens"]
115
+ data["max_output_tokens"] = options["max_tokens"]
116
+ if "temperature" in options:
117
+ data["temperature"] = options["temperature"]
118
+ if "top_p" in options:
119
+ data["top_p"] = options["top_p"]
120
+ return data
121
+
122
+
123
+ # XP endpoint returns a dict response which needs to be converted to a format which can
124
+ # be consumed by the SSEClient. This method does that.
125
+ def _xp_dict_to_response(raw_resp: Dict[str, Any]) -> requests.Response:
126
+ response = requests.Response()
127
+ response.status_code = int(raw_resp["status"])
128
+ response.headers = raw_resp["headers"]
129
+
130
+ data = raw_resp["content"]
131
+ data = json.loads(data)
132
+ # Convert the dictionary to a string format that resembles the SSE event format
133
+ # For example, if the dict is {'event': 'message', 'data': 'your data'}, it should be formatted like this:
134
+ sse_format_data = ""
135
+ for event in data:
136
+ event_type = event.get("event", "message")
137
+ event_data = event.get("data", "")
138
+ event_data = json.dumps(event_data)
139
+ sse_format_data += f"event: {event_type}\ndata: {event_data}\n\n" # Add each event with new lines
140
+
141
+ response.raw = BytesIO(sse_format_data.encode("utf-8"))
142
+ return response
143
+
144
+
145
+ @retry
146
+ def _call_complete_xp(
147
+ model: str,
148
+ prompt: Union[str, List[ConversationMessage]],
149
+ options: Optional[CompleteOptions] = None,
150
+ deadline: Optional[float] = None,
151
+ ) -> requests.Response:
152
+ headers = _make_common_request_headers()
153
+ body = _make_request_body(model, prompt, options)
154
+ import _snowflake
155
+
156
+ raw_resp = _snowflake.send_snow_api_request("POST", _REST_COMPLETE_URL, {}, headers, body, {}, deadline)
157
+ return _xp_dict_to_response(raw_resp)
158
+
159
+
87
160
  @retry
88
161
  def _call_complete_rest(
89
162
  model: str,
@@ -110,36 +183,16 @@ def _call_complete_rest(
110
183
  scheme = "https"
111
184
  if hasattr(session.connection, "scheme"):
112
185
  scheme = session.connection.scheme
113
- url = urlunparse((scheme, session.connection.host, "api/v2/cortex/inference:complete", "", "", ""))
186
+ url = urlunparse((scheme, session.connection.host, _REST_COMPLETE_URL, "", "", ""))
114
187
 
115
- headers = {
116
- "Content-Type": "application/json",
117
- "Authorization": f'Snowflake Token="{session.connection.rest.token}"',
118
- "Accept": "application/json, text/event-stream",
119
- }
120
-
121
- data = {
122
- "model": model,
123
- "stream": True,
124
- }
125
- if isinstance(prompt, List):
126
- data["messages"] = prompt
127
- else:
128
- data["messages"] = [{"content": prompt}]
129
-
130
- if options:
131
- if "max_tokens" in options:
132
- data["max_tokens"] = options["max_tokens"]
133
- data["max_output_tokens"] = options["max_tokens"]
134
- if "temperature" in options:
135
- data["temperature"] = options["temperature"]
136
- if "top_p" in options:
137
- data["top_p"] = options["top_p"]
188
+ headers = _make_common_request_headers()
189
+ headers["Authorization"] = f'Snowflake Token="{session.connection.rest.token}"'
138
190
 
191
+ body = _make_request_body(model, prompt, options)
139
192
  logger.debug(f"making POST request to {url} (model={model})")
140
193
  return requests.post(
141
194
  url,
142
- json=data,
195
+ json=body,
143
196
  headers=headers,
144
197
  stream=True,
145
198
  )
@@ -164,49 +217,24 @@ def _complete_call_sql_function_snowpark(
164
217
  return cast(snowpark.Column, functions.builtin(function)(*args))
165
218
 
166
219
 
167
- def _complete_call_sql_function_immediate(
168
- function: str,
220
+ def _complete_non_streaming_immediate(
169
221
  model: str,
170
222
  prompt: Union[str, List[ConversationMessage]],
171
223
  options: Optional[CompleteOptions],
172
- session: Optional[snowpark.Session],
224
+ session: Optional[snowpark.Session] = None,
225
+ deadline: Optional[float] = None,
173
226
  ) -> str:
174
- session = session or context.get_active_session()
175
- if session is None:
176
- raise SnowflakeAuthenticationException(
177
- """Session required. Provide the session through a session=... argument or ensure an active session is
178
- available in your environment."""
179
- )
227
+ response = _complete_rest(model=model, prompt=prompt, options=options, session=session, deadline=deadline)
228
+ return "".join(response)
180
229
 
181
- # https://docs.snowflake.com/en/sql-reference/functions/complete-snowflake-cortex
182
- if options is not None or not isinstance(prompt, str):
183
- if isinstance(prompt, List):
184
- prompt_arg = prompt
185
- else:
186
- prompt_arg = [{"role": "user", "content": prompt}]
187
- options = options or {}
188
- lit_args = [
189
- functions.lit(model),
190
- functions.lit(prompt_arg),
191
- functions.lit(options),
192
- ]
193
- else:
194
- lit_args = [
195
- functions.lit(model),
196
- functions.lit(prompt),
197
- ]
198
-
199
- empty_df = session.create_dataframe([snowpark.Row()])
200
- df = empty_df.select(functions.builtin(function)(*lit_args))
201
- return cast(str, df.collect()[0][0])
202
230
 
203
-
204
- def _complete_sql_impl(
231
+ def _complete_non_streaming_impl(
205
232
  function: str,
206
233
  model: Union[str, snowpark.Column],
207
234
  prompt: Union[str, List[ConversationMessage], snowpark.Column],
208
235
  options: Optional[Union[CompleteOptions, snowpark.Column]],
209
- session: Optional[snowpark.Session],
236
+ session: Optional[snowpark.Session] = None,
237
+ deadline: Optional[float] = None,
210
238
  ) -> Union[str, snowpark.Column]:
211
239
  if isinstance(prompt, snowpark.Column):
212
240
  if options is not None:
@@ -217,7 +245,24 @@ def _complete_sql_impl(
217
245
  raise ValueError("'model' cannot be a snowpark.Column when 'prompt' is a string.")
218
246
  if isinstance(options, snowpark.Column):
219
247
  raise ValueError("'options' cannot be a snowpark.Column when 'prompt' is a string.")
220
- return _complete_call_sql_function_immediate(function, model, prompt, options, session)
248
+ return _complete_non_streaming_immediate(
249
+ model=model, prompt=prompt, options=options, session=session, deadline=deadline
250
+ )
251
+
252
+
253
+ def _complete_rest(
254
+ model: str,
255
+ prompt: Union[str, List[ConversationMessage]],
256
+ options: Optional[CompleteOptions] = None,
257
+ session: Optional[snowpark.Session] = None,
258
+ deadline: Optional[float] = None,
259
+ ) -> Iterator[str]:
260
+ if is_in_stored_procedure(): # type: ignore[no-untyped-call]
261
+ response = _call_complete_xp(model=model, prompt=prompt, options=options, deadline=deadline)
262
+ else:
263
+ response = _call_complete_rest(model=model, prompt=prompt, options=options, session=session, deadline=deadline)
264
+ assert response.status_code >= 200 and response.status_code < 300
265
+ return _return_stream_response(response, deadline)
221
266
 
222
267
 
223
268
  def _complete_impl(
@@ -239,10 +284,8 @@ def _complete_impl(
239
284
  raise ValueError("in REST mode, 'model' must be a string")
240
285
  if not isinstance(prompt, str) and not isinstance(prompt, List):
241
286
  raise ValueError("in REST mode, 'prompt' must be a string or a list of ConversationMessage")
242
- response = _call_complete_rest(model, prompt, options, session=session, deadline=deadline)
243
- assert response.status_code >= 200 and response.status_code < 300
244
- return _return_stream_response(response, deadline)
245
- return _complete_sql_impl(function, model, prompt, options, session)
287
+ return _complete_rest(model=model, prompt=prompt, options=options, session=session, deadline=deadline)
288
+ return _complete_non_streaming_impl(function, model, prompt, options, session, deadline)
246
289
 
247
290
 
248
291
  @telemetry.send_api_usage_telemetry(
@@ -0,0 +1,273 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from typing import Any, Dict, List, Optional, Union, cast
4
+
5
+ from snowflake import snowpark
6
+ from snowflake.cortex._util import (
7
+ CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
8
+ call_sql_function_literals,
9
+ )
10
+ from snowflake.ml._internal import telemetry
11
+ from snowflake.ml._internal.utils import snowpark_dataframe_utils
12
+
13
+ _CORTEX_FINETUNE_SYSTEM_FUNCTION_NAME = "SNOWFLAKE.CORTEX.FINETUNE"
14
+ CORTEX_FINETUNE_TELEMETRY_SUBPROJECT = "FINETUNE"
15
+ CORTEX_FINETUNE_FIRST_VERSION = "1.7.0"
16
+ CORTEX_FINETUNE_DOCUMENTATION_URL = "https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-finetuning"
17
+
18
+
19
+ class FinetuneError(Exception):
20
+ def __init__(self, message: str, original_exception: Optional[Exception] = None) -> None:
21
+ """Finetuning Exception Class.
22
+
23
+ Args:
24
+ message: Error message to be reported.
25
+ original_exception: Original exception. This is the exception raised to users by telemetry.
26
+
27
+ Attributes:
28
+ original_exception: Original exception with an error code in its message.
29
+ """
30
+ self.original_exception = original_exception
31
+ self._pretty_msg = message + repr(self.original_exception) if self.original_exception is not None else ""
32
+
33
+ def __repr__(self) -> str:
34
+ return f"{self.__class__.__name__}({self._pretty_msg!r})"
35
+
36
+ def __str__(self) -> str:
37
+ return self._pretty_msg
38
+
39
+
40
+ @dataclass
41
+ class FinetuneStatus:
42
+ """Fine-tuning job status."""
43
+
44
+ id: Optional[str] = None
45
+ """Workflow ID for the fine-tuning run."""
46
+
47
+ status: Optional[str] = None
48
+ """Status string, e.g. PENDING, RUNNING, SUCCESS, ERROR, CANCELLED."""
49
+
50
+ base_model: Optional[str] = None
51
+ """Name of the base model that is being fine-tuned."""
52
+
53
+ created_on: Optional[int] = None
54
+ """Creation timestamp of the Fine-tuning job in milliseconds."""
55
+
56
+ error: Optional[Dict[str, Any]] = None
57
+ """Error message propagated from the job."""
58
+
59
+ finished_on: Optional[int] = None
60
+ """Completion timestamp of the Fine-tuning job in milliseconds."""
61
+
62
+ progress: Optional[float] = None
63
+ """Progress made as a fraction of total [0.0,1.0]."""
64
+
65
+ training_result: Optional[List[Dict[str, Any]]] = None
66
+ """Detailed metrics report for a completed training."""
67
+
68
+ trained_tokens: Optional[int] = None
69
+ """Number of tokens trained on. If multiple epochs are run, this can be larger than number of tokens in the
70
+ training data."""
71
+
72
+ training_data: Optional[str] = None
73
+ """Training data query."""
74
+
75
+ validation_data: Optional[str] = None
76
+ """Validation data query."""
77
+
78
+ model: Optional[str] = None
79
+ """Location of the fine-tuned model."""
80
+
81
+
82
+ class FinetuneJob:
83
+ def __init__(self, session: Optional[snowpark.Session], status: FinetuneStatus) -> None:
84
+ """Fine-tuning Job.
85
+
86
+ Args:
87
+ session: Snowpark session to use to communicate with Snowflake.
88
+ status: FinetuneStatus for this job.
89
+ """
90
+ self._session = session
91
+ self.status = status
92
+
93
+ def __repr__(self) -> str:
94
+ return self.status.__repr__()
95
+
96
+ def __eq__(self, other: Any) -> bool:
97
+ if not isinstance(other, FinetuneJob):
98
+ raise NotImplementedError(
99
+ f"Equality comparison of FinetuneJob with objects of type {type(other)} is not implemented."
100
+ )
101
+ return self.status == other.status
102
+
103
+ @snowpark._internal.utils.experimental(version=CORTEX_FINETUNE_FIRST_VERSION)
104
+ @telemetry.send_api_usage_telemetry(
105
+ project=CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
106
+ subproject=CORTEX_FINETUNE_TELEMETRY_SUBPROJECT,
107
+ )
108
+ def cancel(self) -> bool:
109
+ """Cancel a fine-tuning run.
110
+
111
+ No confirmation will be required.
112
+
113
+ [Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-finetuning)
114
+
115
+ Args:
116
+
117
+ Returns:
118
+ True if the cancellation was successful, False otherwise.
119
+ """
120
+ result = _finetune_impl(operation="CANCEL", session=self._session, function_args=[self.status.id])
121
+ return result is not None and isinstance(result, str) and result.startswith("Canceled Cortex Fine-tuning job: ")
122
+
123
+ @snowpark._internal.utils.experimental(version=CORTEX_FINETUNE_FIRST_VERSION)
124
+ @telemetry.send_api_usage_telemetry(
125
+ project=CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
126
+ subproject=CORTEX_FINETUNE_TELEMETRY_SUBPROJECT,
127
+ )
128
+ def describe(self) -> FinetuneStatus:
129
+ """Describe a fine-tuning run.
130
+
131
+ Args:
132
+
133
+ Returns:
134
+ FinetuneStatus containing of attributes of the fine-tuning run.
135
+ """
136
+ result_string = _finetune_impl(operation="DESCRIBE", session=self._session, function_args=[self.status.id])
137
+
138
+ result = FinetuneStatus(**cast(Dict[str, Any], _try_load_json(result_string)))
139
+ return result
140
+
141
+
142
+ class Finetune:
143
+ @snowpark._internal.utils.experimental(version=CORTEX_FINETUNE_FIRST_VERSION)
144
+ @telemetry.send_api_usage_telemetry(
145
+ project=CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
146
+ subproject=CORTEX_FINETUNE_TELEMETRY_SUBPROJECT,
147
+ )
148
+ def __init__(self, session: Optional[snowpark.Session] = None) -> None:
149
+ """Cortex Fine-Tuning API.
150
+
151
+ [Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-finetuning)
152
+
153
+ Args:
154
+ session: Snowpark session to be used. If none is given, we will attempt to
155
+ use the currently active session.
156
+ """
157
+ self._session = session
158
+
159
+ @snowpark._internal.utils.experimental(version=CORTEX_FINETUNE_FIRST_VERSION)
160
+ @telemetry.send_api_usage_telemetry(
161
+ project=CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
162
+ subproject=CORTEX_FINETUNE_TELEMETRY_SUBPROJECT,
163
+ )
164
+ def create(
165
+ self,
166
+ name: str,
167
+ base_model: str,
168
+ training_data: Union[str, snowpark.DataFrame],
169
+ validation_data: Optional[Union[str, snowpark.DataFrame]] = None,
170
+ options: Optional[Dict[str, Any]] = None,
171
+ ) -> FinetuneJob:
172
+ """Create a new fine-tuning runs.
173
+
174
+ The expected format of training and validation data is two fields or columns:
175
+ "prompt": the input to the model and
176
+ "completion": the output that the model is expected to generate.
177
+
178
+ Both data parameters "training_data" and "validation_data" expect to be one of
179
+ (1) stage path to JSONL-formatted data,
180
+ (2) select-query string resulting in a table,
181
+ (3) Snowpark DataFrame containing the data
182
+
183
+ [Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-finetuning)
184
+
185
+ Args:
186
+ name: Name of the resulting fine-tuned model.
187
+ base_model: The name of the base model to start fine-tuning from.
188
+ training_data: Data used for fine-tuning the model.
189
+ validation_data: Data used for validating the fine-tuned model (not used in training)
190
+ options: Dictionary of additional options to be passed to the training procedure.
191
+ Please refer to the official documentation for a list of available options.
192
+
193
+ Returns:
194
+ The identifier of the fine-tuning run.
195
+
196
+ Raises:
197
+ ValueError: If the Snowpark DataFrame used is incompatible with this API.
198
+ This can happen if the DataFrame contains multiple queries.
199
+ """
200
+
201
+ # Handle data provided as snowpark dataframes
202
+ if isinstance(training_data, snowpark.DataFrame):
203
+ if snowpark_dataframe_utils.is_single_query_snowpark_dataframe(training_data):
204
+ training_string = str(training_data.queries["queries"][0])
205
+ else:
206
+ raise ValueError(
207
+ "Snowpark DataFrame given in 'training_data' contains "
208
+ + f'{training_data.queries["queries"]} queries and '
209
+ + f'{training_data.queries["post_actions"]} post_actions. It needs '
210
+ "to contain exactly one query and no post_actions."
211
+ )
212
+ else:
213
+ training_string = training_data
214
+
215
+ validation_string: Optional[str] = None
216
+ if isinstance(validation_data, snowpark.DataFrame):
217
+ if snowpark_dataframe_utils.is_single_query_snowpark_dataframe(validation_data):
218
+ validation_string = str(validation_data.queries["queries"][0])
219
+ else:
220
+ raise ValueError(
221
+ "Snowpark DataFrame given in 'validation_data' contains "
222
+ + f'{validation_data.queries["queries"]} queries and '
223
+ + f'{validation_data.queries["post_actions"]} post_actions. It needs '
224
+ "to contain exactly one query and no post_actions."
225
+ )
226
+ else:
227
+ validation_string = validation_data
228
+
229
+ result = _finetune_impl(
230
+ operation="CREATE",
231
+ session=self._session,
232
+ function_args=[name, base_model, training_string, validation_string, options],
233
+ )
234
+ finetune_status = FinetuneStatus(id=result)
235
+ finetune_run = FinetuneJob(self._session, finetune_status)
236
+ return finetune_run
237
+
238
+ @snowpark._internal.utils.experimental(version=CORTEX_FINETUNE_FIRST_VERSION)
239
+ @telemetry.send_api_usage_telemetry(
240
+ project=CORTEX_FUNCTIONS_TELEMETRY_PROJECT,
241
+ subproject=CORTEX_FINETUNE_TELEMETRY_SUBPROJECT,
242
+ )
243
+ def list_jobs(self) -> List["FinetuneJob"]:
244
+ """Show current and past fine-tuning runs.
245
+
246
+ Returns:
247
+ List of dictionaries of attributes of the fine-tuning runs. Please refer to the official documentation for a
248
+ list of expected fields.
249
+ """
250
+ result_string = _finetune_impl(operation="SHOW", session=self._session, function_args=[])
251
+ result = _try_load_json(result_string)
252
+
253
+ return [FinetuneJob(session=self._session, status=FinetuneStatus(**run_status)) for run_status in result]
254
+
255
+
256
+ def _try_load_json(json_string: str) -> Union[Dict[Any, Any], List[Any]]:
257
+ try:
258
+ result = json.loads(str(json_string))
259
+ except json.JSONDecodeError as e:
260
+ message = f"""Unable to parse JSON from: "{json_string}". """
261
+ raise FinetuneError(message=message, original_exception=e)
262
+ except Exception as e:
263
+ message = f"""Unable to parse JSON from: "{json_string}". """
264
+ raise FinetuneError(message=message, original_exception=e)
265
+ else:
266
+ if not isinstance(result, dict) and not isinstance(result, list):
267
+ message = f"""Unable to parse JSON from: "{json_string}". Result was not a dictionary."""
268
+ raise FinetuneError(message=message)
269
+ return result
270
+
271
+
272
+ def _finetune_impl(operation: str, session: Optional[snowpark.Session], function_args: List[Any]) -> str:
273
+ return call_sql_function_literals(_CORTEX_FINETUNE_SYSTEM_FUNCTION_NAME, session, operation, *function_args)