salesforce-data-customcode 6.0.2__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.
Files changed (101) hide show
  1. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/PKG-INFO +51 -1
  2. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/README.md +49 -0
  3. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/pyproject.toml +2 -1
  4. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/__init__.py +15 -0
  5. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/client.py +104 -1
  6. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.yaml +3 -0
  7. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/deploy.py +109 -19
  8. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_config.py +8 -4
  9. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions_config.py +1 -1
  10. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/chunking.py +1 -0
  11. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/__init__.py +6 -0
  12. salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/base.py → salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/errors.py +17 -5
  13. salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/spark_base.py +62 -0
  14. salesforce_data_customcode-6.0.3/src/datacustomcode/llm_gateway/spark_default.py +184 -0
  15. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway_config.py +43 -2
  16. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +3 -0
  17. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +3 -0
  18. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/entrypoint.py +11 -12
  19. salesforce_data_customcode-6.0.3/src/datacustomcode/templates/script/payload/entrypoint.py +53 -0
  20. salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/__init__.py +0 -14
  21. salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/client/__init__.py +0 -14
  22. salesforce_data_customcode-6.0.2/src/datacustomcode/proxy/client/base.py +0 -32
  23. salesforce_data_customcode-6.0.2/src/datacustomcode/templates/script/payload/entrypoint.py +0 -25
  24. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/LICENSE.txt +0 -0
  25. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/auth.py +0 -0
  26. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cli.py +0 -0
  27. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/cmd.py +0 -0
  28. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/common_config.py +0 -0
  29. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/config.py +0 -0
  30. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/constants.py +0 -0
  31. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/credentials.py +0 -0
  32. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_platform_client.py +0 -0
  33. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  34. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/base.py +0 -0
  35. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  36. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/einstein_predictions/types.py +0 -0
  37. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/__init__.py +0 -0
  38. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/base.py +0 -0
  39. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/__init__.py +0 -0
  40. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/file/path/default.py +0 -0
  41. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/__init__.py +0 -0
  42. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/base.py +0 -0
  43. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  44. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function/runtime.py +0 -0
  45. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/function_utils.py +0 -0
  46. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/__init__.py +0 -0
  47. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/base.py +0 -0
  48. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/__init__.py +0 -0
  49. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/base.py +0 -0
  50. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/query_api.py +0 -0
  51. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  52. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/reader/utils.py +0 -0
  53. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/__init__.py +0 -0
  54. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/base.py +0 -0
  55. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/csv.py +0 -0
  56. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/io/writer/print.py +0 -0
  57. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/base.py +0 -0
  58. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/default.py +0 -0
  59. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  60. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
  61. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
  62. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
  63. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
  64. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/mixin.py +0 -0
  65. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/py.typed +0 -0
  66. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/run.py +0 -0
  67. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/scan.py +0 -0
  68. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/__init__.py +0 -0
  69. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/base.py +0 -0
  70. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/spark/default.py +0 -0
  71. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/template.py +0 -0
  72. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/__init__.py +0 -0
  73. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +0 -0
  74. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  75. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/README.md +0 -0
  76. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  77. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  78. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  79. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  80. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
  81. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
  82. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
  83. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
  84. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
  85. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/payload/config.json +0 -0
  86. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  87. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/function/requirements.txt +0 -0
  88. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +0 -0
  89. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile +0 -0
  90. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  91. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/README.md +0 -0
  92. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/account.ipynb +0 -0
  93. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  94. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  95. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  96. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  97. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/payload/config.json +0 -0
  98. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  99. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/templates/script/requirements.txt +0 -0
  100. {salesforce_data_customcode-6.0.2 → salesforce_data_customcode-6.0.3}/src/datacustomcode/token_provider.py +0 -0
  101. {salesforce_data_customcode-6.0.2 → 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.2
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)
@@ -330,6 +331,55 @@ Options:
330
331
  - `--function-invoke-opt TEXT`: Currently we support only `UnstructuredChunking` for functions.
331
332
 
332
333
 
334
+ ## Testing LLM Gateway
335
+
336
+ You can use AI models configured in Salesforce to generate responses while transforming your data. Below is a sample code example:
337
+
338
+ ```
339
+ from datacustomcode.client import Client, llm_gateway_generate_text_col
340
+
341
+
342
+ def main():
343
+ client = Client()
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 [].
348
+ df_generated = df.withColumn(
349
+ "greeting__c",
350
+ llm_gateway_generate_text_col(
351
+ "In one sentence, greet {name} from {city}.",
352
+ {"name": col("name__c"), "city": col("homecity__c")},
353
+ model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
354
+ )["response"],
355
+ )
356
+
357
+ dlo_name = "Output_dll"
358
+ client.write_to_dlo(dlo_name, df_upper1, write_mode=WriteMode.APPEND)
359
+
360
+ greeting = client.llm_gateway_generate_text("In one sentence, generate a greeting message", "sfdc_ai__DefaultGPT52")
361
+
362
+ if __name__ == "__main__":
363
+ main()
364
+ ```
365
+
366
+ In order to test this code on your local machine before deploying it to Data Cloud, you must first set up an External Client App that allows access to the Agent API. Follow this guide to create the ECA https://developer.salesforce.com/docs/ai/agentforce/guide/agent-api-get-started.html#create-a-salesforce-app. You must use `http://localhost:1717/OauthRedirect` as the callback URL.
367
+
368
+ Once the ECA is set up, log in to your org using this ECA
369
+ ```
370
+ sf org login web \
371
+ --alias myorg \
372
+ --instance-url https://{MY_DOMAIN_URL} \
373
+ --client-id {CONSUMER_KEY} \
374
+ --scopes "sfap_api api"
375
+ ```
376
+
377
+ then you can test your code using `myorg` alias
378
+ ```
379
+ datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
380
+ ```
381
+
382
+
333
383
  ## Docker usage
334
384
 
335
385
  The SDK provides Docker-based development options that allow you to test your code in an environment that closely resembles Data Cloud's execution environment.
@@ -305,6 +305,55 @@ Options:
305
305
  - `--function-invoke-opt TEXT`: Currently we support only `UnstructuredChunking` for functions.
306
306
 
307
307
 
308
+ ## Testing LLM Gateway
309
+
310
+ You can use AI models configured in Salesforce to generate responses while transforming your data. Below is a sample code example:
311
+
312
+ ```
313
+ from datacustomcode.client import Client, llm_gateway_generate_text_col
314
+
315
+
316
+ def main():
317
+ client = Client()
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 [].
322
+ df_generated = df.withColumn(
323
+ "greeting__c",
324
+ llm_gateway_generate_text_col(
325
+ "In one sentence, greet {name} from {city}.",
326
+ {"name": col("name__c"), "city": col("homecity__c")},
327
+ model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
328
+ )["response"],
329
+ )
330
+
331
+ dlo_name = "Output_dll"
332
+ client.write_to_dlo(dlo_name, df_upper1, write_mode=WriteMode.APPEND)
333
+
334
+ greeting = client.llm_gateway_generate_text("In one sentence, generate a greeting message", "sfdc_ai__DefaultGPT52")
335
+
336
+ if __name__ == "__main__":
337
+ main()
338
+ ```
339
+
340
+ In order to test this code on your local machine before deploying it to Data Cloud, you must first set up an External Client App that allows access to the Agent API. Follow this guide to create the ECA https://developer.salesforce.com/docs/ai/agentforce/guide/agent-api-get-started.html#create-a-salesforce-app. You must use `http://localhost:1717/OauthRedirect` as the callback URL.
341
+
342
+ Once the ECA is set up, log in to your org using this ECA
343
+ ```
344
+ sf org login web \
345
+ --alias myorg \
346
+ --instance-url https://{MY_DOMAIN_URL} \
347
+ --client-id {CONSUMER_KEY} \
348
+ --scopes "sfap_api api"
349
+ ```
350
+
351
+ then you can test your code using `myorg` alias
352
+ ```
353
+ datacustomcode run ./payload/entrypoint.py --sf-cli-org myorg
354
+ ```
355
+
356
+
308
357
  ## Docker usage
309
358
 
310
359
  The SDK provides Docker-based development options that allow you to test your code in an environment that closely resembles Data Cloud's execution environment.
@@ -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.2"
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"
@@ -17,8 +17,11 @@ __all__ = [
17
17
  "AuthType",
18
18
  "Client",
19
19
  "Credentials",
20
+ "DefaultSparkLLMGateway",
20
21
  "PrintDataCloudWriter",
21
22
  "QueryAPIDataCloudReader",
23
+ "SparkLLMGateway",
24
+ "llm_gateway_generate_text_col",
22
25
  ]
23
26
 
24
27
 
@@ -44,4 +47,16 @@ def __getattr__(name: str):
44
47
  from datacustomcode.io.reader.query_api import QueryAPIDataCloudReader
45
48
 
46
49
  return QueryAPIDataCloudReader
50
+ elif name == "SparkLLMGateway":
51
+ from datacustomcode.llm_gateway import SparkLLMGateway
52
+
53
+ return SparkLLMGateway
54
+ elif name == "DefaultSparkLLMGateway":
55
+ from datacustomcode.llm_gateway import DefaultSparkLLMGateway
56
+
57
+ return DefaultSparkLLMGateway
58
+ elif name == "llm_gateway_generate_text_col":
59
+ from datacustomcode.client import llm_gateway_generate_text_col
60
+
61
+ return llm_gateway_generate_text_col
47
62
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -18,24 +18,87 @@ from enum import Enum
18
18
  from typing import (
19
19
  TYPE_CHECKING,
20
20
  ClassVar,
21
+ Dict,
21
22
  Optional,
23
+ Union,
22
24
  )
23
25
 
24
26
  from datacustomcode.config import config
25
27
  from datacustomcode.file.path.default import DefaultFindFilePath
26
28
  from datacustomcode.io.reader.base import BaseDataCloudReader
29
+ from datacustomcode.llm_gateway_config import spark_llm_gateway_config
27
30
  from datacustomcode.spark.default import DefaultSparkSessionProvider
28
31
 
29
32
  if TYPE_CHECKING:
30
33
  from pathlib import Path
31
34
 
32
- from pyspark.sql import DataFrame as PySparkDataFrame
35
+ from pyspark.sql import Column, DataFrame as PySparkDataFrame
33
36
 
34
37
  from datacustomcode.io.reader.base import BaseDataCloudReader
35
38
  from datacustomcode.io.writer.base import BaseDataCloudWriter, WriteMode
39
+ from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
36
40
  from datacustomcode.spark.base import BaseSparkSessionProvider
37
41
 
38
42
 
43
+ def _build_spark_llm_gateway() -> "SparkLLMGateway":
44
+ """Instantiate the SDK-configured :class:`SparkLLMGateway`.
45
+
46
+ Raises:
47
+ RuntimeError: If no ``spark_llm_gateway_config`` has been loaded.
48
+ """
49
+ cfg = spark_llm_gateway_config.spark_llm_gateway_config
50
+ if cfg is None:
51
+ raise RuntimeError(
52
+ "spark_llm_gateway_config is not configured. Add a "
53
+ "'spark_llm_gateway_config' section to config.yaml."
54
+ )
55
+ return cfg.to_object()
56
+
57
+
58
+ def llm_gateway_generate_text_col(
59
+ template: str,
60
+ values: Union[Dict[str, "Column"], "Column"],
61
+ model_id: Optional[str] = None,
62
+ ) -> "Column":
63
+ """Build a Spark Column that runs the LLM Gateway per row.
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
+
71
+ Example:
72
+
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"
82
+ ... )
83
+
84
+ Args:
85
+ template: The prompt template, with ``{field}`` placeholders matching
86
+ keys in ``values``. Substitution uses ``str.format``.
87
+ values: Either a mapping from placeholder name to Spark ``Column``, or
88
+ a single ``Column`` whose value is already a struct.
89
+ model_id: LLM model id. Defaults to ``sfdc_ai__DefaultGPT4Omni``.
90
+
91
+ Returns:
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.
97
+ """
98
+ gateway = Client()._get_spark_llm_gateway()
99
+ return gateway.llm_gateway_generate_text_col(template, values, model_id=model_id)
100
+
101
+
39
102
  class DataCloudObjectType(Enum):
40
103
  DLO = "dlo"
41
104
  DMO = "dmo"
@@ -94,18 +157,21 @@ class Client:
94
157
  finder: Find a file path
95
158
  reader: A custom reader to use for reading Data Cloud objects.
96
159
  writer: A custom writer to use for writing Data Cloud objects.
160
+ spark_llm_gateway: Optional custom :class:`SparkLLMGateway`.
97
161
 
98
162
  Example:
99
163
  >>> client = Client()
100
164
  >>> file_path = client.find_file_path("data.csv")
101
165
  >>> dlo = client.read_dlo("my_dlo")
102
166
  >>> client.write_to_dmo("my_dmo", dlo)
167
+ >>> answer = client.llm_gateway_generate_text("Generate a greeting message")
103
168
  """
104
169
 
105
170
  _instance: ClassVar[Optional[Client]] = None
106
171
  _reader: BaseDataCloudReader
107
172
  _writer: BaseDataCloudWriter
108
173
  _file: DefaultFindFilePath
174
+ _spark_llm_gateway: Optional[SparkLLMGateway]
109
175
  _data_layer_history: dict[DataCloudObjectType, set[str]]
110
176
  _code_type: str
111
177
 
@@ -114,11 +180,13 @@ class Client:
114
180
  reader: Optional[BaseDataCloudReader] = None,
115
181
  writer: Optional[BaseDataCloudWriter] = None,
116
182
  spark_provider: Optional[BaseSparkSessionProvider] = None,
183
+ spark_llm_gateway: Optional[SparkLLMGateway] = None,
117
184
  code_type: str = "script",
118
185
  ) -> Client:
119
186
 
120
187
  if cls._instance is None:
121
188
  cls._instance = super().__new__(cls)
189
+ cls._instance._spark_llm_gateway = spark_llm_gateway
122
190
  # Initialize Readers and Writers from config
123
191
  # and/or provided reader and writer
124
192
  if reader is None or writer is None:
@@ -225,6 +293,41 @@ class Client:
225
293
 
226
294
  return self._file.find_file_path(file_name) # type: ignore[no-any-return]
227
295
 
296
+ def llm_gateway_generate_text(
297
+ self,
298
+ prompt: str,
299
+ model_id: Optional[str] = None,
300
+ ) -> str:
301
+ """Issue a one-shot LLM Gateway call. This is the scalar counterpart to
302
+ :func:`llm_gateway_generate_text_col`: it runs **once** — not per row.
303
+ Use the column helper method instead when you want to fan a prompt out across
304
+ every row of a DataFrame.
305
+
306
+ Example:
307
+
308
+ >>> response = Client().llm_gateway_generate_text(
309
+ ... "Generate a greeting message"
310
+ ... )
311
+
312
+ Args:
313
+ prompt: The literal prompt to send. Plain text — no
314
+ ``{field}`` substitution is performed on this string.
315
+ model_id: LLM model id to target. Defaults to
316
+ ``sfdc_ai__DefaultGPT4Omni`` when ``None``.
317
+
318
+ Returns:
319
+ The generated text as a plain Python ``str``; empty when the
320
+ gateway response carries no generated text.
321
+ """
322
+ return self._get_spark_llm_gateway().llm_gateway_generate_text(
323
+ prompt, model_id=model_id
324
+ )
325
+
326
+ def _get_spark_llm_gateway(self) -> SparkLLMGateway:
327
+ if self._spark_llm_gateway is None:
328
+ self._spark_llm_gateway = _build_spark_llm_gateway()
329
+ return self._spark_llm_gateway
330
+
228
331
  def _validate_data_layer_history_does_not_contain(
229
332
  self, data_cloud_object_type: DataCloudObjectType
230
333
  ) -> None:
@@ -28,3 +28,6 @@ llm_gateway_config:
28
28
  type_config_name: DefaultLLMGateway
29
29
  options:
30
30
  credentials_profile: default
31
+
32
+ spark_llm_gateway_config:
33
+ type_config_name: DefaultSparkLLMGateway
@@ -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
- class DloPermission(BaseModel):
385
- dlo: list[str]
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
- missing_fields = [str(err["loc"][0]) for err in err.errors()]
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 missing required "
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, dlo in enumerate(data_transform_config.permissions.write.dlo, 1):
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": dlo,
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, dlo in enumerate(data_transform_config.permissions.read.dlo, 1):
434
- request_hydrated["sources"][f"source{i}"] = {"relation_name": dlo}
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",
@@ -15,20 +15,23 @@
15
15
 
16
16
  from typing import (
17
17
  ClassVar,
18
+ Generic,
18
19
  Optional,
19
20
  Type,
20
- cast,
21
+ TypeVar,
21
22
  )
22
23
 
23
24
  from datacustomcode.common_config import BaseObjectConfig
24
25
 
26
+ _T = TypeVar("_T")
25
27
 
26
- class CredentialsObjectConfig(BaseObjectConfig):
28
+
29
+ class CredentialsObjectConfig(BaseObjectConfig, Generic[_T]):
27
30
  type_to_create: ClassVar[Type]
28
31
  credentials_profile: Optional[str] = None
29
32
  sf_cli_org: Optional[str] = None
30
33
 
31
- def to_object(self):
34
+ def to_object(self) -> _T:
32
35
  """Create an object instance, automatically including credentials in options"""
33
36
 
34
37
  options = self.options.copy()
@@ -38,4 +41,5 @@ class CredentialsObjectConfig(BaseObjectConfig):
38
41
  options["sf_cli_org"] = self.sf_cli_org
39
42
 
40
43
  type_ = self.type_to_create.subclass_from_config_name(self.type_config_name)
41
- return cast(type_, type_(**options))
44
+ instance: _T = type_(**options)
45
+ return instance
@@ -28,7 +28,7 @@ from datacustomcode.einstein_predictions.base import EinsteinPredictions
28
28
  _E = TypeVar("_E", bound=EinsteinPredictions)
29
29
 
30
30
 
31
- class EinsteinPredictionsObjectConfig(CredentialsObjectConfig, Generic[_E]):
31
+ class EinsteinPredictionsObjectConfig(CredentialsObjectConfig[_E], Generic[_E]):
32
32
  type_to_create: ClassVar[Type[EinsteinPredictions]] = EinsteinPredictions # type: ignore[type-abstract]
33
33
 
34
34
 
@@ -16,6 +16,7 @@
16
16
  """
17
17
  Pydantic models for Search Index Chunking V1
18
18
  """
19
+
19
20
  from enum import Enum
20
21
  from typing import (
21
22
  Dict,
@@ -15,8 +15,14 @@
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
19
+ from datacustomcode.llm_gateway.spark_base import SparkLLMGateway
20
+ from datacustomcode.llm_gateway.spark_default import DefaultSparkLLMGateway
18
21
 
19
22
  __all__ = [
20
23
  "DefaultLLMGateway",
24
+ "DefaultSparkLLMGateway",
21
25
  "LLMGateway",
26
+ "LLMGatewayCallError",
27
+ "SparkLLMGateway",
22
28
  ]
@@ -12,13 +12,25 @@
12
12
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
+ """Exceptions raised by LLM Gateway implementations."""
16
+
15
17
  from __future__ import annotations
16
18
 
17
- from abc import ABC
19
+ from typing import Optional
18
20
 
19
- from datacustomcode.mixin import UserExtendableNamedConfigMixin
20
21
 
22
+ class LLMGatewayCallError(RuntimeError):
23
+ """Raised when an LLM Gateway call returns an error."""
21
24
 
22
- class BaseProxyAccessLayer(ABC, UserExtendableNamedConfigMixin):
23
- def __init__(self):
24
- pass
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