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,79 @@
|
|
|
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
|
+
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
|
+
(:mod:`agentforce_byoc.schema_utils.generator`) reads the decorator at
|
|
30
|
+
the AST level to produce OpenAPI ``properties``.
|
|
31
|
+
|
|
32
|
+
Example::
|
|
33
|
+
|
|
34
|
+
from typing import TypedDict
|
|
35
|
+
from agentforce_byoc.schema_utils import requestSchema
|
|
36
|
+
|
|
37
|
+
class AddRequest(TypedDict):
|
|
38
|
+
a: int
|
|
39
|
+
b: int
|
|
40
|
+
|
|
41
|
+
@entry_func
|
|
42
|
+
@requestSchema(AddRequest)
|
|
43
|
+
def add(request: dict) -> dict:
|
|
44
|
+
...
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def decorator(fn: F) -> F:
|
|
48
|
+
return fn
|
|
49
|
+
|
|
50
|
+
return decorator
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def responseSchema(cls: Type[Any]) -> Callable[[F], F]:
|
|
54
|
+
"""Decorator that declares the response body schema for an ``@entry_func`` function.
|
|
55
|
+
|
|
56
|
+
The *cls* argument must be a class with typed fields (e.g. a
|
|
57
|
+
``TypedDict`` subclass). At runtime the decorator is a pass-through —
|
|
58
|
+
it returns the original function unchanged. The schema generator
|
|
59
|
+
(:mod:`agentforce_byoc.schema_utils.generator`) reads the decorator at
|
|
60
|
+
the AST level to produce OpenAPI ``properties``.
|
|
61
|
+
|
|
62
|
+
Example::
|
|
63
|
+
|
|
64
|
+
from typing import TypedDict
|
|
65
|
+
from agentforce_byoc.schema_utils import responseSchema
|
|
66
|
+
|
|
67
|
+
class AddResponse(TypedDict):
|
|
68
|
+
result: int
|
|
69
|
+
|
|
70
|
+
@entry_func
|
|
71
|
+
@responseSchema(AddResponse)
|
|
72
|
+
def add(request: dict) -> dict:
|
|
73
|
+
...
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def decorator(fn: F) -> F:
|
|
77
|
+
return fn
|
|
78
|
+
|
|
79
|
+
return decorator
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
ENV/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
*~
|
|
23
|
+
|
|
24
|
+
# Testing
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
htmlcov/
|
|
28
|
+
.tox/
|
|
29
|
+
|
|
30
|
+
# Git
|
|
31
|
+
.git/
|
|
32
|
+
.gitignore
|
|
33
|
+
|
|
34
|
+
# Documentation
|
|
35
|
+
*.md
|
|
36
|
+
docs/
|
|
37
|
+
|
|
38
|
+
# CI/CD
|
|
39
|
+
.github/
|
|
40
|
+
.gitlab-ci.yml
|
|
41
|
+
|
|
42
|
+
# Logs
|
|
43
|
+
*.log
|
|
44
|
+
|
|
45
|
+
# OS
|
|
46
|
+
.DS_Store
|
|
47
|
+
Thumbs.db
|
|
48
|
+
|
|
49
|
+
# Temporary files
|
|
50
|
+
*.tmp
|
|
51
|
+
*.bak
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# PyCharm
|
|
30
|
+
.idea/
|
|
31
|
+
|
|
32
|
+
# VS Code
|
|
33
|
+
.vscode/
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
.coverage.*
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
|
|
43
|
+
# Jupyter Notebook
|
|
44
|
+
.ipynb_checkpoints
|
|
45
|
+
|
|
46
|
+
# pyenv
|
|
47
|
+
.python-version
|
|
48
|
+
|
|
49
|
+
# Poetry
|
|
50
|
+
poetry.lock
|
|
51
|
+
|
|
52
|
+
# Logs
|
|
53
|
+
*.log
|
|
54
|
+
|
|
55
|
+
# OS
|
|
56
|
+
.DS_Store
|
|
57
|
+
Thumbs.db
|
|
58
|
+
|
|
59
|
+
# Environment variables
|
|
60
|
+
.env
|
|
61
|
+
.env.local
|
|
62
|
+
|
|
63
|
+
# Deployment artifacts
|
|
64
|
+
*.zip
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Agentforce BYOC Function - {{project_name}}
|
|
2
|
+
# Python 3.11 runtime
|
|
3
|
+
|
|
4
|
+
FROM python:3.11-slim
|
|
5
|
+
|
|
6
|
+
# Set working directory
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
# Install system dependencies
|
|
10
|
+
RUN apt-get update && apt-get install -y \
|
|
11
|
+
gcc \
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
13
|
+
|
|
14
|
+
# Copy dependency files
|
|
15
|
+
COPY pyproject.toml poetry.lock* ./
|
|
16
|
+
|
|
17
|
+
# Install Poetry
|
|
18
|
+
RUN pip install --no-cache-dir poetry==1.7.1
|
|
19
|
+
|
|
20
|
+
# Configure Poetry to not create virtual environment
|
|
21
|
+
RUN poetry config virtualenvs.create false
|
|
22
|
+
|
|
23
|
+
# Install dependencies
|
|
24
|
+
RUN poetry install --no-dev --no-interaction --no-ansi
|
|
25
|
+
|
|
26
|
+
# Copy application code
|
|
27
|
+
COPY main.py ./
|
|
28
|
+
COPY entry_func.py ./
|
|
29
|
+
COPY schema.py ./
|
|
30
|
+
COPY config.json ./
|
|
31
|
+
|
|
32
|
+
# Set environment variables
|
|
33
|
+
ENV PYTHONUNBUFFERED=1
|
|
34
|
+
ENV FUNCTION_ENTRY_POINT=main:function
|
|
35
|
+
|
|
36
|
+
# Health check (optional)
|
|
37
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
38
|
+
CMD python -c "import main; print('OK')" || exit 1
|
|
39
|
+
|
|
40
|
+
# Run the function (this will be overridden by the BYOC runtime)
|
|
41
|
+
CMD ["python", "-c", "from main import function; import json; result = function({}); print(json.dumps(result))"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Makefile for {{project_name}}
|
|
2
|
+
|
|
3
|
+
.PHONY: help
|
|
4
|
+
help: ## Show this help message
|
|
5
|
+
@echo "{{project_name}} - Agentforce BYOC Agent"
|
|
6
|
+
@echo ""
|
|
7
|
+
@echo "Available targets:"
|
|
8
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
|
|
9
|
+
|
|
10
|
+
.PHONY: install
|
|
11
|
+
install: ## Install dependencies with Poetry
|
|
12
|
+
poetry install
|
|
13
|
+
|
|
14
|
+
.PHONY: run
|
|
15
|
+
run: ## Run the agent locally (requires ORG_JWT_TOKEN + SFDC_CORE_TENANT_ID + SFDC_USER_ID + SFDC_APP_CONTEXT env vars)
|
|
16
|
+
@echo "Make sure ORG_JWT_TOKEN, SFDC_CORE_TENANT_ID, SFDC_USER_ID, and SFDC_APP_CONTEXT are set:"
|
|
17
|
+
@echo " export ORG_JWT_TOKEN=<org-jwt>"
|
|
18
|
+
@echo " export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>"
|
|
19
|
+
@echo " export SFDC_USER_ID=<user-id>"
|
|
20
|
+
@echo " export SFDC_APP_CONTEXT=<app-context>"
|
|
21
|
+
@echo ""
|
|
22
|
+
poetry run agentforce run main.py
|
|
23
|
+
|
|
24
|
+
.PHONY: test
|
|
25
|
+
test: ## Run tests
|
|
26
|
+
poetry run pytest
|
|
27
|
+
|
|
28
|
+
.PHONY: format
|
|
29
|
+
format: ## Format code with black
|
|
30
|
+
poetry run black .
|
|
31
|
+
|
|
32
|
+
.PHONY: validate
|
|
33
|
+
validate: ## Validate agent package
|
|
34
|
+
poetry run agentforce validate .
|
|
35
|
+
|
|
36
|
+
.PHONY: zip
|
|
37
|
+
zip: ## Create a deployment-ready package archive
|
|
38
|
+
poetry run agentforce zip --package-dir .
|
|
39
|
+
|
|
40
|
+
.PHONY: package
|
|
41
|
+
package: ## Build end-to-end: validate -> run -> zip
|
|
42
|
+
poetry run agentforce package .
|
|
43
|
+
|
|
44
|
+
.PHONY: deploy
|
|
45
|
+
deploy: ## Register + upload to Agentforce BYOC, then poll status (requires ORG_JWT_TOKEN + SFDC_CORE_TENANT_ID + SFDC_USER_ID + SFDC_APP_CONTEXT env vars)
|
|
46
|
+
poetry run agentforce deploy --package-name "{{project_name}}" --package-file {{project_name}}-0.1.0.tar.gz
|
|
47
|
+
poetry run agentforce deploy-status --package-name "{{project_name}}" --version 1.0
|
|
48
|
+
|
|
49
|
+
.PHONY: clean
|
|
50
|
+
clean: ## Clean build artifacts
|
|
51
|
+
rm -rf dist/ build/ *.egg-info .pytest_cache __pycache__
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# {{project_name}}
|
|
2
|
+
|
|
3
|
+
Agentforce BYOC Function created with the Agentforce SDK.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This is a BYOC (Bring Your Own Code) function that can be deployed to Salesforce and executed within Agentforce workflows. Functions are defined using decorators for schema validation and can be tested locally or deployed to Salesforce.
|
|
8
|
+
|
|
9
|
+
## Project Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
{{project_name}}/
|
|
13
|
+
├── main.py # Your function implementation with decorators
|
|
14
|
+
├── entry_func.py # Schema decorator utilities (entry point)
|
|
15
|
+
├── schema.py # Schema decorators (request/response)
|
|
16
|
+
├── agentforce_byoc/ # Vendored SDK (until it's published to PyPI)
|
|
17
|
+
├── config.json # Function configuration (entryPoint)
|
|
18
|
+
├── Dockerfile # Container definition for deployment
|
|
19
|
+
├── pyproject.toml # Python dependencies (Poetry)
|
|
20
|
+
├── .python-version # Python 3.11
|
|
21
|
+
├── .gitignore # Git ignore patterns
|
|
22
|
+
├── .dockerignore # Docker ignore patterns
|
|
23
|
+
├── Makefile # Development tasks
|
|
24
|
+
└── README.md # This file
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> `agentforce_byoc/` is a copy of the SDK runtime, vendored by `agentforce init`
|
|
28
|
+
> because the SDK isn't on PyPI yet. It's what lets your deployed package
|
|
29
|
+
> `import agentforce_byoc`. When the SDK is published, this directory goes away
|
|
30
|
+
> and it becomes a normal dependency.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### 1. Install Dependencies
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
poetry install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Test Locally
|
|
41
|
+
|
|
42
|
+
Set your credentials, then run the agent against the Agentforce BYOC runtime:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export ORG_JWT_TOKEN=<org-jwt>
|
|
46
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
47
|
+
export SFDC_USER_ID=<user-id>
|
|
48
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
49
|
+
agentforce run main.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Implement Your Function
|
|
53
|
+
|
|
54
|
+
Edit `main.py` and define your schemas:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# TODO: Define your function schemas here
|
|
58
|
+
class MyRequest(TypedDict):
|
|
59
|
+
input_text: str
|
|
60
|
+
max_length: int
|
|
61
|
+
|
|
62
|
+
class MyResponse(TypedDict):
|
|
63
|
+
output_text: str
|
|
64
|
+
status: str
|
|
65
|
+
|
|
66
|
+
@entry_func
|
|
67
|
+
@requestSchema(MyRequest)
|
|
68
|
+
@responseSchema(MyResponse)
|
|
69
|
+
def function(request: dict) -> dict:
|
|
70
|
+
# Your implementation
|
|
71
|
+
return {"output_text": "result", "status": "success"}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Running Locally
|
|
75
|
+
|
|
76
|
+
Run against the Agentforce BYOC runtime. Set your credentials first (the
|
|
77
|
+
command aborts with instructions if they're unset):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export ORG_JWT_TOKEN=<org-jwt>
|
|
81
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
82
|
+
export SFDC_USER_ID=<user-id>
|
|
83
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
84
|
+
agentforce run main.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`run` executes your function on this machine; relay calls go out to the real
|
|
88
|
+
Agentforce service. The tenant id, user id, and app context are read from the
|
|
89
|
+
environment (`SFDC_CORE_TENANT_ID` / `SFDC_USER_ID` / `SFDC_APP_CONTEXT`).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Function Schema Definition
|
|
94
|
+
|
|
95
|
+
Your function uses decorators to define input/output schemas:
|
|
96
|
+
|
|
97
|
+
### `@entry_func`
|
|
98
|
+
Marks the function as a BYOC entry point.
|
|
99
|
+
|
|
100
|
+
### `@requestSchema(TypedDictClass)`
|
|
101
|
+
Declares the expected request structure.
|
|
102
|
+
|
|
103
|
+
### `@responseSchema(TypedDictClass)`
|
|
104
|
+
Declares the expected response structure.
|
|
105
|
+
|
|
106
|
+
**Example:**
|
|
107
|
+
```python
|
|
108
|
+
from typing import TypedDict, List
|
|
109
|
+
|
|
110
|
+
class MyRequest(TypedDict):
|
|
111
|
+
items: List[str]
|
|
112
|
+
separator: str
|
|
113
|
+
|
|
114
|
+
class MyResponse(TypedDict):
|
|
115
|
+
result: str
|
|
116
|
+
status: str
|
|
117
|
+
|
|
118
|
+
@entry_func
|
|
119
|
+
@requestSchema(MyRequest)
|
|
120
|
+
@responseSchema(MyResponse)
|
|
121
|
+
def function(request: dict) -> dict:
|
|
122
|
+
items = request.get("items", [])
|
|
123
|
+
separator = request.get("separator", " ")
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"result": separator.join(items),
|
|
127
|
+
"status": "success"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
### config.json
|
|
134
|
+
|
|
135
|
+
Simple configuration with function entry point:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"entryPoint": "main.py"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This tells the runtime which file contains your function.
|
|
144
|
+
|
|
145
|
+
## Adding Dependencies
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Add a package
|
|
149
|
+
poetry add <package-name>
|
|
150
|
+
|
|
151
|
+
# Add a dev dependency
|
|
152
|
+
poetry add --dev <package-name>
|
|
153
|
+
|
|
154
|
+
# Update all dependencies
|
|
155
|
+
poetry update
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Example:**
|
|
159
|
+
```bash
|
|
160
|
+
poetry add requests pandas numpy
|
|
161
|
+
poetry add --dev pytest black mypy
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Building and Deploying
|
|
165
|
+
|
|
166
|
+
### 1. Package Your Function
|
|
167
|
+
|
|
168
|
+
Create a deployment-ready gzip tar archive:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
agentforce zip .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This creates `{{project_name}}-0.1.0.tar.gz` containing:
|
|
175
|
+
- `main.py` - Your function code
|
|
176
|
+
- `entry_func.py` - Schema decorators
|
|
177
|
+
- `schema.py` - Schema validation
|
|
178
|
+
- `config.json` - Configuration
|
|
179
|
+
- `Dockerfile` - Container definition
|
|
180
|
+
- `pyproject.toml` - Dependencies
|
|
181
|
+
- `requirements.txt` - Exported dependencies
|
|
182
|
+
|
|
183
|
+
You can also build end-to-end (validate → run → zip) in one step:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
agentforce package .
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 2. Deploy to Agentforce BYOC
|
|
190
|
+
|
|
191
|
+
Auth and identity come from environment variables (required — commands abort if unset):
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
export ORG_JWT_TOKEN=<org-jwt>
|
|
195
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
196
|
+
export SFDC_USER_ID=<user-id>
|
|
197
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Then deploy in one step — `deploy` registers the metadata **and** uploads the
|
|
201
|
+
archive — and optionally poll the status:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Register + upload: registers metadata AND PUTs the package to the presigned URL
|
|
205
|
+
agentforce deploy --package-name {{project_name}} --package-file {{project_name}}-0.1.0.tar.gz
|
|
206
|
+
|
|
207
|
+
# (Optional) Poll status — read-only: reports registrationStatus + uploadStatus
|
|
208
|
+
agentforce deploy-status --package-name {{project_name}} --version 1.0
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Environment variables (used by `deploy`, `deploy-status`, and `run` — all four required):**
|
|
212
|
+
- `ORG_JWT_TOKEN` - Org JWT for authentication (required)
|
|
213
|
+
- `SFDC_CORE_TENANT_ID` - core tenant id `core/<instance>-<fd>/<org-id>` (required)
|
|
214
|
+
- `SFDC_USER_ID` - caller's user id (sent as `x-sfdc-user-id`)
|
|
215
|
+
- `SFDC_APP_CONTEXT` - app context (sent as `x-sfdc-app-context`)
|
|
216
|
+
|
|
217
|
+
**`deploy` options:**
|
|
218
|
+
- `--package-name <name>` - Package name (required)
|
|
219
|
+
- `--package-file <path>` - Package archive from `agentforce zip` (required)
|
|
220
|
+
- `--entry-point <file>` - Function entry point (default: main.py)
|
|
221
|
+
- `--version <ver>` - Package version (default: 1.0)
|
|
222
|
+
- `--max-live-duration <seconds>` - Max live duration (default: 300)
|
|
223
|
+
|
|
224
|
+
**`deploy-status` options:** (read-only poll — never writes)
|
|
225
|
+
- `--package-name <name>` - Package name (required)
|
|
226
|
+
- `--version <ver>` - Package version (default: 1.0)
|
|
227
|
+
|
|
228
|
+
## Development Workflow
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# 1. Set credentials (all four required for run, deploy, and deploy-status)
|
|
232
|
+
export ORG_JWT_TOKEN=<org-jwt>
|
|
233
|
+
export SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id>
|
|
234
|
+
export SFDC_USER_ID=<user-id>
|
|
235
|
+
export SFDC_APP_CONTEXT=<app-context>
|
|
236
|
+
|
|
237
|
+
# 2. Develop + test locally against the Agentforce BYOC runtime
|
|
238
|
+
agentforce run main.py
|
|
239
|
+
|
|
240
|
+
# 3. Package
|
|
241
|
+
agentforce zip . # Create deployment archive
|
|
242
|
+
|
|
243
|
+
# 4. Deploy: register + upload in one step, then optionally poll status
|
|
244
|
+
agentforce deploy --package-name {{project_name}} --package-file {{project_name}}-0.1.0.tar.gz
|
|
245
|
+
agentforce deploy-status --package-name {{project_name}} --version 1.0
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Testing
|
|
249
|
+
|
|
250
|
+
### Unit Tests
|
|
251
|
+
|
|
252
|
+
Create a `tests/` directory:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
mkdir tests
|
|
256
|
+
poetry add --dev pytest
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Example test (`tests/test_main.py`):**
|
|
260
|
+
```python
|
|
261
|
+
from main import function
|
|
262
|
+
|
|
263
|
+
def test_function_missing_input():
|
|
264
|
+
# No text -> error, no relay call
|
|
265
|
+
response = function({})
|
|
266
|
+
assert response["status"] == "error"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
> The success path calls the relay (a network request needing a valid
|
|
270
|
+
> `ORG_JWT_TOKEN`), so mock the relay client or test it as an integration test
|
|
271
|
+
> rather than a plain unit test.
|
|
272
|
+
|
|
273
|
+
**Run tests:**
|
|
274
|
+
```bash
|
|
275
|
+
poetry run pytest
|
|
276
|
+
poetry run pytest --cov=main
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Docker
|
|
280
|
+
|
|
281
|
+
Build and test locally with Docker:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Build container
|
|
285
|
+
docker build -t {{project_name}}:latest .
|
|
286
|
+
|
|
287
|
+
# Run container
|
|
288
|
+
docker run --rm {{project_name}}:latest
|
|
289
|
+
|
|
290
|
+
# Run with environment variables
|
|
291
|
+
docker run --rm \
|
|
292
|
+
-e ORG_JWT_TOKEN=<org-jwt> \
|
|
293
|
+
{{project_name}}:latest
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Troubleshooting
|
|
297
|
+
|
|
298
|
+
### Function fails locally
|
|
299
|
+
|
|
300
|
+
1. Check Python version: `python --version` (should be 3.11+)
|
|
301
|
+
2. Install dependencies: `poetry install`
|
|
302
|
+
3. Check imports in `main.py`
|
|
303
|
+
4. Verify `ORG_JWT_TOKEN`, `SFDC_CORE_TENANT_ID`, `SFDC_USER_ID`, and `SFDC_APP_CONTEXT` are exported
|
|
304
|
+
|
|
305
|
+
### Agentforce BYOC runtime fails
|
|
306
|
+
|
|
307
|
+
1. Check `ORG_JWT_TOKEN` is set: `echo $ORG_JWT_TOKEN`
|
|
308
|
+
2. Verify the JWT is valid and not expired
|
|
309
|
+
3. Check `SFDC_CORE_TENANT_ID` is set and well-formed (core/<instance>-<fd>/<org-id>)
|
|
310
|
+
4. Check `SFDC_USER_ID` is set (relay sends it as `x-sfdc-user-id`)
|
|
311
|
+
5. Check `SFDC_APP_CONTEXT` is set (relay sends it as `x-sfdc-app-context`)
|
|
312
|
+
|
|
313
|
+
### Deployment fails
|
|
314
|
+
|
|
315
|
+
1. Verify `ORG_JWT_TOKEN`, `SFDC_CORE_TENANT_ID`, `SFDC_USER_ID`, and `SFDC_APP_CONTEXT` env vars are set (commands abort if unset)
|
|
316
|
+
2. Check the JWT is valid and not expired
|
|
317
|
+
3. Check package structure: `tar -tzf {{project_name}}-*.tar.gz`
|
|
318
|
+
|
|
319
|
+
## CLI Command Reference
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Initialize new function
|
|
323
|
+
agentforce init <name>
|
|
324
|
+
|
|
325
|
+
# Run locally (requires ORG_JWT_TOKEN + SFDC_CORE_TENANT_ID + SFDC_USER_ID + SFDC_APP_CONTEXT)
|
|
326
|
+
agentforce run main.py
|
|
327
|
+
|
|
328
|
+
# Package for deployment
|
|
329
|
+
agentforce zip .
|
|
330
|
+
agentforce zip . --output custom-name.tar.gz
|
|
331
|
+
|
|
332
|
+
# Build end-to-end (validate -> run -> zip)
|
|
333
|
+
agentforce package .
|
|
334
|
+
|
|
335
|
+
# Deploy to Agentforce BYOC (set ORG_JWT_TOKEN + SFDC_CORE_TENANT_ID + SFDC_USER_ID + SFDC_APP_CONTEXT env vars first)
|
|
336
|
+
agentforce deploy --package-name <name> --package-file <name>-0.1.0.tar.gz
|
|
337
|
+
agentforce deploy-status --package-name <name> --version 1.0
|
|
338
|
+
|
|
339
|
+
# Get help
|
|
340
|
+
agentforce --help
|
|
341
|
+
agentforce run --help
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Environment Variables
|
|
345
|
+
|
|
346
|
+
### Agentforce BYOC Runtime
|
|
347
|
+
```bash
|
|
348
|
+
ORG_JWT_TOKEN=<org-jwt> # Required
|
|
349
|
+
SFDC_CORE_TENANT_ID=core/<instance>-<fd>/<org-id> # Required
|
|
350
|
+
SFDC_USER_ID=<user-id> # Required by run + deploy
|
|
351
|
+
SFDC_APP_CONTEXT=<app-context> # Required by run + deploy
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Learn More
|
|
355
|
+
|
|
356
|
+
- [Agentforce BYOC SDK Documentation](https://github.com/salesforce/agentforce-byoc-python-sdk)
|
|
357
|
+
- [Poetry Documentation](https://python-poetry.org/docs/)
|
|
358
|
+
- [TypedDict Documentation](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import functools
|
|
19
|
+
from typing import (
|
|
20
|
+
Any,
|
|
21
|
+
Callable,
|
|
22
|
+
TypeVar,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def entry_func(fn: F) -> F:
|
|
29
|
+
"""Mark a function as the BYOC entry point.
|
|
30
|
+
|
|
31
|
+
The ``@entry_func`` decorator identifies a function whose signature
|
|
32
|
+
will be extracted by ``generate_byoc_schema.py`` to produce an
|
|
33
|
+
OpenAPI specification. At runtime the decorator is a pass-through —
|
|
34
|
+
it returns the original function unchanged.
|
|
35
|
+
|
|
36
|
+
All parameters of the decorated function **must** have type
|
|
37
|
+
annotations; the schema generator will reject untyped parameters.
|
|
38
|
+
|
|
39
|
+
Example::
|
|
40
|
+
|
|
41
|
+
from entry_func import entry_func
|
|
42
|
+
|
|
43
|
+
@entry_func
|
|
44
|
+
def add(a: int, b: int = 0) -> int:
|
|
45
|
+
return a + b
|
|
46
|
+
"""
|
|
47
|
+
@functools.wraps(fn)
|
|
48
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
49
|
+
return fn(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
return wrapper # type: ignore[return-value]
|