sfdc-agentforce-byoc-python-sdk 0.1.0__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 (39) hide show
  1. agentforce_byoc/__init__.py +26 -0
  2. agentforce_byoc/_http.py +142 -0
  3. agentforce_byoc/cli/__init__.py +8 -0
  4. agentforce_byoc/cli/commands.py +913 -0
  5. agentforce_byoc/cli/main.py +44 -0
  6. agentforce_byoc/client.py +224 -0
  7. agentforce_byoc/deploy_client.py +246 -0
  8. agentforce_byoc/gateway/__init__.py +26 -0
  9. agentforce_byoc/gateway/base.py +66 -0
  10. agentforce_byoc/gateway/mock_relay_client.py +54 -0
  11. agentforce_byoc/gateway/relay_client.py +394 -0
  12. agentforce_byoc/logging/__init__.py +20 -0
  13. agentforce_byoc/logging/formatter.py +96 -0
  14. agentforce_byoc/logging/logger.py +59 -0
  15. agentforce_byoc/runtime/__init__.py +21 -0
  16. agentforce_byoc/runtime/agentforce_runtime.py +47 -0
  17. agentforce_byoc/runtime/base.py +54 -0
  18. agentforce_byoc/runtime/mock_runtime.py +50 -0
  19. agentforce_byoc/schema_utils/__init__.py +35 -0
  20. agentforce_byoc/schema_utils/entry_func.py +48 -0
  21. agentforce_byoc/schema_utils/generator.py +694 -0
  22. agentforce_byoc/schema_utils/schema.py +79 -0
  23. agentforce_byoc/templates/__init__.py +4 -0
  24. agentforce_byoc/templates/agent/.dockerignore.template +51 -0
  25. agentforce_byoc/templates/agent/.gitignore.template +64 -0
  26. agentforce_byoc/templates/agent/.python-version.template +1 -0
  27. agentforce_byoc/templates/agent/Dockerfile.template +41 -0
  28. agentforce_byoc/templates/agent/Makefile.template +51 -0
  29. agentforce_byoc/templates/agent/README.md.template +358 -0
  30. agentforce_byoc/templates/agent/config.json.template +3 -0
  31. agentforce_byoc/templates/agent/entry_func.py.template +51 -0
  32. agentforce_byoc/templates/agent/main.py.template +149 -0
  33. agentforce_byoc/templates/agent/pyproject.toml.template +24 -0
  34. agentforce_byoc/templates/agent/schema.py.template +73 -0
  35. sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/METADATA +439 -0
  36. sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/RECORD +39 -0
  37. sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/WHEEL +4 -0
  38. sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/entry_points.txt +3 -0
  39. sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/licenses/LICENSE +206 -0
@@ -0,0 +1,44 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Main CLI entry point for Agentforce BYOC SDK."""
5
+
6
+ import click
7
+
8
+ from agentforce_byoc import __version__
9
+
10
+
11
+ @click.group()
12
+ @click.version_option(version=__version__)
13
+ def cli():
14
+ """
15
+ Agentforce BYOC SDK - Build and deploy custom agents.
16
+
17
+ Commands:
18
+ init Initialize a new agent project
19
+ run Run agent locally against the Agentforce runtime
20
+ validate Validate agent before deployment
21
+ zip Create a deployment-ready package archive
22
+ package Build end-to-end: validate -> run -> zip
23
+ deploy Register + upload a packaged agent to Agentforce BYOC
24
+ deploy-status Poll deployment status (read-only)
25
+ generate-schema Generate an OpenAPI spec from @entry_func functions
26
+
27
+ Auth/identity for run + deploy + deploy-status come from env vars (required;
28
+ abort if unset): ORG_JWT_TOKEN, SFDC_CORE_TENANT_ID
29
+ (core/<instance>-<fd>/<org-id>), SFDC_USER_ID, SFDC_APP_CONTEXT.
30
+
31
+ Examples:
32
+ agentforce init my-agent
33
+ agentforce run main.py
34
+ agentforce zip
35
+ agentforce package
36
+ export ORG_JWT_TOKEN=<jwt> && export SFDC_CORE_TENANT_ID=core/<inst>-<fd>/<org> && export SFDC_USER_ID=<user-id> && export SFDC_APP_CONTEXT=<app-context>
37
+ agentforce deploy --package-name test --package-file test-0.1.0.tar.gz
38
+ agentforce deploy-status --package-name test --version 1.0
39
+ """
40
+ pass
41
+
42
+
43
+ # Import commands to register them with the CLI group
44
+ from agentforce_byoc.cli import commands # noqa: E402, F401
@@ -0,0 +1,224 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
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
+
16
+ """Client for Agentforce BYOC SDK."""
17
+
18
+ import os
19
+ from typing import Any, Dict, Optional
20
+
21
+ from agentforce_byoc.logging import get_logger
22
+ from agentforce_byoc.runtime.base import BaseRuntimeProvider
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ def _is_mock_mode() -> bool:
28
+ """Return True if mock mode is enabled via AGENTFORCE_BYOC_MOCK."""
29
+ return os.getenv("AGENTFORCE_BYOC_MOCK", "").strip().lower() in (
30
+ "1",
31
+ "true",
32
+ "yes",
33
+ )
34
+
35
+
36
+ def _select_runtime_provider() -> BaseRuntimeProvider:
37
+ """
38
+ Select the runtime provider.
39
+
40
+ Mock mode (AGENTFORCE_BYOC_MOCK truthy) uses the mock provider; otherwise
41
+ the Agentforce BYOC runtime is used (default).
42
+ """
43
+ if _is_mock_mode():
44
+ logger.info("AGENTFORCE_BYOC_MOCK set - using MOCK runtime")
45
+ from agentforce_byoc.runtime.mock_runtime import MockRuntimeProvider
46
+
47
+ return MockRuntimeProvider()
48
+
49
+ from agentforce_byoc.runtime.agentforce_runtime import AgentforceRuntimeProvider
50
+
51
+ return AgentforceRuntimeProvider()
52
+
53
+
54
+ class Client:
55
+ """
56
+ Main client for interacting with Agentforce services.
57
+
58
+ The Client selects the runtime (Agentforce BYOC by default, or mock when
59
+ AGENTFORCE_BYOC_MOCK is set) and provides a unified interface for relaying
60
+ calls through the Agentforce Relay API.
61
+
62
+ Features:
63
+ - Runtime selection via AGENTFORCE_BYOC_MOCK env var
64
+ - Singleton pattern (one instance per application)
65
+ - Relay calls for LLM generations and chat generations
66
+
67
+ Example:
68
+ >>> from agentforce_byoc import Client
69
+ >>> client = Client()
70
+ >>> response = client.call_llm_generations(
71
+ ... parameter={"prompt": "What is AI?"},
72
+ ... )
73
+
74
+ Note:
75
+ Typically you don't instantiate Client directly - use Agent base class
76
+ which provides self.client automatically.
77
+ """
78
+
79
+ _instance: Optional["Client"] = None
80
+ _runtime_provider: Optional[BaseRuntimeProvider] = None
81
+
82
+ def __new__(cls, runtime_provider: Optional[BaseRuntimeProvider] = None):
83
+ """
84
+ Create or return singleton Client instance.
85
+
86
+ Args:
87
+ runtime_provider: Optional runtime provider (for testing/advanced use)
88
+
89
+ Returns:
90
+ Client singleton instance
91
+
92
+ Raises:
93
+ ValueError: If trying to change runtime provider after initialization
94
+ """
95
+ if cls._instance is None:
96
+ cls._instance = super().__new__(cls)
97
+
98
+ # Select runtime if not provided
99
+ if runtime_provider is None:
100
+ cls._runtime_provider = _select_runtime_provider()
101
+ else:
102
+ cls._runtime_provider = runtime_provider
103
+
104
+ logger.info(f"Initialized Client with runtime: {cls._runtime_provider.name}")
105
+
106
+ elif runtime_provider is not None and cls._instance is not None:
107
+ raise ValueError(
108
+ "Cannot set runtime_provider after Client is initialized. "
109
+ "Create a new Client by resetting the singleton first."
110
+ )
111
+
112
+ return cls._instance
113
+
114
+ @classmethod
115
+ def reset(cls):
116
+ """
117
+ Reset the singleton instance.
118
+
119
+ Useful for testing when you need to reinitialize with different runtime.
120
+
121
+ Example:
122
+ >>> Client.reset()
123
+ >>> client = Client(custom_runtime_provider)
124
+ """
125
+ cls._instance = None
126
+ cls._runtime_provider = None
127
+
128
+ @property
129
+ def runtime(self) -> BaseRuntimeProvider:
130
+ """Get the current runtime provider."""
131
+ if self._runtime_provider is None:
132
+ raise RuntimeError("Client is not initialized with a runtime provider.")
133
+ return self._runtime_provider
134
+
135
+ def call_llm_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
136
+ """
137
+ Relay an LLM generations call through the Agentforce Relay API.
138
+
139
+ The tenant id, user id, and app context are resolved from the
140
+ environment (``SFDC_CORE_TENANT_ID`` / ``SFDC_USER_ID`` /
141
+ ``SFDC_APP_CONTEXT``) injected into the managed sandbox.
142
+
143
+ Args:
144
+ parameter: Raw request body, passed through to the Relay API as-is.
145
+
146
+ Returns:
147
+ The Relay API response envelope (``{body, requestId, traceId}``).
148
+
149
+ Raises:
150
+ RuntimeError: If the Relay API call fails, or the tenant id / user
151
+ id / app context environment variables are not set.
152
+ ValueError: If the tenant id cannot be resolved to an endpoint.
153
+ """
154
+ provider = self.runtime
155
+ logger.debug(
156
+ "Calling Relay (llm)",
157
+ extra={"runtime": provider.name},
158
+ )
159
+ return provider.relay_gateway_client.call_llm_generations(parameter=parameter)
160
+
161
+ def call_llm_chat_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
162
+ """
163
+ Relay an LLM chat generations call through the Agentforce Relay API.
164
+
165
+ The tenant id, user id, and app context are resolved from the
166
+ environment (``SFDC_CORE_TENANT_ID`` / ``SFDC_USER_ID`` /
167
+ ``SFDC_APP_CONTEXT``) injected into the managed sandbox.
168
+
169
+ Args:
170
+ parameter: Raw request body, passed through to the Relay API as-is.
171
+
172
+ Returns:
173
+ The Relay API response envelope (``{body, requestId, traceId}``).
174
+
175
+ Raises:
176
+ RuntimeError: If the Relay API call fails, or the tenant id / user
177
+ id / app context environment variables are not set.
178
+ ValueError: If the tenant id cannot be resolved to an endpoint.
179
+ """
180
+ provider = self.runtime
181
+ logger.debug(
182
+ "Calling Relay (llm_chat)",
183
+ extra={"runtime": provider.name},
184
+ )
185
+ return provider.relay_gateway_client.call_llm_chat_generations(parameter=parameter)
186
+
187
+
188
+ # ============================================================================
189
+ # Functional API
190
+ # ============================================================================
191
+
192
+
193
+ def get_client(runtime_provider: Optional[BaseRuntimeProvider] = None) -> Client:
194
+ """
195
+ Get the singleton Client instance (functional API).
196
+
197
+ This is the recommended way to access the client in functional-style code.
198
+ The client selects the runtime on first call.
199
+
200
+ Args:
201
+ runtime_provider: Optional runtime provider (for testing/advanced use)
202
+
203
+ Returns:
204
+ Client singleton instance
205
+
206
+ Example:
207
+ >>> from agentforce_byoc import get_client
208
+ >>>
209
+ >>> def function(request: dict) -> dict:
210
+ ... client = get_client()
211
+ ... response = client.call_llm_generations(
212
+ ... parameter={"prompt": request.get("text", "")},
213
+ ... )
214
+ ... return {"response": response}
215
+
216
+ Runtime Selection:
217
+ - Agentforce BYOC runtime (default)
218
+ - Mock mode: AGENTFORCE_BYOC_MOCK is set (truthy)
219
+
220
+ Note:
221
+ The client is a singleton - calling get_client() multiple times
222
+ returns the same instance.
223
+ """
224
+ return Client(runtime_provider=runtime_provider)
@@ -0,0 +1,246 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
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
+
16
+ """HTTP client for the Agentforce BYOC deploy/upload API.
17
+
18
+ Implements the two-step package upload against the Agentforce BYOC platform:
19
+
20
+ 1. ``POST <base>/byoc/upload/request`` to register package metadata AND obtain a
21
+ presigned upload URL in one call. The body carries ``packageName``,
22
+ ``version``, ``packageSize``, ``entryPoints``, and ``maxLiveDurationS``; the
23
+ response returns the presigned ``uploadUrl``.
24
+ 2. ``PUT`` the package archive to that presigned URL.
25
+
26
+ Deployment status can then be polled read-only via
27
+ ``GET <base>/byoc/upload/status?packageName=&version=``, which reports
28
+ ``registrationStatus`` (is the package registered) and ``uploadStatus`` (has the
29
+ archive been uploaded). That endpoint never writes — registration happens in
30
+ step 1.
31
+
32
+ The base URL is resolved by
33
+ :func:`agentforce_byoc.gateway.relay_client.resolve_sfap_base_url` — from the
34
+ ``BYOC_PROXY_URL`` environment variable when set, else the production default.
35
+ Auth is sent as ``Authorization: Bearer <org-jwt>`` plus the
36
+ ``x-sfdc-core-tenant-id``, ``x-sfdc-user-id``, and ``x-sfdc-app-context``
37
+ headers.
38
+
39
+ Uses only the Python standard library so nothing extra is bundled into a BYOC
40
+ package.
41
+ """
42
+
43
+ import json
44
+ import urllib.parse
45
+ from pathlib import Path
46
+ from typing import Any, Dict, List, cast
47
+
48
+ from agentforce_byoc import _http
49
+ from agentforce_byoc.logging import get_logger
50
+
51
+ logger = get_logger(__name__)
52
+
53
+ # The presigned URL is signed for ContentType "application/gzip". The PUT must
54
+ # send exactly this Content-Type or the upload is rejected as a signature
55
+ # mismatch.
56
+ _PACKAGE_CONTENT_TYPE = "application/gzip"
57
+
58
+ # Timeout (seconds) for API calls and the package upload PUT.
59
+ _DEPLOY_TIMEOUT_S = 120
60
+
61
+
62
+ def _post_json(
63
+ url: str,
64
+ body: Dict[str, Any],
65
+ org_jwt: str,
66
+ tenant_id: str,
67
+ user_id: str,
68
+ app_context: str,
69
+ ) -> Dict[str, Any]:
70
+ """
71
+ POST a JSON body to a BYOC API endpoint and parse the response.
72
+
73
+ Sends the org JWT as a bearer token plus the tenant id, user id, and app
74
+ context as the ``x-sfdc-core-tenant-id`` / ``x-sfdc-user-id`` /
75
+ ``x-sfdc-app-context`` headers.
76
+
77
+ Raises:
78
+ RuntimeError: On HTTP error, transport error, or a non-JSON response.
79
+ """
80
+ # The org JWT is sent as a bearer token; the tenant id also travels as the
81
+ # x-sfdc-core-tenant-id header. A 400 "x-sfdc-core-tenant-id header is
82
+ # required" indicates the JWT was rejected, not a missing header here. The
83
+ # user id and app context identify who is deploying, matching the relay path.
84
+ headers = {
85
+ "Authorization": f"Bearer {org_jwt}",
86
+ "x-sfdc-core-tenant-id": tenant_id,
87
+ "x-sfdc-user-id": user_id,
88
+ "x-sfdc-app-context": app_context,
89
+ }
90
+ try:
91
+ status, raw = _http.post_json(url, body, headers, timeout=_DEPLOY_TIMEOUT_S)
92
+ except OSError as e:
93
+ raise RuntimeError(f"BYOC API call to {url} failed: {e}") from e
94
+
95
+ # Surface the BYOC API response (status + body) so callers can see exactly
96
+ # what the platform returned.
97
+ logger.info("BYOC API %s -> status=%s body=%s", url, status, raw)
98
+
99
+ if status >= 400:
100
+ raise RuntimeError(f"BYOC API call to {url} failed with status {status}: {raw}")
101
+
102
+ try:
103
+ return cast(Dict[str, Any], json.loads(raw))
104
+ except json.JSONDecodeError as e:
105
+ raise RuntimeError(f"BYOC API at {url} returned a non-JSON response: {raw!r}") from e
106
+
107
+
108
+ def _get_json(
109
+ url: str,
110
+ org_jwt: str,
111
+ tenant_id: str,
112
+ user_id: str,
113
+ app_context: str,
114
+ ) -> Dict[str, Any]:
115
+ """
116
+ GET a BYOC API endpoint and parse the JSON response.
117
+
118
+ Sends the same identity headers as :func:`_post_json`. Used for the
119
+ read-only ``GET /byoc/upload/status`` poll, which takes its parameters in
120
+ the query string and has no request body.
121
+
122
+ Raises:
123
+ RuntimeError: On HTTP error, transport error, or a non-JSON response.
124
+ """
125
+ headers = {
126
+ "Authorization": f"Bearer {org_jwt}",
127
+ "x-sfdc-core-tenant-id": tenant_id,
128
+ "x-sfdc-user-id": user_id,
129
+ "x-sfdc-app-context": app_context,
130
+ }
131
+ try:
132
+ status, raw = _http.request("GET", url, headers=headers, timeout=_DEPLOY_TIMEOUT_S)
133
+ except OSError as e:
134
+ raise RuntimeError(f"BYOC API call to {url} failed: {e}") from e
135
+
136
+ body_text = raw.decode("utf-8", errors="replace")
137
+ logger.info("BYOC API %s -> status=%s body=%s", url, status, body_text)
138
+
139
+ if status >= 400:
140
+ raise RuntimeError(f"BYOC API call to {url} failed with status {status}: {body_text}")
141
+
142
+ try:
143
+ return cast(Dict[str, Any], json.loads(body_text))
144
+ except json.JSONDecodeError as e:
145
+ raise RuntimeError(f"BYOC API at {url} returned a non-JSON response: {body_text!r}") from e
146
+
147
+
148
+ def _put_file(url: str, file_path: Path, content_type: str) -> None:
149
+ """
150
+ PUT a file's bytes to a (presigned) URL.
151
+
152
+ No auth header is added: the presigned URL carries its own signature.
153
+
154
+ Raises:
155
+ RuntimeError: On HTTP error or transport error.
156
+ """
157
+ data = file_path.read_bytes()
158
+ try:
159
+ status, raw = _http.request(
160
+ "PUT",
161
+ url,
162
+ headers={"Content-Type": content_type},
163
+ body=data,
164
+ timeout=_DEPLOY_TIMEOUT_S,
165
+ )
166
+ except OSError as e:
167
+ raise RuntimeError(f"Upload to presigned URL failed: {e}") from e
168
+
169
+ if status >= 400:
170
+ body_text = raw.decode("utf-8", errors="replace")
171
+ raise RuntimeError(f"Upload to presigned URL failed with status {status}: {body_text}")
172
+
173
+
174
+ def request_presigned_upload(
175
+ base_url: str,
176
+ tenant_id: str,
177
+ org_jwt: str,
178
+ user_id: str,
179
+ app_context: str,
180
+ package_name: str,
181
+ version: str,
182
+ package_size: int,
183
+ entry_points: List[str],
184
+ max_live_duration_s: int,
185
+ ) -> Dict[str, Any]:
186
+ """
187
+ Step 1: register package metadata and get a presigned upload URL.
188
+
189
+ ``POST /byoc/upload/request`` does both in one call: it registers the
190
+ package metadata (``packageSize``, ``entryPoints``, ``maxLiveDurationS``) and
191
+ returns the presigned ``uploadUrl``. The caller then PUTs the archive to that
192
+ URL (see :func:`upload_package`).
193
+
194
+ Returns:
195
+ The response ``{uploadUrl, s3Key, tenantId, packageName, version,
196
+ uploadTime, packageSize, entryPoints, maxLiveDurationS, status}``.
197
+ """
198
+ url = f"{base_url}/byoc/upload/request"
199
+ body = {
200
+ "packageName": package_name,
201
+ "version": version,
202
+ "packageSize": package_size,
203
+ "entryPoints": entry_points,
204
+ "maxLiveDurationS": max_live_duration_s,
205
+ }
206
+ logger.info("Registering package + requesting presigned upload URL from %s", url)
207
+ return _post_json(url, body, org_jwt, tenant_id, user_id, app_context)
208
+
209
+
210
+ def upload_package(upload_url: str, file_path: Path) -> None:
211
+ """Step 2: upload the package archive to the presigned URL."""
212
+ logger.info("Uploading %s (%d bytes)", file_path.name, file_path.stat().st_size)
213
+ _put_file(upload_url, file_path, _PACKAGE_CONTENT_TYPE)
214
+
215
+
216
+ def get_upload_status(
217
+ base_url: str,
218
+ tenant_id: str,
219
+ org_jwt: str,
220
+ user_id: str,
221
+ app_context: str,
222
+ package_name: str,
223
+ version: str,
224
+ ) -> Dict[str, Any]:
225
+ """
226
+ Poll deployment status (read-only).
227
+
228
+ Calls ``GET /byoc/upload/status`` with ``packageName`` and ``version`` in the
229
+ query string. The endpoint never writes — registration happens in
230
+ :func:`request_presigned_upload`. It reports both halves of the two-phase
231
+ upload independently:
232
+
233
+ - ``registrationStatus``: ``"registered"`` if the package metadata exists,
234
+ else ``"unregistered"``.
235
+ - ``uploadStatus``: ``"uploaded"`` if the archive has been uploaded,
236
+ ``"pending"`` if not, ``"unknown"`` if the check itself failed.
237
+
238
+ Returns:
239
+ ``{tenantId, packageName, version, s3Key, registrationStatus,
240
+ uploadStatus, ...}`` (plus ``uploadTime``/``packageSize``/``entryPoints``/
241
+ ``maxLiveDurationS`` when registered).
242
+ """
243
+ query = urllib.parse.urlencode({"packageName": package_name, "version": version})
244
+ url = f"{base_url}/byoc/upload/status?{query}"
245
+ logger.info("Polling upload status from %s", url)
246
+ return _get_json(url, org_jwt, tenant_id, user_id, app_context)
@@ -0,0 +1,26 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
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
+
16
+ """Gateway clients for external services."""
17
+
18
+ from agentforce_byoc.gateway.base import BaseRelayGatewayClient
19
+ from agentforce_byoc.gateway.mock_relay_client import MockRelayGatewayClient
20
+ from agentforce_byoc.gateway.relay_client import AgentforceRelayGatewayClient
21
+
22
+ __all__ = [
23
+ "BaseRelayGatewayClient",
24
+ "AgentforceRelayGatewayClient",
25
+ "MockRelayGatewayClient",
26
+ ]
@@ -0,0 +1,66 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
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
+
16
+ """Base class for relay gateway clients."""
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import Any, Dict
20
+
21
+
22
+ class BaseRelayGatewayClient(ABC):
23
+ """
24
+ Abstract base class for Relay gateway clients.
25
+
26
+ The Relay gateway routes API calls from BYOC code (running in the managed
27
+ sandbox, which has no direct outbound network) through the Agentforce Relay
28
+ API. Each relay function is exposed as a method.
29
+
30
+ Implementations must relay the raw ``parameter`` body through to the
31
+ Relay API unchanged.
32
+ """
33
+
34
+ @abstractmethod
35
+ def call_llm_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
36
+ """
37
+ Relay an LLM generations call (Relay function ``llm``).
38
+
39
+ The tenant id, user id, and app context are resolved from the
40
+ environment (``SFDC_CORE_TENANT_ID`` / ``SFDC_USER_ID`` /
41
+ ``SFDC_APP_CONTEXT``) injected into the managed sandbox.
42
+
43
+ Args:
44
+ parameter: Raw request body, passed through to the Relay API as-is.
45
+
46
+ Returns:
47
+ The Relay API response envelope (``{body, requestId, traceId}``).
48
+ """
49
+ ...
50
+
51
+ @abstractmethod
52
+ def call_llm_chat_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
53
+ """
54
+ Relay an LLM chat generations call (Relay function ``llm_chat``).
55
+
56
+ The tenant id, user id, and app context are resolved from the
57
+ environment (``SFDC_CORE_TENANT_ID`` / ``SFDC_USER_ID`` /
58
+ ``SFDC_APP_CONTEXT``) injected into the managed sandbox.
59
+
60
+ Args:
61
+ parameter: Raw request body, passed through to the Relay API as-is.
62
+
63
+ Returns:
64
+ The Relay API response envelope (``{body, requestId, traceId}``).
65
+ """
66
+ ...
@@ -0,0 +1,54 @@
1
+ # Copyright (c) 2026, Salesforce, Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
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
+
16
+ """Mock Relay gateway client for local development."""
17
+
18
+ import os
19
+ from typing import Optional
20
+
21
+ from agentforce_byoc.gateway.relay_client import ORG_JWT_ENV_VAR, AgentforceRelayGatewayClient
22
+ from agentforce_byoc.logging import get_logger
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class MockRelayGatewayClient(AgentforceRelayGatewayClient):
28
+ """
29
+ Relay gateway client for local development outside the managed sandbox.
30
+
31
+ Behaves exactly like :class:`AgentforceRelayGatewayClient` — same URL
32
+ resolution, request shape, and transport — except the org JWT is supplied
33
+ by the SDK author rather than injected by the runtime. The JWT is taken
34
+ from the ``org_jwt`` constructor argument, falling back to the
35
+ ``ORG_JWT_TOKEN`` environment variable.
36
+ """
37
+
38
+ def __init__(self, org_jwt: Optional[str] = None):
39
+ self._org_jwt = org_jwt
40
+
41
+ def _get_org_jwt(self) -> str:
42
+ """
43
+ Resolve the org JWT from the constructor arg or ``ORG_JWT_TOKEN``.
44
+
45
+ Raises:
46
+ RuntimeError: If neither source provides a JWT.
47
+ """
48
+ org_jwt = self._org_jwt or os.getenv(ORG_JWT_ENV_VAR)
49
+ if not org_jwt:
50
+ raise RuntimeError(
51
+ f"No org JWT supplied for mock mode. Pass org_jwt=... or "
52
+ f"export {ORG_JWT_ENV_VAR}."
53
+ )
54
+ return org_jwt