salesforce-data-customcode 6.0.2.dev1__tar.gz → 6.0.3__tar.gz
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.
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/PKG-INFO +6 -3
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/README.md +4 -2
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/pyproject.toml +2 -1
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cli.py +6 -3
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/client.py +23 -21
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/deploy.py +109 -19
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/base.py +11 -1
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/__init__.py +2 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/default.py +0 -2
- salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/errors.py +36 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/spark_base.py +10 -3
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/spark_default.py +81 -16
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -7
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -6
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +1 -1
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +1 -1
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/payload/entrypoint.py +5 -4
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/token_provider.py +70 -36
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/LICENSE.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/auth.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cmd.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/common_config.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.yaml +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/constants.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/credentials.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_client.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_config.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/types.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions_config.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/default.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/chunking.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/runtime.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function_utils.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/query_api.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/sf_cli.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/utils.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/csv.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/print.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway_config.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/mixin.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/py.typed +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/run.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/scan.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/base.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/default.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/template.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/__init__.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/README.md +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/README.md +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/account.ipynb +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/payload/config.json +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements.txt +0 -0
- {salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: salesforce-data-customcode
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.3
|
|
4
4
|
Summary: Data Cloud Custom Code SDK
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -15,6 +15,7 @@ Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
|
15
15
|
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
16
16
|
Requires-Dist: numpy
|
|
17
17
|
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: pipreqs
|
|
18
19
|
Requires-Dist: pydantic (==2.13.1)
|
|
19
20
|
Requires-Dist: pyspark (==3.5.1)
|
|
20
21
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
@@ -341,14 +342,16 @@ from datacustomcode.client import Client, llm_gateway_generate_text_col
|
|
|
341
342
|
def main():
|
|
342
343
|
client = Client()
|
|
343
344
|
df = client.read_dlo("Input__dll")
|
|
345
|
+
# llm_gateway_generate_text_col returns a struct
|
|
346
|
+
# {status, response, error_code, error_message} per row, so per-row
|
|
347
|
+
# failures don't abort the Spark job. Pick the field you want with [].
|
|
344
348
|
df_generated = df.withColumn(
|
|
345
349
|
"greeting__c",
|
|
346
350
|
llm_gateway_generate_text_col(
|
|
347
351
|
"In one sentence, greet {name} from {city}.",
|
|
348
352
|
{"name": col("name__c"), "city": col("homecity__c")},
|
|
349
353
|
model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
|
|
350
|
-
|
|
351
|
-
),
|
|
354
|
+
)["response"],
|
|
352
355
|
)
|
|
353
356
|
|
|
354
357
|
dlo_name = "Output_dll"
|
|
@@ -316,14 +316,16 @@ from datacustomcode.client import Client, llm_gateway_generate_text_col
|
|
|
316
316
|
def main():
|
|
317
317
|
client = Client()
|
|
318
318
|
df = client.read_dlo("Input__dll")
|
|
319
|
+
# llm_gateway_generate_text_col returns a struct
|
|
320
|
+
# {status, response, error_code, error_message} per row, so per-row
|
|
321
|
+
# failures don't abort the Spark job. Pick the field you want with [].
|
|
319
322
|
df_generated = df.withColumn(
|
|
320
323
|
"greeting__c",
|
|
321
324
|
llm_gateway_generate_text_col(
|
|
322
325
|
"In one sentence, greet {name} from {city}.",
|
|
323
326
|
{"name": col("name__c"), "city": col("homecity__c")},
|
|
324
327
|
model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
|
|
325
|
-
|
|
326
|
-
),
|
|
328
|
+
)["response"],
|
|
327
329
|
)
|
|
328
330
|
|
|
329
331
|
dlo_name = "Output_dll"
|
|
@@ -18,7 +18,7 @@ license = "Apache-2.0"
|
|
|
18
18
|
name = "salesforce-data-customcode"
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
requires-python = ">=3.10,<3.12"
|
|
21
|
-
version = "6.0.
|
|
21
|
+
version = "6.0.3"
|
|
22
22
|
|
|
23
23
|
[tool.black]
|
|
24
24
|
exclude = '''
|
|
@@ -101,6 +101,7 @@ click = "^8.1.8"
|
|
|
101
101
|
loguru = "^0.7.3"
|
|
102
102
|
numpy = "*"
|
|
103
103
|
pandas = "*"
|
|
104
|
+
pipreqs = "*"
|
|
104
105
|
pydantic = "2.13.1"
|
|
105
106
|
pyspark = "3.5.1"
|
|
106
107
|
python = ">=3.10,<3.12"
|
{salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cli.py
RENAMED
|
@@ -198,6 +198,7 @@ def zip(path: str, network: str):
|
|
|
198
198
|
default=None,
|
|
199
199
|
help="SF CLI org alias or username. Fetches credentials via `sf org display`.",
|
|
200
200
|
)
|
|
201
|
+
@click.option("--use-in-feature", default=None, hidden=True, deprecated=True)
|
|
201
202
|
def deploy(
|
|
202
203
|
path: str,
|
|
203
204
|
name: str,
|
|
@@ -207,6 +208,7 @@ def deploy(
|
|
|
207
208
|
profile: str,
|
|
208
209
|
network: str,
|
|
209
210
|
sf_cli_org: Optional[str],
|
|
211
|
+
use_in_feature: Optional[str],
|
|
210
212
|
):
|
|
211
213
|
from datacustomcode.constants import USE_IN_FEATURE_MAPPING_FOR_CONNECT_API
|
|
212
214
|
from datacustomcode.deploy import (
|
|
@@ -249,13 +251,14 @@ def deploy(
|
|
|
249
251
|
logger.info(f"Inferred use_in_feature: {use_in_feature}")
|
|
250
252
|
else:
|
|
251
253
|
click.secho(
|
|
252
|
-
"Error:
|
|
253
|
-
"
|
|
254
|
+
"Error: Function signature does not match a supported type. "
|
|
255
|
+
"Use SearchIndexChunkingV1Request and "
|
|
256
|
+
"SearchIndexChunkingV1Response in function signature.",
|
|
254
257
|
fg="red",
|
|
255
258
|
)
|
|
256
259
|
raise click.Abort()
|
|
257
260
|
|
|
258
|
-
# Map
|
|
261
|
+
# Map feature names to Connect API names
|
|
259
262
|
mapped_feature = USE_IN_FEATURE_MAPPING_FOR_CONNECT_API.get(
|
|
260
263
|
use_in_feature, use_in_feature
|
|
261
264
|
)
|
|
@@ -59,20 +59,26 @@ def llm_gateway_generate_text_col(
|
|
|
59
59
|
template: str,
|
|
60
60
|
values: Union[Dict[str, "Column"], "Column"],
|
|
61
61
|
model_id: Optional[str] = None,
|
|
62
|
-
max_tokens: Optional[int] = None,
|
|
63
62
|
) -> "Column":
|
|
64
63
|
"""Build a Spark Column that runs the LLM Gateway per row.
|
|
65
64
|
|
|
65
|
+
The returned Column yields a struct ``{status, response, error_code,
|
|
66
|
+
error_message}`` for each row. Use ``[...]`` (or ``getField``) to pick the
|
|
67
|
+
field you want, e.g. ``llm_gateway_generate_text_col(...)["response"]``.
|
|
68
|
+
Per-row failures populate ``status`` / ``error_code`` / ``error_message``
|
|
69
|
+
so a single bad row does not abort the whole Spark job.
|
|
70
|
+
|
|
66
71
|
Example:
|
|
67
72
|
|
|
68
|
-
>>>
|
|
69
|
-
... "
|
|
70
|
-
...
|
|
71
|
-
...
|
|
72
|
-
...
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
>>> result = llm_gateway_generate_text_col(
|
|
74
|
+
... "In one sentence, greet {name} from {city}.",
|
|
75
|
+
... {"name": col("name__c"), "city": col("homecity__c")},
|
|
76
|
+
... model_id="sfdc_ai__DefaultGPT4Omni",
|
|
77
|
+
... )
|
|
78
|
+
>>> df.withColumn("greeting__c", result["response"])
|
|
79
|
+
>>> # …or keep the struct around and inspect failures:
|
|
80
|
+
>>> df.withColumn("llm", result).select(
|
|
81
|
+
... "llm.status", "llm.response", "llm.error_message"
|
|
76
82
|
... )
|
|
77
83
|
|
|
78
84
|
Args:
|
|
@@ -81,15 +87,16 @@ def llm_gateway_generate_text_col(
|
|
|
81
87
|
values: Either a mapping from placeholder name to Spark ``Column``, or
|
|
82
88
|
a single ``Column`` whose value is already a struct.
|
|
83
89
|
model_id: LLM model id. Defaults to ``sfdc_ai__DefaultGPT4Omni``.
|
|
84
|
-
max_tokens: Maximum tokens to generate. Defaults to 200.
|
|
85
90
|
|
|
86
91
|
Returns:
|
|
87
|
-
A Spark ``Column``
|
|
92
|
+
A Spark ``Column`` of ``StructType`` with fields ``status``,
|
|
93
|
+
``response``, ``error_code``, and ``error_message`` (all nullable
|
|
94
|
+
strings). On success, ``status == "SUCCESS"`` and ``response`` holds
|
|
95
|
+
the generated text; on failure, ``status == "ERROR"`` and the
|
|
96
|
+
``error_*`` fields carry diagnostic detail.
|
|
88
97
|
"""
|
|
89
98
|
gateway = Client()._get_spark_llm_gateway()
|
|
90
|
-
return gateway.llm_gateway_generate_text_col(
|
|
91
|
-
template, values, model_id=model_id, max_tokens=max_tokens
|
|
92
|
-
)
|
|
99
|
+
return gateway.llm_gateway_generate_text_col(template, values, model_id=model_id)
|
|
93
100
|
|
|
94
101
|
|
|
95
102
|
class DataCloudObjectType(Enum):
|
|
@@ -150,9 +157,7 @@ class Client:
|
|
|
150
157
|
finder: Find a file path
|
|
151
158
|
reader: A custom reader to use for reading Data Cloud objects.
|
|
152
159
|
writer: A custom writer to use for writing Data Cloud objects.
|
|
153
|
-
spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
|
|
154
|
-
omitted, the gateway is lazily resolved from
|
|
155
|
-
``spark_llm_gateway_config``.
|
|
160
|
+
spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
|
|
156
161
|
|
|
157
162
|
Example:
|
|
158
163
|
>>> client = Client()
|
|
@@ -292,7 +297,6 @@ class Client:
|
|
|
292
297
|
self,
|
|
293
298
|
prompt: str,
|
|
294
299
|
model_id: Optional[str] = None,
|
|
295
|
-
max_tokens: Optional[int] = None,
|
|
296
300
|
) -> str:
|
|
297
301
|
"""Issue a one-shot LLM Gateway call. This is the scalar counterpart to
|
|
298
302
|
:func:`llm_gateway_generate_text_col`: it runs **once** — not per row.
|
|
@@ -310,15 +314,13 @@ class Client:
|
|
|
310
314
|
``{field}`` substitution is performed on this string.
|
|
311
315
|
model_id: LLM model id to target. Defaults to
|
|
312
316
|
``sfdc_ai__DefaultGPT4Omni`` when ``None``.
|
|
313
|
-
max_tokens: Hard upper bound on the number of tokens the model
|
|
314
|
-
may generate. Defaults to 200 when ``None``.
|
|
315
317
|
|
|
316
318
|
Returns:
|
|
317
319
|
The generated text as a plain Python ``str``; empty when the
|
|
318
320
|
gateway response carries no generated text.
|
|
319
321
|
"""
|
|
320
322
|
return self._get_spark_llm_gateway().llm_gateway_generate_text(
|
|
321
|
-
prompt, model_id=model_id
|
|
323
|
+
prompt, model_id=model_id
|
|
322
324
|
)
|
|
323
325
|
|
|
324
326
|
def _get_spark_llm_gateway(self) -> SparkLLMGateway:
|
|
@@ -26,12 +26,13 @@ from typing import (
|
|
|
26
26
|
Callable,
|
|
27
27
|
Dict,
|
|
28
28
|
List,
|
|
29
|
+
Optional,
|
|
29
30
|
Union,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
from loguru import logger
|
|
33
34
|
import pydantic
|
|
34
|
-
from pydantic import BaseModel
|
|
35
|
+
from pydantic import BaseModel, model_validator
|
|
35
36
|
import requests
|
|
36
37
|
|
|
37
38
|
from datacustomcode.cmd import cmd_output
|
|
@@ -366,23 +367,86 @@ class BaseConfig(BaseModel):
|
|
|
366
367
|
entryPoint: str
|
|
367
368
|
|
|
368
369
|
|
|
370
|
+
class DataObjectField(BaseModel):
|
|
371
|
+
name: str
|
|
372
|
+
label: str
|
|
373
|
+
dataType: str
|
|
374
|
+
isPrimaryKey: bool = False
|
|
375
|
+
keyQualifierFieldName: Optional[str] = None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class DataObject(BaseModel):
|
|
379
|
+
name: str
|
|
380
|
+
label: str
|
|
381
|
+
type: str
|
|
382
|
+
category: str
|
|
383
|
+
fields: list[DataObjectField]
|
|
384
|
+
|
|
385
|
+
|
|
369
386
|
class DataTransformConfig(BaseConfig):
|
|
370
387
|
sdkVersion: str
|
|
371
388
|
dataspace: str
|
|
372
389
|
permissions: Permissions
|
|
390
|
+
dataObjects: Optional[list[DataObject]] = None
|
|
373
391
|
|
|
374
392
|
|
|
375
393
|
class FunctionConfig(BaseConfig):
|
|
376
394
|
pass
|
|
377
395
|
|
|
378
396
|
|
|
397
|
+
class DloPermission(BaseModel):
|
|
398
|
+
dlo: list[str]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class DmoPermission(BaseModel):
|
|
402
|
+
dmo: list[str]
|
|
403
|
+
|
|
404
|
+
|
|
379
405
|
class Permissions(BaseModel):
|
|
380
|
-
read: Union[DloPermission]
|
|
381
|
-
write: Union[DloPermission]
|
|
406
|
+
read: Union[DloPermission, DmoPermission]
|
|
407
|
+
write: Union[DloPermission, DmoPermission]
|
|
408
|
+
|
|
409
|
+
@model_validator(mode="after")
|
|
410
|
+
def _no_mixed_layers(self) -> "Permissions":
|
|
411
|
+
read_is_dlo = isinstance(self.read, DloPermission)
|
|
412
|
+
write_is_dlo = isinstance(self.write, DloPermission)
|
|
413
|
+
if read_is_dlo != write_is_dlo:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
"permissions.read and permissions.write must both reference "
|
|
416
|
+
"DLOs or both reference DMOs (got "
|
|
417
|
+
f"read={type(self.read).__name__}, "
|
|
418
|
+
f"write={type(self.write).__name__})"
|
|
419
|
+
)
|
|
420
|
+
return self
|
|
382
421
|
|
|
383
422
|
|
|
384
|
-
|
|
385
|
-
|
|
423
|
+
def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
|
|
424
|
+
"""Return the list of object names regardless of layer (DLO or DMO)."""
|
|
425
|
+
if isinstance(perm, DloPermission):
|
|
426
|
+
return perm.dlo
|
|
427
|
+
return perm.dmo
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _data_object_to_output(obj: DataObject) -> dict[str, Any]:
|
|
431
|
+
"""Convert a config.json DataObject into an outputDataObjects entry."""
|
|
432
|
+
fields: list[dict[str, Any]] = []
|
|
433
|
+
for field in obj.fields:
|
|
434
|
+
entry: dict[str, Any] = {
|
|
435
|
+
"isPrimaryKey": field.isPrimaryKey,
|
|
436
|
+
"label": field.label,
|
|
437
|
+
"name": field.name,
|
|
438
|
+
"type": field.dataType,
|
|
439
|
+
}
|
|
440
|
+
if field.keyQualifierFieldName is not None:
|
|
441
|
+
entry["keyQualifierField"] = field.keyQualifierFieldName
|
|
442
|
+
fields.append(entry)
|
|
443
|
+
return {
|
|
444
|
+
"category": obj.category,
|
|
445
|
+
"fields": fields,
|
|
446
|
+
"label": obj.label,
|
|
447
|
+
"name": obj.name,
|
|
448
|
+
"type": obj.type,
|
|
449
|
+
}
|
|
386
450
|
|
|
387
451
|
|
|
388
452
|
def get_config(directory: str) -> BaseConfig:
|
|
@@ -404,10 +468,17 @@ def get_config(directory: str) -> BaseConfig:
|
|
|
404
468
|
except json.JSONDecodeError as err:
|
|
405
469
|
raise ValueError(f"config.json at {config_path} is not valid JSON") from err
|
|
406
470
|
except pydantic.ValidationError as err:
|
|
407
|
-
|
|
471
|
+
errors = err.errors()
|
|
472
|
+
missing = [e for e in errors if e.get("type") == "missing"]
|
|
473
|
+
if missing and len(missing) == len(errors):
|
|
474
|
+
missing_fields = [str(e["loc"][0]) for e in missing]
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"config.json at {config_path} is missing required "
|
|
477
|
+
f"fields: {', '.join(missing_fields)}"
|
|
478
|
+
) from err
|
|
479
|
+
messages = [str(e.get("msg", "")) for e in errors]
|
|
408
480
|
raise ValueError(
|
|
409
|
-
f"config.json at {config_path} is
|
|
410
|
-
f"fields: {', '.join(missing_fields)}"
|
|
481
|
+
f"config.json at {config_path} is invalid: {'; '.join(messages)}"
|
|
411
482
|
) from err
|
|
412
483
|
|
|
413
484
|
|
|
@@ -421,26 +492,45 @@ def create_data_transform(
|
|
|
421
492
|
script_name = metadata.name
|
|
422
493
|
request_hydrated = DATA_TRANSFORM_REQUEST_TEMPLATE.copy()
|
|
423
494
|
|
|
424
|
-
# Add nodes for each write DLO
|
|
425
|
-
for i,
|
|
495
|
+
# Add nodes for each write entry (DLO or DMO)
|
|
496
|
+
for i, name in enumerate(
|
|
497
|
+
_permission_entries(data_transform_config.permissions.write), 1
|
|
498
|
+
):
|
|
426
499
|
request_hydrated["nodes"][f"node{i}"] = {
|
|
427
|
-
"relation_name":
|
|
500
|
+
"relation_name": name,
|
|
428
501
|
"config": {"materialized": "table"},
|
|
429
502
|
"compiled_code": "",
|
|
430
503
|
}
|
|
431
504
|
|
|
432
|
-
# Add sources for each read DLO
|
|
433
|
-
for i,
|
|
434
|
-
|
|
505
|
+
# Add sources for each read entry (DLO or DMO)
|
|
506
|
+
for i, name in enumerate(
|
|
507
|
+
_permission_entries(data_transform_config.permissions.read), 1
|
|
508
|
+
):
|
|
509
|
+
request_hydrated["sources"][f"source{i}"] = {"relation_name": name}
|
|
435
510
|
|
|
436
511
|
request_hydrated["macros"]["macro.byoc"]["arguments"][0]["name"] = script_name
|
|
437
512
|
|
|
513
|
+
definition: dict[str, Any] = {
|
|
514
|
+
"type": "DCSQL",
|
|
515
|
+
"manifest": request_hydrated,
|
|
516
|
+
"version": "56.0",
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# outputDataObjects is only set for DMO-backed transforms. The server requires
|
|
520
|
+
# the schema of any DMO created/updated by the transform; DLO transforms use
|
|
521
|
+
# an existing materialized table and must not include this field.
|
|
522
|
+
if isinstance(data_transform_config.permissions.write, DmoPermission):
|
|
523
|
+
if not data_transform_config.dataObjects:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
"DMO transforms require 'dataObjects' in config.json describing "
|
|
526
|
+
"the schema of each output DMO."
|
|
527
|
+
)
|
|
528
|
+
definition["outputDataObjects"] = [
|
|
529
|
+
_data_object_to_output(obj) for obj in data_transform_config.dataObjects
|
|
530
|
+
]
|
|
531
|
+
|
|
438
532
|
body = {
|
|
439
|
-
"definition":
|
|
440
|
-
"type": "DCSQL",
|
|
441
|
-
"manifest": request_hydrated,
|
|
442
|
-
"version": "56.0",
|
|
443
|
-
},
|
|
533
|
+
"definition": definition,
|
|
444
534
|
"label": f"{metadata.name}",
|
|
445
535
|
"name": f"{metadata.name}",
|
|
446
536
|
"type": "BATCH",
|
|
@@ -27,11 +27,21 @@ if TYPE_CHECKING:
|
|
|
27
27
|
class WriteMode(str, Enum):
|
|
28
28
|
APPEND = "append"
|
|
29
29
|
OVERWRITE = "overwrite"
|
|
30
|
-
OVERWRITE_PARTITIONS = "overwrite_partitions"
|
|
30
|
+
OVERWRITE_PARTITIONS = "overwrite_partitions" # Deprecated: raises error if used
|
|
31
31
|
MERGE = "merge"
|
|
32
32
|
MERGE_UPSERT_DELETE = "merge_upsert_delete"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class MergeRecordType(str, Enum):
|
|
36
|
+
"""Values for the _merge_record_type column used by MERGE_UPSERT_DELETE."""
|
|
37
|
+
|
|
38
|
+
UPSERT = "UPSERT"
|
|
39
|
+
DELETE = "DELETE"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
MERGE_RECORD_TYPE_COLUMN = "_merge_record_type"
|
|
43
|
+
|
|
44
|
+
|
|
35
45
|
class BaseDataCloudWriter(BaseDataAccessLayer):
|
|
36
46
|
"""Base class for Data Cloud writers."""
|
|
37
47
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
from datacustomcode.llm_gateway.base import LLMGateway
|
|
17
17
|
from datacustomcode.llm_gateway.default import DefaultLLMGateway
|
|
18
|
+
from datacustomcode.llm_gateway.errors import LLMGatewayCallError
|
|
18
19
|
from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
|
|
19
20
|
from datacustomcode.llm_gateway.spark_default import DefaultSparkLLMGateway
|
|
20
21
|
|
|
@@ -22,5 +23,6 @@ __all__ = [
|
|
|
22
23
|
"DefaultLLMGateway",
|
|
23
24
|
"DefaultSparkLLMGateway",
|
|
24
25
|
"LLMGateway",
|
|
26
|
+
"LLMGatewayCallError",
|
|
25
27
|
"SparkLLMGateway",
|
|
26
28
|
]
|
|
@@ -34,8 +34,6 @@ class DefaultLLMGateway(EinsteinPlatformClient, LLMGateway):
|
|
|
34
34
|
|
|
35
35
|
payload: Dict[str, Any] = {"prompt": request.prompt}
|
|
36
36
|
|
|
37
|
-
if request.max_tokens is not None:
|
|
38
|
-
payload["max_tokens"] = request.max_tokens
|
|
39
37
|
if request.localization:
|
|
40
38
|
payload["localization"] = request.localization
|
|
41
39
|
if request.tags:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) 2025, Salesforce, Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Exceptions raised by LLM Gateway implementations."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LLMGatewayCallError(RuntimeError):
|
|
23
|
+
"""Raised when an LLM Gateway call returns an error."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
*,
|
|
29
|
+
status: Optional[object] = None,
|
|
30
|
+
error_code: Optional[str] = None,
|
|
31
|
+
error_message: Optional[str] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.status = status
|
|
35
|
+
self.error_code = error_code
|
|
36
|
+
self.error_message = error_message
|
|
@@ -40,7 +40,6 @@ class SparkLLMGateway(ABC, UserExtendableNamedConfigMixin):
|
|
|
40
40
|
self,
|
|
41
41
|
prompt: str,
|
|
42
42
|
model_id: Optional[str] = None,
|
|
43
|
-
max_tokens: Optional[int] = None,
|
|
44
43
|
) -> str:
|
|
45
44
|
"""Issue a one-shot LLM Gateway call and return the generated text."""
|
|
46
45
|
|
|
@@ -50,6 +49,14 @@ class SparkLLMGateway(ABC, UserExtendableNamedConfigMixin):
|
|
|
50
49
|
template: str,
|
|
51
50
|
values: Union[Dict[str, "Column"], "Column"],
|
|
52
51
|
model_id: Optional[str] = None,
|
|
53
|
-
max_tokens: Optional[int] = None,
|
|
54
52
|
) -> "Column":
|
|
55
|
-
"""Build a Spark ``Column`` that invokes the LLM Gateway per row
|
|
53
|
+
"""Build a Spark ``Column`` that invokes the LLM Gateway per row and
|
|
54
|
+
yields a struct ``{status, response, error_code, error_message}``.
|
|
55
|
+
|
|
56
|
+
Select an individual field, e.g.
|
|
57
|
+
``llm_gateway_generate_text_col(...)["response"]``. Returning a struct
|
|
58
|
+
means a single failing row doesn't abort the Spark job.
|
|
59
|
+
Failing row leaves the rest of the DataFrame intact — callers can
|
|
60
|
+
inspect ``status`` / ``error_code`` per row instead of having the
|
|
61
|
+
Spark job abort.
|
|
62
|
+
"""
|
|
@@ -28,10 +28,15 @@ if TYPE_CHECKING:
|
|
|
28
28
|
from pyspark.sql import Column
|
|
29
29
|
|
|
30
30
|
from datacustomcode.llm_gateway.base import LLMGateway
|
|
31
|
+
from datacustomcode.llm_gateway.types.generate_text_response import (
|
|
32
|
+
GenerateTextResponse,
|
|
33
|
+
)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
_DEFAULT_LLM_MODEL_ID = "sfdc_ai__DefaultGPT4Omni"
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
_STATUS_SUCCESS = "SUCCESS"
|
|
39
|
+
_STATUS_ERROR = "ERROR"
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
class DefaultSparkLLMGateway(SparkLLMGateway):
|
|
@@ -52,20 +57,26 @@ class DefaultSparkLLMGateway(SparkLLMGateway):
|
|
|
52
57
|
self,
|
|
53
58
|
prompt: str,
|
|
54
59
|
model_id: Optional[str] = None,
|
|
55
|
-
max_tokens: Optional[int] = None,
|
|
56
60
|
) -> str:
|
|
57
|
-
return _invoke_llm_gateway(self._llm_gateway, prompt, model_id
|
|
61
|
+
return _invoke_llm_gateway(self._llm_gateway, prompt, model_id)
|
|
58
62
|
|
|
59
63
|
def llm_gateway_generate_text_col(
|
|
60
64
|
self,
|
|
61
65
|
template: str,
|
|
62
66
|
values: Union[Dict[str, "Column"], "Column"],
|
|
63
67
|
model_id: Optional[str] = None,
|
|
64
|
-
max_tokens: Optional[int] = None,
|
|
65
68
|
) -> "Column":
|
|
66
|
-
|
|
69
|
+
"""Build a per-row UDF that returns a struct ``{status, response,
|
|
70
|
+
error_code, error_message}`` so per-row failures do not abort the
|
|
71
|
+
Spark job. Callers select the field they want, e.g.
|
|
72
|
+
``llm_gateway_generate_text_col(...)["response"]``.
|
|
73
|
+
"""
|
|
67
74
|
from pyspark.sql.functions import struct, udf
|
|
68
|
-
from pyspark.sql.types import
|
|
75
|
+
from pyspark.sql.types import (
|
|
76
|
+
StringType,
|
|
77
|
+
StructField,
|
|
78
|
+
StructType,
|
|
79
|
+
)
|
|
69
80
|
|
|
70
81
|
if isinstance(values, dict):
|
|
71
82
|
values_col = struct(*[v.alias(k) for k, v in values.items()])
|
|
@@ -73,19 +84,32 @@ class DefaultSparkLLMGateway(SparkLLMGateway):
|
|
|
73
84
|
values_col = values
|
|
74
85
|
|
|
75
86
|
gateway = self._llm_gateway
|
|
87
|
+
result_schema = StructType(
|
|
88
|
+
[
|
|
89
|
+
StructField("status", StringType(), True),
|
|
90
|
+
StructField("response", StringType(), True),
|
|
91
|
+
StructField("error_code", StringType(), True),
|
|
92
|
+
StructField("error_message", StringType(), True),
|
|
93
|
+
]
|
|
94
|
+
)
|
|
76
95
|
|
|
77
|
-
def _generate(values_row: Any) -> str:
|
|
96
|
+
def _generate(values_row: Any) -> Dict[str, Optional[str]]:
|
|
78
97
|
if values_row is None:
|
|
79
|
-
return
|
|
98
|
+
return {
|
|
99
|
+
"status": _STATUS_ERROR,
|
|
100
|
+
"response": None,
|
|
101
|
+
"error_code": None,
|
|
102
|
+
"error_message": "values column was null for this row",
|
|
103
|
+
}
|
|
80
104
|
subs = (
|
|
81
105
|
values_row.asDict()
|
|
82
106
|
if hasattr(values_row, "asDict")
|
|
83
107
|
else dict(values_row)
|
|
84
108
|
)
|
|
85
109
|
prompt = template.format(**subs)
|
|
86
|
-
return
|
|
110
|
+
return _invoke_llm_gateway_as_struct(gateway, prompt, model_id)
|
|
87
111
|
|
|
88
|
-
return udf(_generate,
|
|
112
|
+
return udf(_generate, result_schema)(values_col)
|
|
89
113
|
|
|
90
114
|
|
|
91
115
|
def _build_underlying_gateway() -> "LLMGateway":
|
|
@@ -100,20 +124,61 @@ def _build_underlying_gateway() -> "LLMGateway":
|
|
|
100
124
|
return cfg.to_object()
|
|
101
125
|
|
|
102
126
|
|
|
103
|
-
def
|
|
127
|
+
def _call_llm_gateway(
|
|
104
128
|
gateway: "LLMGateway",
|
|
105
129
|
prompt: str,
|
|
106
130
|
model_id: Optional[str],
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
) -> "GenerateTextResponse":
|
|
132
|
+
"""Build the request and dispatch it to the underlying gateway."""
|
|
109
133
|
from datacustomcode.llm_gateway.types.generate_text_request_builder import (
|
|
110
134
|
GenerateTextRequestBuilder,
|
|
111
135
|
)
|
|
112
136
|
|
|
113
|
-
|
|
137
|
+
request = (
|
|
114
138
|
GenerateTextRequestBuilder()
|
|
115
139
|
.set_prompt(prompt)
|
|
116
140
|
.set_model(model_id or _DEFAULT_LLM_MODEL_ID)
|
|
117
|
-
.
|
|
141
|
+
.build()
|
|
118
142
|
)
|
|
119
|
-
return gateway.generate_text(
|
|
143
|
+
return gateway.generate_text(request)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _invoke_llm_gateway(
|
|
147
|
+
gateway: "LLMGateway",
|
|
148
|
+
prompt: str,
|
|
149
|
+
model_id: Optional[str],
|
|
150
|
+
) -> str:
|
|
151
|
+
from datacustomcode.llm_gateway.errors import LLMGatewayCallError
|
|
152
|
+
|
|
153
|
+
response = _call_llm_gateway(gateway, prompt, model_id)
|
|
154
|
+
if response.is_error:
|
|
155
|
+
raise LLMGatewayCallError(
|
|
156
|
+
f"LLM Gateway call failed: status_code={response.status_code}, "
|
|
157
|
+
f"error_code={response.error_code!r}, "
|
|
158
|
+
f"message={response.data!r}",
|
|
159
|
+
status=response.status_code,
|
|
160
|
+
error_code=response.error_code or None,
|
|
161
|
+
error_message=str(response.data) if response.data else None,
|
|
162
|
+
)
|
|
163
|
+
return response.text
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _invoke_llm_gateway_as_struct(
|
|
167
|
+
gateway: "LLMGateway",
|
|
168
|
+
prompt: str,
|
|
169
|
+
model_id: Optional[str],
|
|
170
|
+
) -> Dict[str, Optional[str]]:
|
|
171
|
+
response = _call_llm_gateway(gateway, prompt, model_id)
|
|
172
|
+
if response.is_error:
|
|
173
|
+
return {
|
|
174
|
+
"status": _STATUS_ERROR,
|
|
175
|
+
"response": None,
|
|
176
|
+
"error_code": response.error_code or None,
|
|
177
|
+
"error_message": str(response.data) if response.data else None,
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
"status": _STATUS_SUCCESS,
|
|
181
|
+
"response": response.text,
|
|
182
|
+
"error_code": None,
|
|
183
|
+
"error_message": None,
|
|
184
|
+
}
|
|
@@ -40,13 +40,6 @@ class GenerateTextRequest(BaseModel):
|
|
|
40
40
|
)
|
|
41
41
|
model_name: str = Field(..., min_length=1, description="Name of the model to use")
|
|
42
42
|
prompt: str = Field(..., description="Input prompt")
|
|
43
|
-
max_tokens: Optional[int] = Field(
|
|
44
|
-
default=None,
|
|
45
|
-
ge=1,
|
|
46
|
-
description=(
|
|
47
|
-
"Maximum number of tokens to generate. If None, server default applies."
|
|
48
|
-
),
|
|
49
|
-
)
|
|
50
43
|
localization: Optional[Dict[str, Any]] = Field(
|
|
51
44
|
default=None, description="Localization settings"
|
|
52
45
|
)
|
|
@@ -26,7 +26,6 @@ class GenerateTextRequestBuilder:
|
|
|
26
26
|
def __init__(self) -> None:
|
|
27
27
|
self._prompt = ""
|
|
28
28
|
self._model_name = ""
|
|
29
|
-
self._max_tokens: Optional[int] = None
|
|
30
29
|
self._localization: Optional[Dict[str, Any]] = None
|
|
31
30
|
self._tags: Optional[Dict[str, Any]] = None
|
|
32
31
|
|
|
@@ -38,10 +37,6 @@ class GenerateTextRequestBuilder:
|
|
|
38
37
|
self._model_name = model_name
|
|
39
38
|
return self
|
|
40
39
|
|
|
41
|
-
def set_max_tokens(self, max_tokens: int) -> "GenerateTextRequestBuilder":
|
|
42
|
-
self._max_tokens = max_tokens
|
|
43
|
-
return self
|
|
44
|
-
|
|
45
40
|
def set_localization(
|
|
46
41
|
self,
|
|
47
42
|
localization: Optional[Dict[str, Any]] = None,
|
|
@@ -80,7 +75,6 @@ class GenerateTextRequestBuilder:
|
|
|
80
75
|
request = GenerateTextRequest(
|
|
81
76
|
prompt=self._prompt,
|
|
82
77
|
model_name=self._model_name,
|
|
83
|
-
max_tokens=self._max_tokens,
|
|
84
78
|
localization=self._localization,
|
|
85
79
|
tags=self._tags,
|
|
86
80
|
)
|
|
@@ -16,7 +16,9 @@ def main():
|
|
|
16
16
|
You can use your AI models configured in Salesforce to generate column
|
|
17
17
|
values. See README.md for how to test locally before deploying to Data Cloud.
|
|
18
18
|
|
|
19
|
-
Example
|
|
19
|
+
Example (the per-row helper returns a struct
|
|
20
|
+
``{status, response, error_code, error_message}`` — pick the field you
|
|
21
|
+
want with ``[...]``):
|
|
20
22
|
|
|
21
23
|
>>> from datacustomcode.client import llm_gateway_generate_text_col
|
|
22
24
|
df_generated = df.withColumn(
|
|
@@ -25,8 +27,7 @@ def main():
|
|
|
25
27
|
... "In one sentence, greet {name} from {city}.",
|
|
26
28
|
... {"name": col("name__c"), "city": col("homecity__c")},
|
|
27
29
|
... model_id="sfdc_ai__DefaultGPT4Omni",
|
|
28
|
-
...
|
|
29
|
-
... ),
|
|
30
|
+
... )["response"],
|
|
30
31
|
... )
|
|
31
32
|
|
|
32
33
|
You can also invoke the LLM with a literal plain text prompt — no
|
|
@@ -35,7 +36,7 @@ def main():
|
|
|
35
36
|
Example:
|
|
36
37
|
|
|
37
38
|
>>> generated_text = client.llm_gateway_generate_text(
|
|
38
|
-
... prompt, model_id
|
|
39
|
+
... prompt, model_id
|
|
39
40
|
... )
|
|
40
41
|
"""
|
|
41
42
|
|
|
@@ -101,49 +101,83 @@ class SFCLITokenProvider(TokenProvider):
|
|
|
101
101
|
|
|
102
102
|
from datacustomcode.deploy import AccessTokenResponse
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
104
|
+
def _run_sf_command(args: list[str], description: str) -> dict:
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
args,
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
check=True,
|
|
111
|
+
timeout=30,
|
|
112
|
+
)
|
|
113
|
+
except FileNotFoundError as exc:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"The 'sf' command was not found. "
|
|
116
|
+
"Install Salesforce CLI: "
|
|
117
|
+
"https://developer.salesforce.com/tools/salesforcecli"
|
|
118
|
+
) from exc
|
|
119
|
+
except subprocess.TimeoutExpired as exc:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"'{description}' timed out for org '{self.sf_cli_org}'"
|
|
122
|
+
) from exc
|
|
123
|
+
except subprocess.CalledProcessError as exc:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
f"'{description}' failed for org '{self.sf_cli_org}': "
|
|
126
|
+
f"{exc.stderr}"
|
|
127
|
+
) from exc
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
try:
|
|
130
|
+
data = json.loads(result.stdout)
|
|
131
|
+
except json.JSONDecodeError as exc:
|
|
132
|
+
raise RuntimeError(
|
|
133
|
+
f"Failed to parse JSON from '{description}': {result.stdout}"
|
|
134
|
+
) from exc
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
if data.get("status") != 0:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"SF CLI error for org '{self.sf_cli_org}': "
|
|
139
|
+
f"{data.get('message', 'unknown error')}"
|
|
140
|
+
)
|
|
141
|
+
return dict(data)
|
|
142
|
+
|
|
143
|
+
# Get org info from sf org display
|
|
144
|
+
display_data = _run_sf_command(
|
|
145
|
+
["sf", "org", "display", "--target-org", self.sf_cli_org, "--json"],
|
|
146
|
+
"sf org display",
|
|
147
|
+
)
|
|
148
|
+
result_data = display_data.get("result", {})
|
|
149
|
+
instance_url = result_data.get("instanceUrl")
|
|
150
|
+
if not instance_url:
|
|
134
151
|
raise RuntimeError(
|
|
135
|
-
f"
|
|
136
|
-
f"
|
|
152
|
+
f"'sf org display' did not return an instance URL "
|
|
153
|
+
f"for org '{self.sf_cli_org}'"
|
|
137
154
|
)
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
# Try show-access-token first (SF CLI >= 2.136.6); fall back to the
|
|
157
|
+
# token from sf org display (older CLIs don't redact it).
|
|
158
|
+
access_token = None
|
|
159
|
+
try:
|
|
160
|
+
token_data = _run_sf_command(
|
|
161
|
+
[
|
|
162
|
+
"sf",
|
|
163
|
+
"org",
|
|
164
|
+
"auth",
|
|
165
|
+
"show-access-token",
|
|
166
|
+
"--target-org",
|
|
167
|
+
self.sf_cli_org,
|
|
168
|
+
"--json",
|
|
169
|
+
],
|
|
170
|
+
"sf org auth show-access-token",
|
|
171
|
+
)
|
|
172
|
+
access_token = token_data.get("result", {}).get("accessToken")
|
|
173
|
+
except RuntimeError:
|
|
174
|
+
# Command not available on older SF CLI versions
|
|
175
|
+
access_token = result_data.get("accessToken")
|
|
142
176
|
|
|
143
|
-
if not access_token
|
|
177
|
+
if not access_token:
|
|
144
178
|
raise RuntimeError(
|
|
145
|
-
f"
|
|
146
|
-
f"
|
|
179
|
+
f"Could not obtain an access token for org '{self.sf_cli_org}'. "
|
|
180
|
+
f"Upgrade SF CLI to 2.136.6+ or ensure the org is authenticated."
|
|
147
181
|
)
|
|
148
182
|
|
|
149
183
|
return AccessTokenResponse(access_token=access_token, instance_url=instance_url)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{salesforce_data_customcode-6.0.2.dev1 → salesforce_data_customcode-6.0.3}/src/datacustomcode/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|