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.
- agentforce_byoc/__init__.py +26 -0
- agentforce_byoc/_http.py +142 -0
- agentforce_byoc/cli/__init__.py +8 -0
- agentforce_byoc/cli/commands.py +913 -0
- agentforce_byoc/cli/main.py +44 -0
- agentforce_byoc/client.py +224 -0
- agentforce_byoc/deploy_client.py +246 -0
- agentforce_byoc/gateway/__init__.py +26 -0
- agentforce_byoc/gateway/base.py +66 -0
- agentforce_byoc/gateway/mock_relay_client.py +54 -0
- agentforce_byoc/gateway/relay_client.py +394 -0
- agentforce_byoc/logging/__init__.py +20 -0
- agentforce_byoc/logging/formatter.py +96 -0
- agentforce_byoc/logging/logger.py +59 -0
- agentforce_byoc/runtime/__init__.py +21 -0
- agentforce_byoc/runtime/agentforce_runtime.py +47 -0
- agentforce_byoc/runtime/base.py +54 -0
- agentforce_byoc/runtime/mock_runtime.py +50 -0
- agentforce_byoc/schema_utils/__init__.py +35 -0
- agentforce_byoc/schema_utils/entry_func.py +48 -0
- agentforce_byoc/schema_utils/generator.py +694 -0
- agentforce_byoc/schema_utils/schema.py +79 -0
- agentforce_byoc/templates/__init__.py +4 -0
- agentforce_byoc/templates/agent/.dockerignore.template +51 -0
- agentforce_byoc/templates/agent/.gitignore.template +64 -0
- agentforce_byoc/templates/agent/.python-version.template +1 -0
- agentforce_byoc/templates/agent/Dockerfile.template +41 -0
- agentforce_byoc/templates/agent/Makefile.template +51 -0
- agentforce_byoc/templates/agent/README.md.template +358 -0
- agentforce_byoc/templates/agent/config.json.template +3 -0
- agentforce_byoc/templates/agent/entry_func.py.template +51 -0
- agentforce_byoc/templates/agent/main.py.template +149 -0
- agentforce_byoc/templates/agent/pyproject.toml.template +24 -0
- agentforce_byoc/templates/agent/schema.py.template +73 -0
- sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/METADATA +439 -0
- sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/RECORD +39 -0
- sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/WHEEL +4 -0
- sfdc_agentforce_byoc_python_sdk-0.1.0.dist-info/entry_points.txt +3 -0
- 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"
|