Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ATO-1652] Add gRPC support #1109

Merged
merged 19 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ install:
poetry run python -m pip install -U pip
poetry install


clean:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
Expand All @@ -37,7 +36,7 @@ formatter:

lint:
poetry run ruff check rasa_sdk tests --ignore D
poetry run black --check rasa_sdk tests
poetry run black --exclude="rasa_sdk/grpc_py" --check rasa_sdk tests
radovanZRasa marked this conversation as resolved.
Show resolved Hide resolved
make lint-docstrings

# Compare against `main` if no branch was provided
Expand All @@ -62,3 +61,11 @@ cleanup-generated-changelog:
release:
poetry run python scripts/release.py

generate-grpc:
python -m grpc_tools.protoc \
-Irasa_sdk/grpc_py=./proto \
--python_out=. \
--grpc_python_out=. \
--pyi_out=. \
proto/action_webhook.proto \
proto/health.proto
6 changes: 6 additions & 0 deletions changelog/1109.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Rasa SDK now supports gRPC protocol.
This allows users to use gRPC to invoke custom actions.
Users can use secure (TLS) and insecure connections to communicate over gRPC.
To start action server with gRPC use `--grpc` flag.
For SSL support, users can provide `--ssl-keyfile`, `--ssl-certificate` and `--ssl-ca-file`.
Support for `--ssl-password` is not available yet due to a limitation in the gRPC Python library.
348 changes: 270 additions & 78 deletions poetry.lock

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions proto/action_webhook.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
syntax = "proto3";

package action_server_webhook;
import "google/protobuf/struct.proto";

service ActionService {
rpc Webhook (WebhookRequest) returns (WebhookResponse);
rpc Actions (ActionsRequest) returns (ActionsResponse);
}

message ActionsRequest {}

message ActionsResponse {
map<string, string> actions = 1;
}

message Tracker {
string sender_id = 1;
google.protobuf.Struct slots = 2;
google.protobuf.Struct latest_message = 3;
repeated google.protobuf.Struct events = 4;
bool paused = 5;
optional string followup_action = 6;
map<string, string> active_loop = 7;
optional string latest_action_name = 8;
repeated google.protobuf.Struct stack = 9;
}

message Intent {
string string_value = 1;
google.protobuf.Struct dict_value = 2;
}

message Entity {
string string_value = 1;
google.protobuf.Struct dict_value = 2;
}

message Action {
string string_value = 1;
google.protobuf.Struct dict_value = 2;
}

message Domain {
google.protobuf.Struct config = 1;
google.protobuf.Struct session_config = 2;
repeated Intent intents = 3;
repeated Entity entities = 4;
google.protobuf.Struct slots = 5;
google.protobuf.Struct responses = 6;
repeated Action actions = 7;
google.protobuf.Struct forms = 8;
repeated google.protobuf.Struct e2e_actions = 9;
}

message WebhookRequest {
string next_action = 1;
string sender_id = 2;
Tracker tracker = 3;
Domain domain = 4;
string version = 5;
optional string domain_digest = 6;
}

message WebhookResponse {
repeated google.protobuf.Struct events = 1;
repeated google.protobuf.Struct responses = 2;
}
11 changes: 11 additions & 0 deletions proto/health.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {}

message HealthCheckResponse {}

service HealthService {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 88
target-version = [ "py37", "py38", "py39", "py310",]
exclude = "((.eggs | .git | .mypy_cache | .pytest_cache | build | dist))"
exclude = "((.eggs | .git | .mypy_cache | .pytest_cache | build | dist ))"

[tool.poetry]
name = "rasa-sdk"
Expand Down Expand Up @@ -67,11 +67,13 @@ ignore_missing_imports = true
show_error_codes = true
warn_redundant_casts = true
warn_unused_ignores = true
exclude = "rasa_sdk/grpc_py"

[tool.ruff]
ignore = [ "D100", "D104", "D105", "RUF005",]
line-length = 88
select = [ "D", "E", "F", "W", "RUF",]
exclude = [ "rasa_sdk/grpc_py" ]

[tool.poetry.dependencies]
python = ">=3.8,<3.11"
Expand All @@ -87,6 +89,10 @@ opentelemetry-api = "~1.15.0"
opentelemetry-sdk = "~1.15.0"
opentelemetry-exporter-jaeger = "~1.15.0"
opentelemetry-exporter-otlp = "~1.15.0"
grpcio = "1.59.3"
protobuf = "4.25.3"
grpcio-tools = "1.56.2"
pydantic = "2.6.4"

[tool.poetry.dev-dependencies]
pytest-cov = "^4.1.0"
Expand All @@ -111,3 +117,4 @@ asyncio_mode = "auto"
[tool.poetry.group.dev.dependencies]
ruff = ">=0.0.256,<0.0.286"
pytest-asyncio = "^0.21.0"
types-protobuf = "4.25.0.20240417"
39 changes: 28 additions & 11 deletions rasa_sdk/__main__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging
import asyncio

from rasa_sdk import utils
from rasa_sdk.endpoint import create_argument_parser, run
from rasa_sdk.constants import APPLICATION_ROOT_LOGGER_NAME
from rasa_sdk.grpc_server import run_grpc

logger = logging.getLogger(__name__)


def main_from_args(args):
Expand All @@ -18,20 +22,33 @@ def main_from_args(args):
)
utils.update_sanic_log_level()

run(
args.actions,
args.port,
args.cors,
args.ssl_certificate,
args.ssl_keyfile,
args.ssl_password,
args.auto_reload,
args.endpoints,
)
if args.grpc:
asyncio.run(
run_grpc(
args.actions,
args.port,
args.ssl_certificate,
args.ssl_keyfile,
args.ssl_ca_file,
args.auto_reload,
args.endpoints,
)
)
else:
run(
args.actions,
args.port,
args.cors,
args.ssl_certificate,
args.ssl_keyfile,
args.ssl_password,
args.auto_reload,
radovanZRasa marked this conversation as resolved.
Show resolved Hide resolved
args.endpoints,
)


def main():
# Running as standalone python application
"""Runs the action server as standalone application."""
arg_parser = create_argument_parser()
cmdline_args = arg_parser.parse_args()

Expand Down
35 changes: 30 additions & 5 deletions rasa_sdk/cli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
from rasa_sdk.constants import DEFAULT_SERVER_PORT, DEFAULT_ENDPOINTS_PATH


def action_arg(action):
if "/" in action:
def action_arg(actions_module_path: str) -> str:
"""Validate the action module path.

Valid action module path is python module, so it should not contain a slash.

Args:
actions_module_path: Path to the actions python module.

Returns:
actions_module_path: If provided module path is valid.

Raises:
argparse.ArgumentTypeError: If the module path is invalid.
"""
if "/" in actions_module_path:
raise argparse.ArgumentTypeError(
"Invalid actions format. Actions file should be a python module "
"and passed with module notation (e.g. directory.actions)."
)
else:
return action
return actions_module_path


def add_endpoint_arguments(parser):
def add_endpoint_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all the arguments to the argument parser."""
parser.add_argument(
"-p",
"--port",
Expand Down Expand Up @@ -47,7 +61,15 @@ def add_endpoint_arguments(parser):
"--ssl-password",
default=None,
help="If your ssl-keyfile is protected by a password, you can specify it "
"using this paramer.",
"using this parameter. "
"Not supported in grpc mode.",
)
parser.add_argument(
"--ssl-ca-file",
default=None,
help="If you want to authenticate the client using a certificate, you can "
"specify the CA certificate of the client using this parameter. "
"Supported only in grpc mode.",
)
parser.add_argument(
"--auto-reload",
Expand All @@ -59,3 +81,6 @@ def add_endpoint_arguments(parser):
default=DEFAULT_ENDPOINTS_PATH,
help="Configuration file for the assistant as a yml file.",
)
parser.add_argument(
"--grpc", help="Starts grpc server instead of http", action="store_true"
)
1 change: 1 addition & 0 deletions rasa_sdk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
"https://docs.python.org/3/library/logging.config.html#dictionary-schema-details"
)
DEFAULT_ENDPOINTS_PATH = "endpoints.yml"
NO_GRACE_PERIOD = 0
5 changes: 4 additions & 1 deletion rasa_sdk/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ async def actions(_) -> HTTPResponse:
if auto_reload:
executor.reload()

body = [{"name": k} for k in executor.actions.keys()]
body = [
action_name_item.model_dump()
for action_name_item in executor.list_actions()
]
return response.json(body, status=200)

@app.exception(Exception)
Expand Down
Loading
Loading