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,149 @@
|
|
|
1
|
+
"""{{project_name}} - Agentforce BYOC Agent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
# The agentforce_byoc SDK is vendored into this project directory (it is not
|
|
7
|
+
# yet on PyPI). Importing it gives access to the relay client + helpers.
|
|
8
|
+
from agentforce_byoc import get_client, get_logger
|
|
9
|
+
from agentforce_byoc.schema_utils import entry_func, requestSchema, responseSchema
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
# Default prompt for the local-run sample request only (see `main`). A real
|
|
14
|
+
# invoke must supply `text`; `function` returns an input error if it is missing.
|
|
15
|
+
_DEFAULT_PROMPT = "Hello, Agentforce, What is the best place to eat dumplings in Seattle!"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# TODO: Define your function schemas here.
|
|
19
|
+
class ExampleRequest(TypedDict):
|
|
20
|
+
"""Request schema - define your input fields here."""
|
|
21
|
+
|
|
22
|
+
text: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExampleResponse(TypedDict):
|
|
26
|
+
"""Response schema - define your output fields here."""
|
|
27
|
+
|
|
28
|
+
result: str
|
|
29
|
+
status: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@entry_func
|
|
33
|
+
@requestSchema(ExampleRequest)
|
|
34
|
+
@responseSchema(ExampleResponse)
|
|
35
|
+
def function(request: dict) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
Entry point called by the Agentforce BYOC runtime.
|
|
38
|
+
|
|
39
|
+
Relays a chat-generations call through the Agentforce Relay API.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
request: Input request dictionary from Agentforce. Expected fields:
|
|
43
|
+
- text: the user message
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Response dictionary {"result": ..., "status": ...}
|
|
47
|
+
|
|
48
|
+
TODO: Implement your agent logic here.
|
|
49
|
+
"""
|
|
50
|
+
logger.info("Processing request", extra={"request_keys": list(request.keys())})
|
|
51
|
+
|
|
52
|
+
# Validate the caller-supplied `text`. Missing, null, or empty `text` is an
|
|
53
|
+
# invalid invoke: return an input error instead of calling the relay, so an
|
|
54
|
+
# empty invocation never incurs model traffic. (`_DEFAULT_PROMPT` is only for
|
|
55
|
+
# local runs — `main()` uses it when there is no runtime-bound input.)
|
|
56
|
+
text = request.get("text")
|
|
57
|
+
if not text:
|
|
58
|
+
logger.warning("Missing or empty 'text' field in request")
|
|
59
|
+
return {"result": "", "status": "error"}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
client = get_client()
|
|
63
|
+
|
|
64
|
+
# Relay a chat-generations call. `parameter` is the raw request body
|
|
65
|
+
# for the LLM chat service and is passed through to the Relay API. The
|
|
66
|
+
# tenant id, user id, and app context are resolved from the
|
|
67
|
+
# runtime-injected environment (SFDC_CORE_TENANT_ID / SFDC_USER_ID /
|
|
68
|
+
# SFDC_APP_CONTEXT).
|
|
69
|
+
relay_response = client.call_llm_chat_generations(
|
|
70
|
+
parameter={
|
|
71
|
+
"messages": [{"role": "user", "content": text}],
|
|
72
|
+
"generation_settings": {"max_tokens": 50},
|
|
73
|
+
"model": "llmgateway__EinsteinLlama4Maverick",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info("Relay response received")
|
|
78
|
+
return {"result": str(relay_response), "status": "success"}
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Request processing failed: {e}", exc_info=True)
|
|
82
|
+
return {"result": "", "status": "error"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ============================================================================
|
|
86
|
+
# Module-Level State (Optional)
|
|
87
|
+
# ============================================================================
|
|
88
|
+
# If you need state that persists across requests, use module-level variables:
|
|
89
|
+
#
|
|
90
|
+
# _request_count = 0
|
|
91
|
+
# _cache = {}
|
|
92
|
+
#
|
|
93
|
+
# def function(request: dict) -> dict:
|
|
94
|
+
# global _request_count
|
|
95
|
+
# _request_count += 1
|
|
96
|
+
# ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ============================================================================
|
|
100
|
+
# Entry Point for Managed Sandbox / Module Execution
|
|
101
|
+
# ============================================================================
|
|
102
|
+
def main():
|
|
103
|
+
"""
|
|
104
|
+
Entry point for Agentforce runtime execution.
|
|
105
|
+
|
|
106
|
+
- Agentforce runtime: import main; main.main()
|
|
107
|
+
- Script execution: python main.py (calls main via __name__ == "__main__")
|
|
108
|
+
|
|
109
|
+
The invoke's ``input`` payload is bound by the runtime as the module global
|
|
110
|
+
``byoc_input`` before this file runs. We read it and pass it straight to
|
|
111
|
+
``function``. When running locally (``python main.py``), that global is not
|
|
112
|
+
bound, so we fall back to a sample request. Only an *unbound* global selects
|
|
113
|
+
the sample — a request the runtime explicitly binds is respected as-is, even
|
|
114
|
+
an empty ``{}`` or ``None``, so an in-runtime invoke never silently runs the
|
|
115
|
+
local sample.
|
|
116
|
+
"""
|
|
117
|
+
print("Testing {{project_name}} agent...")
|
|
118
|
+
print()
|
|
119
|
+
|
|
120
|
+
# `byoc_input` is injected by the Agentforce runtime from the invoke's
|
|
121
|
+
# `input` field. A unique sentinel as the `globals().get(...)` default lets
|
|
122
|
+
# us distinguish "the runtime never bound the global" (local run → use the
|
|
123
|
+
# sample) from "the runtime bound it to None/{}" (respect it). A plain
|
|
124
|
+
# `is None` check would conflate the two and run the sample on a real null
|
|
125
|
+
# invoke.
|
|
126
|
+
# TODO: replace the placeholder request text used for local runs.
|
|
127
|
+
_UNBOUND = object()
|
|
128
|
+
request = globals().get("byoc_input", _UNBOUND)
|
|
129
|
+
if request is _UNBOUND:
|
|
130
|
+
request = {"text": _DEFAULT_PROMPT}
|
|
131
|
+
|
|
132
|
+
print(f"Request: {request}")
|
|
133
|
+
response = function(request)
|
|
134
|
+
print(f"Response: {response}")
|
|
135
|
+
|
|
136
|
+
# Emit the response as a `result: <json>` line. The Agentforce runtime scans
|
|
137
|
+
# stdout for this line and returns the parsed value as the invoke response's
|
|
138
|
+
# `result` field; without it, `result` comes back null.
|
|
139
|
+
print(f"result: {json.dumps(response)}")
|
|
140
|
+
print()
|
|
141
|
+
|
|
142
|
+
return response
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ============================================================================
|
|
146
|
+
# Local Testing
|
|
147
|
+
# ============================================================================
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "{{project_name}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agentforce BYOC Agent"
|
|
5
|
+
authors = ["Your Name <you@example.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "main.py"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11"
|
|
11
|
+
# TODO(WI): the SDK is vendored into this project (the agentforce_byoc/
|
|
12
|
+
# directory copied in by `agentforce init`) because it is not yet on PyPI.
|
|
13
|
+
# Once it is published, REMOVE the vendored agentforce_byoc/ directory and
|
|
14
|
+
# declare the dependency here instead:
|
|
15
|
+
# agentforce-byoc = "^0.1.0"
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^7.4.0"
|
|
19
|
+
pytest-mock = "^3.11.1"
|
|
20
|
+
black = "^24.0.0"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright (c) 2025, Salesforce, Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable, Type, TypeVar
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def requestSchema(cls: Type[Any]) -> Callable[[F], F]:
|
|
24
|
+
"""Decorator that declares the request body schema for an ``@entry_func`` function.
|
|
25
|
+
|
|
26
|
+
The *cls* argument must be a class with typed fields (e.g. a
|
|
27
|
+
``TypedDict`` subclass). At runtime the decorator is a pass-through —
|
|
28
|
+
it returns the original function unchanged. The schema generator
|
|
29
|
+
reads the decorator at the AST level to produce OpenAPI ``properties``.
|
|
30
|
+
|
|
31
|
+
Example::
|
|
32
|
+
|
|
33
|
+
from typing import TypedDict
|
|
34
|
+
from schema import requestSchema
|
|
35
|
+
|
|
36
|
+
class AddRequest(TypedDict):
|
|
37
|
+
a: int
|
|
38
|
+
b: int
|
|
39
|
+
|
|
40
|
+
@entry_func
|
|
41
|
+
@requestSchema(AddRequest)
|
|
42
|
+
def add(request: dict) -> dict:
|
|
43
|
+
...
|
|
44
|
+
"""
|
|
45
|
+
def decorator(fn: F) -> F:
|
|
46
|
+
return fn
|
|
47
|
+
return decorator
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def responseSchema(cls: Type[Any]) -> Callable[[F], F]:
|
|
51
|
+
"""Decorator that declares the response body schema for an ``@entry_func`` function.
|
|
52
|
+
|
|
53
|
+
The *cls* argument must be a class with typed fields (e.g. a
|
|
54
|
+
``TypedDict`` subclass). At runtime the decorator is a pass-through —
|
|
55
|
+
it returns the original function unchanged. The schema generator
|
|
56
|
+
reads the decorator at the AST level to produce OpenAPI ``properties``.
|
|
57
|
+
|
|
58
|
+
Example::
|
|
59
|
+
|
|
60
|
+
from typing import TypedDict
|
|
61
|
+
from schema import responseSchema
|
|
62
|
+
|
|
63
|
+
class AddResponse(TypedDict):
|
|
64
|
+
result: int
|
|
65
|
+
|
|
66
|
+
@entry_func
|
|
67
|
+
@responseSchema(AddResponse)
|
|
68
|
+
def add(request: dict) -> dict:
|
|
69
|
+
...
|
|
70
|
+
"""
|
|
71
|
+
def decorator(fn: F) -> F:
|
|
72
|
+
return fn
|
|
73
|
+
return decorator
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sfdc-agentforce-byoc-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Agentforce BYOC (Bring Your Own Code)
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: agentforce,byoc,salesforce,sdk
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
20
|
+
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
21
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
22
|
+
Project-URL: Homepage, https://github.com/salesforce/agentforce-byoc-python-sdk
|
|
23
|
+
Project-URL: Repository, https://github.com/salesforce/agentforce-byoc-python-sdk
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Agentforce BYOC Python SDK
|
|
27
|
+
|
|
28
|
+
Python SDK for building custom agents with Agentforce BYOC (Bring Your Own Code).
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
This SDK provides a straightforward way to build agents that integrate with Agentforce. Agents are simple Python functions that receive requests and return responses. The SDK handles relaying LLM calls through the Agentforce platform and structured logging.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Functional API**: Write agents as simple functions
|
|
37
|
+
- **Agentforce BYOC runtime**: Relay LLM calls through the Agentforce platform
|
|
38
|
+
- **LLM Gateway Integration**: Call language models from the Agentforce runtime
|
|
39
|
+
- **Poetry Support**: Modern dependency management
|
|
40
|
+
- **CLI Tools**: Commands for project initialization, testing, and deployment
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- Python 3.11+
|
|
45
|
+
- Poetry for dependency management
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### From PyPI (available once published)
|
|
50
|
+
|
|
51
|
+
Once the package is published to PyPI, this is the official way to install the
|
|
52
|
+
SDK and its `agentforce` CLI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install sfdc-agentforce-byoc-python-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Development Setup
|
|
59
|
+
|
|
60
|
+
1. Clone the repository:
|
|
61
|
+
```bash
|
|
62
|
+
git clone <repository-url>
|
|
63
|
+
cd agentforce-byoc-python-sdk
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
2. Install dependencies:
|
|
67
|
+
```bash
|
|
68
|
+
make setup
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. Verify installation:
|
|
72
|
+
```bash
|
|
73
|
+
poetry run agentforce --version
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
### Create a New Agent
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
agentforce init my-agent
|
|
82
|
+
cd my-agent
|
|
83
|
+
poetry install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Write Your Agent
|
|
87
|
+
|
|
88
|
+
The generated `main.py` provides a working template. The entry point is a
|
|
89
|
+
function named `function` decorated with the SDK's schema helpers:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from agentforce_byoc import get_client, get_logger
|
|
93
|
+
from agentforce_byoc.schema_utils import entry_func, requestSchema, responseSchema
|
|
94
|
+
|
|
95
|
+
logger = get_logger(__name__)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@entry_func
|
|
99
|
+
@requestSchema(ExampleRequest)
|
|
100
|
+
@responseSchema(ExampleResponse)
|
|
101
|
+
def function(request: dict) -> dict:
|
|
102
|
+
"""Entry point called by the Agentforce BYOC runtime."""
|
|
103
|
+
text = request.get("text", "")
|
|
104
|
+
if not text:
|
|
105
|
+
return {"result": "", "status": "error"}
|
|
106
|
+
|
|
107
|
+
# Relay a chat-generations call through the Agentforce Relay API. The
|
|
108
|
+
# tenant id, user id, and app context are read from the environment
|
|
109
|
+
# (SFDC_CORE_TENANT_ID / SFDC_USER_ID / SFDC_APP_CONTEXT), which the
|
|
110
|
+
# runtime injects.
|
|
111
|
+
client = get_client()
|
|
112
|
+
relay_response = client.call_llm_chat_generations(
|
|
113
|
+
parameter={"messages": [{"role": "user", "content": text}]},
|
|
114
|
+
)
|
|
115
|
+
return {"result": str(relay_response), "status": "success"}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The template's `main()` builds a sample request (with a `text` placeholder to
|
|
119
|
+
fill in) and calls `function(...)` so you can test with `agentforce run`.
|
|
120
|
+
|
|
121
|
+
### Test Your Agent
|
|
122
|
+
|
|
123
|
+
Run against the Agentforce BYOC runtime (set auth + tenant + user + app context
|
|
124
|
+
first — commands abort if unset):
|
|
125
|
+
```bash
|
|
126
|
+
export ORG_JWT_TOKEN=<your-org-jwt>
|
|
127
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
128
|
+
export SFDC_USER_ID=<user-id>
|
|
129
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
130
|
+
|
|
131
|
+
agentforce run main.py
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Package and Deploy
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Validate your agent
|
|
138
|
+
agentforce validate .
|
|
139
|
+
|
|
140
|
+
# Build end-to-end (validate -> run -> zip)
|
|
141
|
+
agentforce package .
|
|
142
|
+
|
|
143
|
+
# Register + upload to Agentforce BYOC in one step, then poll status
|
|
144
|
+
# (set all four identity env vars first; required)
|
|
145
|
+
export ORG_JWT_TOKEN=<jwt>
|
|
146
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
147
|
+
export SFDC_USER_ID=<user-id>
|
|
148
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
149
|
+
agentforce deploy --package-name "my-agent" --package-file my-agent-0.1.0.tar.gz
|
|
150
|
+
agentforce deploy-status --package-name "my-agent"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## API Reference
|
|
154
|
+
|
|
155
|
+
### `get_client() -> Client`
|
|
156
|
+
|
|
157
|
+
Returns the singleton Client instance for relaying calls through the Agentforce Relay API.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from agentforce_byoc import get_client
|
|
161
|
+
|
|
162
|
+
client = get_client()
|
|
163
|
+
response = client.call_llm_generations(
|
|
164
|
+
parameter={"prompt": "What is AI?"},
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The client exposes one method per Relay function:
|
|
169
|
+
- `call_llm_generations(parameter)` — Relay function `llm`
|
|
170
|
+
- `call_llm_chat_generations(parameter)` — Relay function `llm_chat`
|
|
171
|
+
|
|
172
|
+
`parameter` is the raw request body and is passed through to the Relay API
|
|
173
|
+
unchanged. The tenant id, user id, and app context are read from the
|
|
174
|
+
environment (`SFDC_CORE_TENANT_ID` / `SFDC_USER_ID` / `SFDC_APP_CONTEXT`),
|
|
175
|
+
which the runtime injects in the managed sandbox. The endpoint URL is
|
|
176
|
+
`BYOC_PROXY_URL` as-is when it already ends in `/byoc/service` (as the runtime
|
|
177
|
+
injects it); otherwise `/byoc/service` is appended to the `BYOC_PROXY_URL` base
|
|
178
|
+
host (the same value `deploy` uses) or to `https://api.salesforce.com`.
|
|
179
|
+
|
|
180
|
+
**Runtime Selection:**
|
|
181
|
+
- **Agentforce BYOC runtime** (default): real Relay calls; org JWT from `ORG_JWT_TOKEN` (injected by the runtime)
|
|
182
|
+
- **Mock mode**: `AGENTFORCE_BYOC_MOCK` is set (truthy); org JWT supplied by the author (`ORG_JWT_TOKEN`)
|
|
183
|
+
|
|
184
|
+
### `get_logger(name: str) -> Logger`
|
|
185
|
+
|
|
186
|
+
Returns a structured logger for your module.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from agentforce_byoc import get_logger
|
|
190
|
+
|
|
191
|
+
logger = get_logger(__name__)
|
|
192
|
+
logger.info("Processing request", extra={"user_id": "123"})
|
|
193
|
+
logger.error("Request failed", exc_info=True)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## State Management
|
|
197
|
+
|
|
198
|
+
Use module-level variables to maintain state across requests:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from agentforce_byoc import get_client, get_logger
|
|
202
|
+
|
|
203
|
+
logger = get_logger(__name__)
|
|
204
|
+
|
|
205
|
+
# Module-level state persists across function calls
|
|
206
|
+
_request_count = 0
|
|
207
|
+
_cache = {}
|
|
208
|
+
|
|
209
|
+
def function(request: dict) -> dict:
|
|
210
|
+
global _request_count
|
|
211
|
+
_request_count += 1
|
|
212
|
+
|
|
213
|
+
query = request.get("query", "")
|
|
214
|
+
|
|
215
|
+
# Check cache
|
|
216
|
+
if query in _cache:
|
|
217
|
+
return {"response": _cache[query], "cached": True}
|
|
218
|
+
|
|
219
|
+
# Relay an LLM generations call
|
|
220
|
+
client = get_client()
|
|
221
|
+
response = client.call_llm_generations(
|
|
222
|
+
parameter={"prompt": query},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Update cache
|
|
226
|
+
_cache[query] = response
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"response": response,
|
|
230
|
+
"cached": False,
|
|
231
|
+
"request_number": _request_count
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## CLI Commands
|
|
236
|
+
|
|
237
|
+
### `agentforce init [directory]`
|
|
238
|
+
|
|
239
|
+
Initialize a new agent project.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
agentforce init my-agent
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `agentforce run [entrypoint]`
|
|
246
|
+
|
|
247
|
+
Run agent locally against the Agentforce BYOC runtime (requires the env vars).
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
export ORG_JWT_TOKEN=<your-org-jwt>
|
|
251
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
252
|
+
export SFDC_USER_ID=<user-id>
|
|
253
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
254
|
+
agentforce run main.py
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `agentforce validate [path]`
|
|
258
|
+
|
|
259
|
+
Validate agent before deployment.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
agentforce validate .
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `agentforce package [path]`
|
|
266
|
+
|
|
267
|
+
Build end-to-end (validate → run → zip).
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
agentforce package .
|
|
271
|
+
agentforce package --output my-agent.tar.gz
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Auth/identity for `run`, `deploy`, and `deploy-status` come from environment
|
|
275
|
+
variables — all four of `ORG_JWT_TOKEN`, `SFDC_CORE_TENANT_ID`
|
|
276
|
+
(`core/<instance>-<fd>/<org-id>`), `SFDC_USER_ID`, and `SFDC_APP_CONTEXT`. `run`
|
|
277
|
+
relays through the Agentforce service and `deploy`/`deploy-status` call the BYOC
|
|
278
|
+
API; both send the user id and app context as `x-sfdc-user-id` /
|
|
279
|
+
`x-sfdc-app-context`. If a required var is unset, the command aborts with an
|
|
280
|
+
`export` instruction. (They are not prompted: an org JWT exceeds the terminal's
|
|
281
|
+
~1024-char line limit, so a typed prompt can't accept it.)
|
|
282
|
+
|
|
283
|
+
### `agentforce deploy`
|
|
284
|
+
|
|
285
|
+
Register and upload a packaged agent to Agentforce BYOC. A single
|
|
286
|
+
`POST /byoc/upload/request` registers the package metadata (size, entry point,
|
|
287
|
+
max live duration) **and** returns a presigned upload URL; the CLI then PUTs the
|
|
288
|
+
archive to that URL.
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
export ORG_JWT_TOKEN=<jwt>
|
|
292
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
293
|
+
export SFDC_USER_ID=<user-id>
|
|
294
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
295
|
+
agentforce deploy --package-name "my-agent" --package-file my-agent-0.1.0.tar.gz
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Options: `--package-name` and `--package-file` (required), plus `--entry-point`
|
|
299
|
+
(default `main.py`), `--version` (default `1.0`), and `--max-live-duration`
|
|
300
|
+
(default `300` seconds).
|
|
301
|
+
|
|
302
|
+
### `agentforce deploy-status`
|
|
303
|
+
|
|
304
|
+
Poll deployment status (read-only). Calls `GET /byoc/upload/status` and reports
|
|
305
|
+
`registrationStatus` (is the package registered) and `uploadStatus` (has the
|
|
306
|
+
archive been uploaded). Registration happens during `deploy`; this command never
|
|
307
|
+
writes.
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
agentforce deploy-status --package-name "my-agent" --version 1.0
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Architecture
|
|
314
|
+
|
|
315
|
+
### Agent Interface
|
|
316
|
+
|
|
317
|
+
Agents are implemented as simple functions:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
def function(request: dict) -> dict:
|
|
321
|
+
# Your code
|
|
322
|
+
return response
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The BYOC runtime calls this function for each request. The function receives a dictionary and returns a dictionary response
|
|
326
|
+
|
|
327
|
+
### Runtime Selection
|
|
328
|
+
|
|
329
|
+
The SDK supports two runtimes, selected on first `get_client()` call:
|
|
330
|
+
|
|
331
|
+
1. **Agentforce BYOC Runtime** (default)
|
|
332
|
+
- Real Relay API calls authenticated with the org JWT
|
|
333
|
+
- Org JWT read from `ORG_JWT_TOKEN` (injected by the runtime)
|
|
334
|
+
- Tenant id, user id, and app context read from `SFDC_CORE_TENANT_ID` /
|
|
335
|
+
`SFDC_USER_ID` / `SFDC_APP_CONTEXT` (injected by the runtime); endpoint
|
|
336
|
+
URL from `BYOC_PROXY_URL` (as-is if it ends in `/byoc/service`, else
|
|
337
|
+
with `/byoc/service` appended), else production + `/byoc/service`
|
|
338
|
+
|
|
339
|
+
2. **Mock Mode**: `AGENTFORCE_BYOC_MOCK` env var is set (truthy)
|
|
340
|
+
- Same relay behavior, with the org JWT supplied by the author
|
|
341
|
+
- JWT from the `MockRelayGatewayClient(org_jwt=...)` arg or `ORG_JWT_TOKEN`
|
|
342
|
+
- Selected via the env var (there is no CLI flag for it)
|
|
343
|
+
|
|
344
|
+
### Code Portability
|
|
345
|
+
|
|
346
|
+
Agent code works across both runtimes without modification:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
# This code works in both runtimes
|
|
350
|
+
def function(request: dict) -> dict:
|
|
351
|
+
client = get_client() # Selects runtime
|
|
352
|
+
return client.call_llm_generations(...)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Examples
|
|
356
|
+
|
|
357
|
+
The `examples/` directory contains working examples:
|
|
358
|
+
|
|
359
|
+
- **01_basic_agent.py** - Minimal functional agent
|
|
360
|
+
- **02_llm_agent.py** - LLM Gateway integration
|
|
361
|
+
- **03_sentiment_analysis.py** - Sentiment analysis use case
|
|
362
|
+
- **04_stateful_agent.py** - State management patterns
|
|
363
|
+
|
|
364
|
+
Run any example directly. Examples that call the relay need `ORG_JWT_TOKEN`,
|
|
365
|
+
`SFDC_CORE_TENANT_ID`, `SFDC_USER_ID`, and `SFDC_APP_CONTEXT` exported;
|
|
366
|
+
`01_basic_agent.py` does not call the relay and runs without credentials.
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
export ORG_JWT_TOKEN=<your-org-jwt>
|
|
370
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
371
|
+
export SFDC_USER_ID=<user-id>
|
|
372
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
373
|
+
python examples/01_basic_agent.py
|
|
374
|
+
python examples/02_llm_agent.py
|
|
375
|
+
python examples/03_sentiment_analysis.py
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
See [examples/README.md](examples/README.md) for detailed documentation.
|
|
379
|
+
|
|
380
|
+
## Development
|
|
381
|
+
|
|
382
|
+
### Project Structure
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
agentforce-byoc-python-sdk/
|
|
386
|
+
├── agentforce_byoc/ # Main package
|
|
387
|
+
│ ├── client.py # Client with runtime selection
|
|
388
|
+
│ ├── _http.py # Stdlib HTTP helper (preserves header casing)
|
|
389
|
+
│ ├── deploy_client.py # /byoc/upload/request + upload/status client
|
|
390
|
+
│ ├── runtime/ # Runtime providers
|
|
391
|
+
│ │ ├── mock_runtime.py # Mock provider
|
|
392
|
+
│ │ ├── agentforce_runtime.py # Agentforce provider (default)
|
|
393
|
+
│ │ └── base.py # Base provider interface
|
|
394
|
+
│ ├── gateway/ # Relay gateway clients
|
|
395
|
+
│ │ ├── relay_client.py # Agentforce relay client
|
|
396
|
+
│ │ ├── mock_relay_client.py # Mock relay client
|
|
397
|
+
│ │ └── base.py # Base interface
|
|
398
|
+
│ ├── schema_utils/ # entry_func / requestSchema / responseSchema
|
|
399
|
+
│ ├── logging/ # Structured logging
|
|
400
|
+
│ ├── cli/ # CLI commands
|
|
401
|
+
│ └── templates/ # Project templates
|
|
402
|
+
├── examples/ # Working examples
|
|
403
|
+
├── tests/ # Test suite
|
|
404
|
+
├── pyproject.toml # Poetry configuration
|
|
405
|
+
└── README.md # This file
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Running Tests
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
poetry run pytest
|
|
412
|
+
poetry run pytest --cov=agentforce_byoc
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Code Quality
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
make format # black + isort
|
|
419
|
+
make lint # pre-commit: black, isort, ruff, codespell, license header, mypy
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Contributing
|
|
423
|
+
|
|
424
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the
|
|
425
|
+
development workflow, how to write tests, and the contributor/reviewer/trusted-committer
|
|
426
|
+
roles. Architecture and design notes live in [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
427
|
+
|
|
428
|
+
## Support
|
|
429
|
+
|
|
430
|
+
- **Questions / discussion**: start a [GitHub Discussion](https://github.com/salesforce/agentforce-byoc-python-sdk/discussions).
|
|
431
|
+
- **Bugs / feature requests**: file a [GitHub Issue](https://github.com/salesforce/agentforce-byoc-python-sdk/issues).
|
|
432
|
+
- **Security vulnerabilities**: follow [SECURITY.md](SECURITY.md) — please don't open a public issue for security reports.
|
|
433
|
+
|
|
434
|
+
## License & Terms of Use
|
|
435
|
+
|
|
436
|
+
This project is licensed under the [Apache License 2.0](LICENSE).
|
|
437
|
+
|
|
438
|
+
Use of this project with Salesforce is subject to the [Terms of Use](TERMS_OF_USE.md).
|
|
439
|
+
|