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,394 @@
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
+ """Agentforce Relay gateway client."""
17
+
18
+ import json
19
+ import os
20
+ from typing import Any, Dict, Optional, cast
21
+
22
+ from agentforce_byoc import _http
23
+ from agentforce_byoc.gateway.base import BaseRelayGatewayClient
24
+ from agentforce_byoc.logging import get_logger
25
+
26
+ logger = get_logger(__name__)
27
+
28
+ # Environment variable carrying the org JWT, injected into the managed sandbox
29
+ # by the Agentforce runtime.
30
+ ORG_JWT_ENV_VAR = "ORG_JWT_TOKEN"
31
+
32
+ # Environment variable carrying the originating invoke's app context, injected
33
+ # into the managed sandbox by the Agentforce runtime. The Relay API requires the
34
+ # x-sfdc-app-context header (400 header_invalid if missing); we forward this
35
+ # value verbatim.
36
+ APP_CONTEXT_ENV_VAR = "SFDC_APP_CONTEXT"
37
+
38
+ # Environment variable carrying the caller's core tenant id
39
+ # (``core/<instance>-<fd>/<org-id>``), injected into the managed sandbox by the
40
+ # Agentforce runtime. It is forwarded as the x-sfdc-core-tenant-id header, so
41
+ # callers no longer thread the tenant id through each relay call.
42
+ CORE_TENANT_ID_ENV_VAR = "SFDC_CORE_TENANT_ID"
43
+
44
+ # Environment variable carrying the caller's user id, injected into the managed
45
+ # sandbox by the Agentforce runtime. Forwarded as the x-sfdc-user-id header.
46
+ USER_ID_ENV_VAR = "SFDC_USER_ID"
47
+
48
+ # Environment variable carrying the client feature id, injected into the managed
49
+ # sandbox by the Agentforce runtime. Forwarded as the x-client-feature-id header
50
+ # so the LLM Gateway can attribute the call to a feature (used for billing).
51
+ # OPTIONAL: when unset, the header is simply not sent.
52
+ FEATURE_ID_ENV_VAR = "SFDC_FEATURE_ID"
53
+
54
+ # Environment variable carrying the caller's B3 trace id, injected into the
55
+ # managed sandbox by the Agentforce runtime. Forwarded as the x-b3-traceid header
56
+ # to propagate the distributed trace. OPTIONAL: when unset, the header is not sent.
57
+ TRACE_ID_ENV_VAR = "SFDC_TRACE_ID"
58
+
59
+ # Relay function identifiers understood by the Relay API.
60
+ RELAY_FUNCTION_LLM = "llm"
61
+ RELAY_FUNCTION_LLM_CHAT = "llm_chat"
62
+
63
+ # Environment variable overriding the production Agentforce BYOC API URL. It
64
+ # takes one of two forms (see :func:`resolve_relay_url`):
65
+ # - the full relay URL (``<base>/byoc/service``), as the Agentforce runtime
66
+ # injects it into the managed sandbox — used as-is by the relay;
67
+ # - a base host (e.g. ``https://<env>.api.salesforce.com``), as exported locally
68
+ # for the deploy CLI, which appends its own ``/byoc/upload/...`` path (see
69
+ # :func:`resolve_sfap_base_url`) — the relay appends ``/byoc/service``.
70
+ PROXY_URL_ENV_VAR = "BYOC_PROXY_URL"
71
+
72
+ # Relay endpoint path, appended to the base URL unless it is already present.
73
+ _RELAY_PATH = "/byoc/service"
74
+
75
+ # Default Agentforce BYOC API base URL, used when BYOC_PROXY_URL is not set.
76
+ # Production is the only public environment, so it is the default; internal
77
+ # developers targeting a pre-production environment point BYOC_PROXY_URL at that
78
+ # environment's host instead.
79
+ _DEFAULT_SFAP_BASE_URL = "https://api.salesforce.com"
80
+
81
+ # Relay timeout (seconds) for the outbound POST.
82
+ _RELAY_TIMEOUT_S = 60
83
+
84
+
85
+ def resolve_sfap_base_url(tenant_id: str) -> str:
86
+ """
87
+ Resolve the Agentforce BYOC API **base** URL (no endpoint path appended).
88
+
89
+ Prefers the ``BYOC_PROXY_URL`` environment variable: when set, it is
90
+ returned verbatim (minus any trailing slash) and the tenant id is ignored.
91
+ Otherwise returns the production default (``https://api.salesforce.com``);
92
+ internal developers targeting a pre-production environment set
93
+ ``BYOC_PROXY_URL`` to that environment's host.
94
+
95
+ This returns only the base (e.g. ``https://api.salesforce.com``); the
96
+ caller appends its own endpoint path. The deploy CLI uses it that way
97
+ (``<base>/byoc/upload/request`` etc.), and :func:`resolve_relay_url` appends
98
+ ``/byoc/service``.
99
+
100
+ Tenant ids follow ``core/<instance>[-<fd>]/<org-id>`` (e.g.
101
+ ``core/exampleInstance/00Dxx...``). The tenant id is validated for shape
102
+ because it is forwarded as the ``x-sfdc-core-tenant-id`` header, but it no
103
+ longer selects the host.
104
+
105
+ Args:
106
+ tenant_id: The core tenant id (validated for shape when
107
+ ``BYOC_PROXY_URL`` is unset; not used to select the host).
108
+
109
+ Returns:
110
+ The base URL (no trailing slash, no endpoint path).
111
+
112
+ Raises:
113
+ ValueError: If ``BYOC_PROXY_URL`` is unset and the tenant id is
114
+ malformed.
115
+ """
116
+ proxy_url = os.getenv(PROXY_URL_ENV_VAR)
117
+ if proxy_url:
118
+ return proxy_url.rstrip("/")
119
+
120
+ if not tenant_id or not isinstance(tenant_id, str):
121
+ raise ValueError(f"Invalid tenant id: {tenant_id!r}")
122
+
123
+ parts = tenant_id.split("/")
124
+ if len(parts) != 3 or parts[0] != "core" or not parts[1] or not parts[2]:
125
+ raise ValueError(
126
+ f"Malformed tenant id {tenant_id!r}; expected " "'core/<instance>-<fd>/<org-id>'."
127
+ )
128
+
129
+ return _DEFAULT_SFAP_BASE_URL
130
+
131
+
132
+ def resolve_relay_url(tenant_id: str) -> str:
133
+ """
134
+ Resolve the full Relay API endpoint URL (``.../byoc/service``).
135
+
136
+ The URL comes from :func:`resolve_sfap_base_url` — ``BYOC_PROXY_URL`` when
137
+ set, else the production default:
138
+
139
+ - If it already ends in ``/byoc/service`` (the Agentforce runtime injects
140
+ ``BYOC_PROXY_URL`` into the managed sandbox as the full relay URL), it is
141
+ used as-is. Appending again would produce
142
+ ``.../byoc/service/byoc/service`` and a 404.
143
+ - Otherwise it is a base host (the production default, or a locally
144
+ exported ``BYOC_PROXY_URL`` — the same value the deploy CLI uses) and
145
+ ``/byoc/service`` is appended.
146
+
147
+ Args:
148
+ tenant_id: The core tenant id (validated for shape on the default path).
149
+
150
+ Returns:
151
+ The full relay endpoint URL (no trailing slash).
152
+
153
+ Raises:
154
+ ValueError: If ``BYOC_PROXY_URL`` is unset and the tenant id is
155
+ malformed.
156
+ """
157
+ url = resolve_sfap_base_url(tenant_id)
158
+ if url.endswith(_RELAY_PATH):
159
+ return url
160
+ return f"{url}{_RELAY_PATH}"
161
+
162
+
163
+ class AgentforceRelayGatewayClient(BaseRelayGatewayClient):
164
+ """
165
+ Relay gateway client for the Agentforce BYOC runtime.
166
+
167
+ Used when running in the managed sandbox. The org JWT, tenant id, user id,
168
+ and app context are all read from environment variables injected into the
169
+ managed sandbox (``ORG_JWT_TOKEN``, ``SFDC_CORE_TENANT_ID``, ``SFDC_USER_ID``,
170
+ ``SFDC_APP_CONTEXT``); the optional client feature id and trace id come from
171
+ ``SFDC_FEATURE_ID`` / ``SFDC_TRACE_ID`` when injected. The Relay API endpoint
172
+ is resolved by :func:`resolve_relay_url`: ``BYOC_PROXY_URL`` as-is when it
173
+ already ends in ``/byoc/service`` (as the runtime injects it), otherwise
174
+ ``/byoc/service`` appended to the ``BYOC_PROXY_URL`` base host or the
175
+ production default.
176
+
177
+ This is the single shared client for all Relay functions. To add a new
178
+ relay function, add a thin method that calls :meth:`_relay` with the
179
+ function name (and a constant for it above).
180
+ """
181
+
182
+ def call_llm_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
183
+ return self._relay(RELAY_FUNCTION_LLM, parameter)
184
+
185
+ def call_llm_chat_generations(self, parameter: Dict[str, Any]) -> Dict[str, Any]:
186
+ return self._relay(RELAY_FUNCTION_LLM_CHAT, parameter)
187
+
188
+ # ------------------------------------------------------------------
189
+ # Shared helpers
190
+ # ------------------------------------------------------------------
191
+
192
+ def _get_org_jwt(self) -> str:
193
+ """
194
+ Resolve the org JWT.
195
+
196
+ Reads ``ORG_JWT_TOKEN`` from the environment (injected by the
197
+ Agentforce runtime into the managed sandbox).
198
+
199
+ Raises:
200
+ RuntimeError: If the variable is not set.
201
+ """
202
+ org_jwt = os.getenv(ORG_JWT_ENV_VAR)
203
+ if not org_jwt:
204
+ raise RuntimeError(
205
+ f"{ORG_JWT_ENV_VAR} environment variable is not set. "
206
+ "It is injected by the Agentforce runtime; for local testing "
207
+ f"export {ORG_JWT_ENV_VAR}."
208
+ )
209
+ return org_jwt
210
+
211
+ def _get_tenant_id(self) -> str:
212
+ """
213
+ Resolve the core tenant id.
214
+
215
+ Reads ``SFDC_CORE_TENANT_ID`` from the environment (injected by the
216
+ Agentforce runtime). It is forwarded as the ``x-sfdc-core-tenant-id``
217
+ header.
218
+
219
+ Raises:
220
+ RuntimeError: If the variable is not set.
221
+ """
222
+ tenant_id = os.getenv(CORE_TENANT_ID_ENV_VAR)
223
+ if not tenant_id:
224
+ raise RuntimeError(
225
+ f"{CORE_TENANT_ID_ENV_VAR} environment variable is not set. "
226
+ "It is injected by the Agentforce runtime; for local testing "
227
+ f"export {CORE_TENANT_ID_ENV_VAR}."
228
+ )
229
+ return tenant_id
230
+
231
+ def _get_user_id(self) -> str:
232
+ """
233
+ Resolve the caller's user id.
234
+
235
+ Reads ``SFDC_USER_ID`` from the environment (injected by the Agentforce
236
+ runtime) and forwards it as the ``x-sfdc-user-id`` header.
237
+
238
+ Raises:
239
+ RuntimeError: If the variable is not set.
240
+ """
241
+ user_id = os.getenv(USER_ID_ENV_VAR)
242
+ if not user_id:
243
+ raise RuntimeError(
244
+ f"{USER_ID_ENV_VAR} environment variable is not set. "
245
+ "It is injected by the Agentforce runtime; for local testing "
246
+ f"export {USER_ID_ENV_VAR}."
247
+ )
248
+ return user_id
249
+
250
+ def _get_app_context(self) -> str:
251
+ """
252
+ Resolve the invoke's app context.
253
+
254
+ Reads ``SFDC_APP_CONTEXT`` from the environment (injected by the
255
+ Agentforce runtime) and forwards it as the ``x-sfdc-app-context``
256
+ header. The relay endpoint requires this header (400
257
+ ``header_invalid`` without it).
258
+
259
+ Raises:
260
+ RuntimeError: If the variable is not set.
261
+ """
262
+ app_context = os.getenv(APP_CONTEXT_ENV_VAR)
263
+ if not app_context:
264
+ raise RuntimeError(
265
+ f"{APP_CONTEXT_ENV_VAR} environment variable is not set. "
266
+ "It is injected by the Agentforce runtime; for local testing "
267
+ f"export {APP_CONTEXT_ENV_VAR}."
268
+ )
269
+ return app_context
270
+
271
+ def _get_feature_id(self) -> Optional[str]:
272
+ """
273
+ Resolve the optional client feature id.
274
+
275
+ Reads ``SFDC_FEATURE_ID`` from the environment (injected by the
276
+ Agentforce runtime) to be forwarded as the ``x-client-feature-id``
277
+ header, which the LLM Gateway uses to attribute the call to a feature.
278
+
279
+ Unlike the mandatory identity vars, this is OPTIONAL: returns ``None``
280
+ when unset (or blank) so the caller omits the header entirely.
281
+ """
282
+ return os.getenv(FEATURE_ID_ENV_VAR) or None
283
+
284
+ def _get_trace_id(self) -> Optional[str]:
285
+ """
286
+ Resolve the optional B3 trace id.
287
+
288
+ Reads ``SFDC_TRACE_ID`` from the environment (injected by the
289
+ Agentforce runtime) to be forwarded as the ``x-b3-traceid`` header,
290
+ propagating the distributed trace across the relay.
291
+
292
+ Unlike the mandatory identity vars, this is OPTIONAL: returns ``None``
293
+ when unset (or blank) so the caller omits the header entirely.
294
+ """
295
+ return os.getenv(TRACE_ID_ENV_VAR) or None
296
+
297
+ def _build_request_body(self, function: str, parameter: Dict[str, Any]) -> Dict[str, Any]:
298
+ """
299
+ Construct the Relay request body.
300
+
301
+ The Relay API (``RelayRequest``) accepts only ``function`` and
302
+ ``parameter``. The tenant and org JWT travel as headers, not in the
303
+ body (see :meth:`_post`).
304
+ """
305
+ return {
306
+ "function": function,
307
+ "parameter": parameter,
308
+ }
309
+
310
+ def _post(
311
+ self,
312
+ function: str,
313
+ body: Dict[str, Any],
314
+ org_jwt: str,
315
+ ) -> Dict[str, Any]:
316
+ """
317
+ Resolve the target URL + identity headers and POST the JSON body,
318
+ returning the parsed JSON response.
319
+
320
+ The endpoint URL comes from :func:`resolve_relay_url`: ``BYOC_PROXY_URL``
321
+ as-is when it already ends in ``/byoc/service``, otherwise
322
+ ``/byoc/service`` appended to the base host (``BYOC_PROXY_URL`` or the
323
+ production default). The tenant id is
324
+ sent as the ``x-sfdc-core-tenant-id`` header. The org JWT
325
+ is sent as a bearer token; the app context and user id are resolved
326
+ from the injected environment and sent as the ``x-sfdc-app-context`` /
327
+ ``x-sfdc-user-id`` headers.
328
+
329
+ Two OPTIONAL client-context headers are forwarded only when their env
330
+ vars are set: ``x-client-feature-id`` (from ``SFDC_FEATURE_ID``, used by
331
+ the LLM Gateway for feature attribution) and ``x-b3-traceid`` (from
332
+ ``SFDC_TRACE_ID``, for distributed-trace propagation). When either env
333
+ var is unset the corresponding header is omitted entirely.
334
+
335
+ Args:
336
+ function: The relay function name (used only for the log line).
337
+ body: The relay request body (``function`` + ``parameter``).
338
+ org_jwt: The org JWT bearer token.
339
+
340
+ Raises:
341
+ RuntimeError: On transport error, non-JSON response, or if any of
342
+ the injected identity env vars (tenant id, app context, user
343
+ id) is unset.
344
+ """
345
+ tenant_id = self._get_tenant_id()
346
+ url = resolve_relay_url(tenant_id)
347
+ # The org JWT is sent as a bearer token; the tenant id also travels as
348
+ # the x-sfdc-core-tenant-id header. A 400 "x-sfdc-core-tenant-id header
349
+ # is required" indicates the JWT was rejected, not a missing header here.
350
+ # x-sfdc-app-context is mandatory on the relay endpoint (400
351
+ # header_invalid without it); forward the injected value.
352
+ headers = {
353
+ "Authorization": f"Bearer {org_jwt}",
354
+ "x-sfdc-core-tenant-id": tenant_id,
355
+ "x-sfdc-app-context": self._get_app_context(),
356
+ "x-sfdc-user-id": self._get_user_id(),
357
+ }
358
+ # Optional client-context headers: forwarded only when the injected env
359
+ # var is present, so agents that don't set them are unaffected.
360
+ feature_id = self._get_feature_id()
361
+ if feature_id:
362
+ headers["x-client-feature-id"] = feature_id
363
+ trace_id = self._get_trace_id()
364
+ if trace_id:
365
+ headers["x-b3-traceid"] = trace_id
366
+ logger.info(
367
+ "Relaying %s for tenant=%s to %s",
368
+ function,
369
+ tenant_id,
370
+ url,
371
+ )
372
+ try:
373
+ status, raw = _http.post_json(url, body, headers, timeout=_RELAY_TIMEOUT_S)
374
+ except OSError as e:
375
+ raise RuntimeError(f"Relay API call to {url} failed: {e}") from e
376
+
377
+ if status >= 400:
378
+ raise RuntimeError(f"Relay API call to {url} failed with status {status}: {raw}")
379
+
380
+ try:
381
+ return cast(Dict[str, Any], json.loads(raw))
382
+ except json.JSONDecodeError as e:
383
+ raise RuntimeError(f"Relay API at {url} returned a non-JSON response: {raw!r}") from e
384
+
385
+ def _relay(self, function: str, parameter: Dict[str, Any]) -> Dict[str, Any]:
386
+ """Resolve the org JWT, build the body, and POST to the Relay API.
387
+
388
+ The org JWT is read from the environment; ``_post`` resolves the
389
+ tenant id (for both the endpoint URL and its header), the app context,
390
+ and the user id from the injected environment itself.
391
+ """
392
+ org_jwt = self._get_org_jwt()
393
+ body = self._build_request_body(function, parameter)
394
+ return self._post(function, body, org_jwt)
@@ -0,0 +1,20 @@
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
+ """Structured logging for Agentforce BYOC SDK."""
17
+
18
+ from agentforce_byoc.logging.logger import get_logger
19
+
20
+ __all__ = ["get_logger"]
@@ -0,0 +1,96 @@
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
+ """JSON formatter for structured logging."""
17
+
18
+ import json
19
+ import logging
20
+ from datetime import datetime
21
+ from typing import Any, Dict
22
+
23
+
24
+ class JSONFormatter(logging.Formatter):
25
+ """
26
+ JSON formatter for structured logging.
27
+
28
+ Outputs log records as JSON objects with fields:
29
+ - timestamp: ISO 8601 timestamp
30
+ - level: Log level name
31
+ - logger: Logger name
32
+ - message: Log message
33
+ - [extra fields]: Any extra fields passed via extra parameter
34
+
35
+ Example:
36
+ >>> handler = logging.StreamHandler()
37
+ >>> handler.setFormatter(JSONFormatter())
38
+ >>> logger.addHandler(handler)
39
+ >>> logger.info("Request processed", extra={"request_id": "123", "duration_ms": 45})
40
+ # Output: {"timestamp": "2024-02-10T10:30:00.000Z", "level": "INFO",
41
+ # "logger": "mymodule", "message": "Request processed",
42
+ # "request_id": "123", "duration_ms": 45}
43
+ """
44
+
45
+ def format(self, record: logging.LogRecord) -> str:
46
+ """
47
+ Format log record as JSON string.
48
+
49
+ Args:
50
+ record: Log record to format
51
+
52
+ Returns:
53
+ JSON formatted string
54
+ """
55
+ log_data: Dict[str, Any] = {
56
+ "timestamp": datetime.utcnow().isoformat() + "Z",
57
+ "level": record.levelname,
58
+ "logger": record.name,
59
+ "message": record.getMessage(),
60
+ }
61
+
62
+ # Add exception info if present
63
+ if record.exc_info:
64
+ log_data["exception"] = self.formatException(record.exc_info)
65
+
66
+ # Add extra fields (passed via extra parameter)
67
+ if hasattr(record, "__dict__"):
68
+ for key, value in record.__dict__.items():
69
+ # Skip standard logging fields
70
+ if key not in [
71
+ "name",
72
+ "msg",
73
+ "args",
74
+ "created",
75
+ "filename",
76
+ "funcName",
77
+ "levelname",
78
+ "levelno",
79
+ "lineno",
80
+ "module",
81
+ "msecs",
82
+ "message",
83
+ "pathname",
84
+ "process",
85
+ "processName",
86
+ "relativeCreated",
87
+ "thread",
88
+ "threadName",
89
+ "exc_info",
90
+ "exc_text",
91
+ "stack_info",
92
+ "taskName",
93
+ ]:
94
+ log_data[key] = value
95
+
96
+ return json.dumps(log_data)
@@ -0,0 +1,59 @@
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
+ """Logger utilities for Agentforce BYOC SDK."""
17
+
18
+ import logging
19
+ import sys
20
+ from typing import Optional
21
+
22
+
23
+ def get_logger(name: str, level: Optional[int] = None) -> logging.Logger:
24
+ """
25
+ Get a configured logger instance.
26
+
27
+ Args:
28
+ name: Logger name (typically __name__ of the module)
29
+ level: Optional logging level (defaults to INFO)
30
+
31
+ Returns:
32
+ Configured logger instance
33
+
34
+ Example:
35
+ >>> from agentforce_byoc.logging import get_logger
36
+ >>> logger = get_logger(__name__)
37
+ >>> logger.info("Processing request", extra={"request_id": "123"})
38
+ """
39
+ logger = logging.getLogger(name)
40
+
41
+ # Set level
42
+ if level is None:
43
+ level = logging.INFO
44
+ logger.setLevel(level)
45
+
46
+ # Avoid duplicate handlers
47
+ if not logger.handlers:
48
+ # Console handler
49
+ handler = logging.StreamHandler(sys.stdout)
50
+ handler.setLevel(level)
51
+
52
+ # Format: timestamp - name - level - message
53
+ formatter = logging.Formatter(
54
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
55
+ )
56
+ handler.setFormatter(formatter)
57
+ logger.addHandler(handler)
58
+
59
+ return logger
@@ -0,0 +1,21 @@
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
+ """Runtime environment providers."""
17
+
18
+ from agentforce_byoc.runtime.agentforce_runtime import AgentforceRuntimeProvider
19
+ from agentforce_byoc.runtime.mock_runtime import MockRuntimeProvider
20
+
21
+ __all__ = ["AgentforceRuntimeProvider", "MockRuntimeProvider"]
@@ -0,0 +1,47 @@
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
+ """Agentforce BYOC runtime provider."""
17
+
18
+ from agentforce_byoc.gateway.relay_client import AgentforceRelayGatewayClient
19
+ from agentforce_byoc.logging import get_logger
20
+ from agentforce_byoc.runtime.base import BaseRuntimeProvider
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class AgentforceRuntimeProvider(BaseRuntimeProvider):
26
+ """
27
+ Agentforce BYOC runtime provider.
28
+
29
+ This is the default runtime, used when running in the managed sandbox. It
30
+ wires up the :class:`AgentforceRelayGatewayClient`, which relays calls
31
+ through the Agentforce Relay API.
32
+
33
+ The org JWT is read from the ``ORG_JWT_TOKEN`` environment variable
34
+ injected into the managed sandbox, and the endpoint URL is resolved per call
35
+ from the tenant id.
36
+ """
37
+
38
+ def __init__(self):
39
+ """Initialize the Agentforce runtime provider."""
40
+ super().__init__()
41
+ logger.info("Initialized Agentforce BYOC runtime")
42
+ self._relay_gateway_client = AgentforceRelayGatewayClient()
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ """Return runtime name."""
47
+ return "agentforce"