diff --git a/docs/messaging/.index b/docs/messaging/.index index 7c5a21b0..413f1d2c 100644 --- a/docs/messaging/.index +++ b/docs/messaging/.index @@ -5,4 +5,10 @@ nav: - SLIM Controller: slim-controller.md - SLIM Group Management: slim-group.md - SLIM Group Communication Tutorial: slim-group-tutorial.md - - SLIM and MCP Integration: slim-mcp.md + - SLIM Integrations: + - SLIMRPC: + - Overview: slim-rpc.md + - Protoc Plugin: slim-slimrpc-compiler.md + - SLIMA2A: slim-a2a.md + - MCP over SLIM: slim-mcp.md + diff --git a/docs/messaging/slim-a2a.md b/docs/messaging/slim-a2a.md new file mode 100644 index 00000000..4503b8bf --- /dev/null +++ b/docs/messaging/slim-a2a.md @@ -0,0 +1,103 @@ +# SLIM A2A + +SLIM A2A is a native integration of A2A built on top of SLIM. It utilizes SLIMRPC (SLIM Remote Procedure Call) and the SLIMRPC compiler to compile A2A protobuf file and generate the necessary code to enable A2A functionality on SLIM. + +## What is SLIMRPC and SLIMRCP compiler + +SLIMRPC is a framework that enables Protocol Buffers (protobuf) Remote Procedure Calls (RPC) over SLIM. This is similar to gRPC, which uses HTTP/2 as its transport layer for protobuf-based RPC. More information can be found [here](./slim-rpc.md) + +To compile a protobuf file and generate the clients and service stub you can use the [SLIMRPC compiler](./slim-slimrpc-compiler.md). This works in a similar way to the protoc compiler. + +For SLIM A2A we compiled the [a2a.proto](https://github.com/a2aproject/A2A/blob/main/specification/grpc/a2a.proto) file using the SLIM RPC compiler. The generated code is in [a2a_pb2_slimrpc.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slima2a/slima2a/types/a2a_pb2_slimrpc.py). + +## How to use SLIMA2A + +Using SLIMA2A is very similar to using the standard A2A implementation. As a reference example here we use the [travel planner agent](https://github.com/a2aproject/a2a-samples/tree/main/samples/python/agents/travel_planner_agent) available on the A2A samples repository. The version adapted to use SLIM A2A can be found in [travel_planner_agent](https://github.com/agntcy/slim/tree/main/data-plane/python/integrations/slima2a/examples/travel_planner_agent) folder. In the following section, we highlight and explain the key difference between the standard and SLIM A2A implementations. + +### Travel Planner: Server + +In this section we highlight the main differences between the SLIM A2A [server](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slima2a/examples/travel_planner_agent/server.py) implementation with respect to the original implementation in the A2A repository. + + +1. Import the SLIMRPC package. + ```python + import slimrpc + ``` +2. Create the SLIMRPCHandler. Notice that the definitions for `AgentCard` and `DefaultRequestHandler` remain unchanged from the original A2A example. + ```python + agent_card = AgentCard( + name="travel planner Agent", + description="travel planner", + url="http://localhost:10001/", + version="1.0.0", + default_input_modes=["text"], + default_output_modes=["text"], + capabilities=AgentCapabilities(streaming=True), + skills=[skill], + ) + request_handler = DefaultRequestHandler( + agent_executor=TravelPlannerAgentExecutor(), + task_store=InMemoryTaskStore(), + ) + servicer = SLIMRPCHandler(agent_card, request_handler) + ``` +3. Setup the `slimrpc.Server`. This is the only place where you need to setup few parameters that are specific to SLIM. + ```python + server = slimrpc.Server( + local="agntcy/demo/travel_planner_agent", + slim={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + shared_secret="secret", + ) + ``` + - local: Name of the local application. + - slim: Dictionary specifying how to connect to the SLIM node. + - shared_secret: Used to set up MLS (Message Layer Security). + For more information about these settings, see the [SLIMRPC](./slim-rpc.md). +4. Register the Service. + ```python + add_A2AServiceServicer_to_server( + servicer, + server, + ) + ``` + +Your A2A server is now ready to run on SLIM. + +### Travel Planner: Client + +These are the main differences between the [client](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slima2a/examples/travel_planner_agent/client.py) using SLIM A2A and the standard one. + +1. Create a channel. This requires a configuration that is similar to the server + ```python + def channel_factory(topic: str) -> slimrpc.Channel: + channel = slimrpc.Channel( + local="agntcy/demo/client", + remote=topic, + slim={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + shared_secret="secret", + ) + return channel + ``` +2. Add SLIM RPC in the supported transports. + ```python + client_config = ClientConfig( + supported_transports=["JSONRPC", "slimrpc"], + streaming=True, + httpx_client=httpx_client, + slimrpc_channel_factory=channel_factory, + ) + client_factory = ClientFactory(client_config) + client_factory.register("slimrpc", SLIMRPCTransport.create) + agent_card = minimal_agent_card("agntcy/demo/travel_planner_agent", ["slimrpc"]) + client = client_factory.create(card=agent_card) + ``` \ No newline at end of file diff --git a/docs/messaging/slim-rpc.md b/docs/messaging/slim-rpc.md new file mode 100644 index 00000000..7549d2e8 --- /dev/null +++ b/docs/messaging/slim-rpc.md @@ -0,0 +1,452 @@ +# SLIMRPC (SLIM Remote Procedure Call) + +SLIMRPC, or SLIM Remote Procedure Call, is a mechanism designed to enable +Protocol Buffers (protobuf) RPC over SLIM (Secure Low-latency Inter-process +Messaging). This is analogous to gRPC, which leverages HTTP/2 as its underlying +transport layer for protobuf RPC. + +A key advantage of SLIMRPC lies in its ability to seamlessly integrate SLIM as +the transport protocol for inter-application message exchange. This +significantly simplifies development: a protobuf file can be compiled to +generate code that utilizes SLIM for communication. Application developers can +then interact with the generated code much like they would with standard gRPC, +while benefiting from the inherent security features and efficiency provided by +the SLIM protocol. + +This documentation explains how SLIMRPC works and +how you can implement it in your applications. For detailed instructions on +compiling a protobuf file to obtain the necessary SLIMRPC stub code, please +refer to the dedicated [SLIMRPC compiler +documentation](./slim-slimrpc-compiler.md). + +## SLIM naming in SLIMRPC + +In SLIMRPC, each service and its individual RPC handlers are assigned a SLIM +name, facilitating efficient message routing and processing. Consider the +[example +protobuf](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple/example.proto) +definition, which defines four distinct services: + +```proto +syntax = "proto3"; + +package example_service; + +service Test { + rpc ExampleUnaryUnary(ExampleRequest) returns (ExampleResponse); + rpc ExampleUnaryStream(ExampleRequest) returns (stream ExampleResponse); + rpc ExampleStreamUnary(stream ExampleRequest) returns (ExampleResponse); + rpc ExampleStreamStream(stream ExampleRequest) returns (stream ExampleResponse); +} + +message ExampleRequest { + string example_string = 1; + int64 example_integer = 2; +} + +message ExampleResponse { + string example_string = 1; + int64 example_integer = 2; +} +``` + +This example showcases the four primary communication patterns supported by +gRPC: + +- Unary-Unary +- Unary-Stream +- Stream-Unary +- Stream-Stream + +For SLIMRPC, a specific SLIM name is generated for each handler within a +service. This naming convention allows an application exposing the service to +listen for its name and process messages intended for a particular RPC method. The format +for these names is: + +``` +{package-name}.{service-name}-{handler_name} +``` + +Based on the example_service.Test definition, the names for each handler would +be: + +``` +example_service.Test-ExampleUnaryUnary +example_service.Test-ExampleUnaryStream +example_service.Test-ExampleStreamUnary +example_service.Test-ExampleStreamStream +``` + +This handler name is appended to the second component of the SLIM name +associated with the running application. For instance, to receive messages for +`example_service.Test-ExampleUnaryUnary`, an application would subscribe to: + +``` +component[0]/component[1]/component[2]-example_service.Test-ExampleUnaryUnary/component[3] +``` + +The subscription process is entirely managed by the SLIMRPC package. Application +developers are not required to explicitly handle SLIM name subscriptions. +Instead, they only need to implement the specific functions that will be invoked +when a message arrives for a defined RPC method, exactly as they would with +standard gRPC. + +## Example + +This section provides a detailed walkthrough of a basic SLIMRPC client-server +interaction, leveraging the simple example provided in +[example](https://github.com/agntcy/slim/tree/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple) +folder. + +### Generated Code + +The foundation of this example is the `example.proto` file, which is a standard +Protocol Buffers definition file. This file is compiled using the [SLIMRPC +compiler](./slim-slimrpc-compiler.md) to generate the necessary Python stub +code. The generated code is available in two files: +[example_pb2.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple/types/example_pb2.py) +and +[example_pb2_slimrpc.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple/types/example_pb2_slimrpc.py). +Specifically, `example_pb2_slimrpc.py` contains the SLIMRPC-specific stubs for +both client and server implementations. Below are the key classes and functions +generated by the compiler: + +_Client Stub (TestStub)_: The TestStub class represents the client-side +interface for interacting with the Test service. It provides methods for each +RPC defined in example.proto, allowing clients to initiate calls to the server. + +```python +class TestStub: + """Client stub for Test.""" + def __init__(self, channel): + """Constructor. + + Args: + channel: A slimrpc.Channel. + """ + self.ExampleUnaryUnary = channel.unary_unary( + "/example_service.Test/ExampleUnaryUnary", + request_serializer=pb2.ExampleRequest.SerializeToString, + response_deserializer=pb2.ExampleResponse.FromString, + ) + self.ExampleUnaryStream = channel.unary_stream( + "/example_service.Test/ExampleUnaryStream", + request_serializer=pb2.ExampleRequest.SerializeToString, + response_deserializer=pb2.ExampleResponse.FromString, + ) + self.ExampleStreamUnary = channel.stream_unary( + "/example_service.Test/ExampleStreamUnary", + request_serializer=pb2.ExampleRequest.SerializeToString, + response_deserializer=pb2.ExampleResponse.FromString, + ) + self.ExampleStreamStream = channel.stream_stream( + "/example_service.Test/ExampleStreamStream", + request_serializer=pb2.ExampleRequest.SerializeToString, + response_deserializer=pb2.ExampleResponse.FromString, + ) +``` + +_Server Servicer (TestServicer)_: The TestServicer class defines the server-side +interface. Developers implement this class to provide the actual logic +for each RPC method. + +```python +class TestServicer(): + """Server servicer for Test. Implement this class to provide your service logic.""" + + def ExampleUnaryUnary(self, request, context): + """Method for ExampleUnaryUnary. Implement your service logic here.""" + raise slimrpc_rpc.SRPCResponseError( + code=code__pb2.UNIMPLEMENTED, message="Method not implemented!" + ) + def ExampleUnaryStream(self, request, context): + """Method for ExampleUnaryStream. Implement your service logic here.""" + raise slimrpc_rpc.SRPCResponseError( + code=code__pb2.UNIMPLEMENTED, message="Method not implemented!" + ) + def ExampleStreamUnary(self, request_iterator, context): + """Method for ExampleStreamUnary. Implement your service logic here.""" + raise slimrpc_rpc.SRPCResponseError( + code=code__pb2.UNIMPLEMENTED, message="Method not implemented!" + ) + def ExampleStreamStream(self, request_iterator, context): + """Method for ExampleStreamStream. Implement your service logic here.""" + raise slimrpc_rpc.SRPCResponseError( + code=code__pb2.UNIMPLEMENTED, message="Method not implemented!" + ) +``` + +_Server Registration Function (add_TestServicer_to_server)_: This utility +function registers an implemented TestServicer instance with an SLIMRPC server. +It maps RPC method names to their corresponding handlers and specifies the +request deserialization and response serialization routines. + +```python +def add_TestServicer_to_server(servicer, server: slimrpc.Server): + rpc_method_handlers = { + "ExampleUnaryUnary": slimrpc.unary_unary_rpc_method_handler( + behaviour=servicer.ExampleUnaryUnary, + request_deserializer=pb2.ExampleRequest.FromString, + response_serializer=pb2.ExampleResponse.SerializeToString, + ), + "ExampleUnaryStream": slimrpc.unary_stream_rpc_method_handler( + behaviour=servicer.ExampleUnaryStream, + request_deserializer=pb2.ExampleRequest.FromString, + response_serializer=pb2.ExampleResponse.SerializeToString, + ), + "ExampleStreamUnary": slimrpc.stream_unary_rpc_method_handler( + behaviour=servicer.ExampleStreamUnary, + request_deserializer=pb2.ExampleRequest.FromString, + response_serializer=pb2.ExampleResponse.SerializeToString, + ), + "ExampleStreamStream": slimrpc.stream_stream_rpc_method_handler( + behaviour=servicer.ExampleStreamStream, + request_deserializer=pb2.ExampleRequest.FromString, + response_serializer=pb2.ExampleResponse.SerializeToString, + ), + + } + + server.register_method_handlers( + "example_service.Test", + rpc_method_handlers, + ) +``` + +### Server implementation + +The server-side logic is defined in +[server.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple/server.py). +Similar to standard gRPC implementations, the core service functionality is +provided by the TestService class, which inherits from TestServicer (as +introduced in the previous section). This class contains the concrete +implementations for each of the defined RPC methods. + +The SLIM-specific code and configuration is handled within the amain() +asynchronous function. This function utilizes the create_server helper to +instantiate an SLIMRPC server: + +```python +def create_server( + local: str, + slim: dict, + enable_opentelemetry: bool = False, + shared_secret: str = "", +) -> Server: + """ + Create a new SLIMRPC server instance. + """ + server = Server( + local=local, + slim=slim, + enable_opentelemetry=enable_opentelemetry, + shared_secret=shared_secret, + ) + + return server + + +async def amain() -> None: + server = create_server( + local="agntcy/grpc/server", + slim={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + enable_opentelemetry=False, + shared_secret="my_shared_secret", + ) + + # Create RPCs + add_TestServicer_to_server( + TestService(), + server, + ) + + await server.run() +``` + +A new server application is created using the `create_server` function. The +local parameter, set to "agntcy/grpc/server", assigns a SLIM name to this server +application. + +This name is then used to construct the full SLIMRPC names for each method: + +``` +agntcy/grpc/server-example_service.Test-ExampleUnaryUnary +agntcy/grpc/server-example_service.Test-ExampleUnaryStream +agntcy/grpc/server-example_service.Test-ExampleStreamUnary +agntcy/grpc/server-example_service.Test-ExampleStreamStream +``` + +Additionally, the `slim` dictionary configures the server to connect to a SLIM +node running at `http://localhost:46357`. The tls setting `insecure: True` +disables TLS for simplicity in this example. The `shared_secret` parameter is +used for initializing the Message Layer Security (MLS) protocol. Note that using +a hardcoded shared_secret like "my_shared_secret" is not recommended, please +refer to [the documentation for proper MLS +configuration](./slim-group.md#identity-management). + +Finally, the add_TestServicer_to_server function is called to register the +implemented TestService with the SLIMRPC server, making its RPC methods +available. + +```python + # Create RPCs + add_TestServicer_to_server( + TestService(), + server, + ) +``` + +### Client implementation + +The client-side implementation, found in +[client.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/examples/simple/client.py), +largely mirrors the structure of a standard gRPC client. The primary distinction +and SLIM-specific aspect lies in the creation of the SLIMRPC channel: + +```python + channel_factory = slimrpc.ChannelFactory( + slim_app_config=slimrpc.SLIMAppConfig( + identity="agntcy/grpc/client", + slim_client_config={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + enable_opentelemetry=False, + shared_secret="my_shared_secret", + ), + ) + + channel = channel_factory.new_channel(remote="agntcy/grpc/server") + + # Stubs + stubs = TestStub(channel) +``` + +As opposite to the server, the client application only register its local name +`agntcy/grpc/client` in the SLIM network. This is done through the `identity` +parameter in the `SLIMAppConfig` class. This name will be then used by the +server to send back the response to the client. + +Also, like in the case of the server application, the `slim` dictionary +specifies the SLIM node endpoint (`http://localhost:46357`) and TLS settings, +consistent with the server's configuration, while `shared_secret` is used to +initialize MLS. + +The remote parameter, set to `agntcy/grpc/server`, explicitly identifies the +SLIM name of the target server application. This allows the SLIMRPC channel to +correctly route messages to the appropriate server endpoint within the SLIM +network. Since both client and server use the same protobuf definition, the +client can invoke specific services and methods with type safety and +consistency. + +## SLIMRPC under the Hood + +SLIMRPC was introduced to simplify the integration of existing applications with +SLIM. From a developer's perspective, using SLIMRPC or gRPC is almost identical. +Application developers do not need to manage endpoint names or connectivity +details, as these aspects are handled automatically by SLIMRPC and SLIM. + +All RPC services underneath utilize a sticky point-to-point session. The SLIM +session creation is implemented in inside SLIMRPC in +[channel.py](https://github.com/agntcy/slim/blob/main/data-plane/python/integrations/slimrpc/slimrpc/channel.py): + +```python + # Create a session + session = await self.local_app.create_session( + slim_bindings.PySessionConfiguration.FireAndForget( + max_retries=10, + timeout=datetime.timedelta(seconds=1), + sticky=True, + ) + ) +``` + +This session used by SLIMRPC is also reliable. For each message, the sender +waits for an acknowledgment (ACK) packet for 1 second +(`timeout=datetime.timedelta(seconds=1)`). If no acknowledgment is received, the +message will be re-sent up to 10 times (`max_retries=10`) before notifying the +application of a communication error. + +Since the session is sticky, all messages in a streaming communication will be +forwarded to the same application instance. Let's illustrate this with an +example using the client and server applications described above. + +Imagine two server instances running the same RPC service. In this example we will +focus on the Stream-Unary service, which is served by both server instances +under the general name +`agntcy/grpc/server-example_service.Test-ExampleStreamUnary`. In SLIM, each +application receives a unique ID. Thus, the full service name will include a +fourth component containing the server's ID. This ID is generated by SLIM itself +(see the [doc](./slim-data-plane.md) for more details). Here we will use +instance-1 and instance-2 for simplicity. So, the two full names for the +services will be: + +- `agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1` +- `agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-2` + +Now, if a new client wants to use the Stream-Unary service it needs to knows +only the general name +`agntcy/grpc/server-example_service.Test-ExampleStreamUnary`. SLIMRPC will +leverage SLIM's capabilities to first discover one of the available services, +and then SLIMRPC will use its full, specific name to consistently communicate +with that same endpoint. + +The client will register to SLIM with the name `agntcy/grpc/client`, and it will +get an unique ID assigned by SLIM, for example `instance-1`. So the full name of +the client will be `agntcy/grpc/client/instance-1`. + +```mermaid +sequenceDiagram + autonumber + + participant Client (instance-1) + participant SLIM Node + participant Server (instance-1) + participant Server (instance-2) + + + Note over Client (instance-1),Server (instance-1): Discovery + Client (instance-1)->>SLIM Node: Discover agntcy/grpc/server-example_service.Test-ExampleStreamUnary + SLIM Node->>Server (instance-1): Discover agntcy/grpc/server-example_service.Test-ExampleStreamUnary + Server (instance-1)->>SLIM Node: Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Client (instance-1): Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + + Note over Client (instance-1),Server (instance-1): Stream + Client (instance-1)->>SLIM Node: Msg to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Server (instance-1): Msg to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + Server (instance-1)->>SLIM Node: Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Client (instance-1): Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + + Client (instance-1)->>SLIM Node: Msg to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Server (instance-1): Msg to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + Server (instance-1)->>SLIM Node: Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Client (instance-1): Ack from agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + + Note over Client (instance-1),Server (instance-1): Unary + Server (instance-1)->>SLIM Node: Msg to agntcy/grpc/client/instance-1 + SLIM Node->>Client (instance-1): Msg to agntcy/grpc/client/instance-1 + Client (instance-1)->>SLIM Node: Ack to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 + SLIM Node->>Server (instance-1): Ack to agntcy/grpc/server-example_service.Test-ExampleStreamUnary/instance-1 +``` + +The initial messages in the sequence diagram are used for the discovery phase. +After this step, the client application knows the specific name of the service +running on instance-1. It's important to note that the first message in the +discovery phase is sent in anycast from the SLIM node, meaning it could be +forwarded to **either of the two running servers**. For instance, a subsequent +call of the same RPC from the same client might be served by the server +with id `instance-2`. + +After the discovery, the client will always send messages to the same endpoint, +as demonstrated in the streaming session phase in the example. + +Finally, the server is expected to send one message to the client to close the +service. The server learns the client's address (where to forward the message) +by examining the source field of the received messages. diff --git a/docs/messaging/slim-slimrpc-compiler.md b/docs/messaging/slim-slimrpc-compiler.md new file mode 100644 index 00000000..69a1e846 --- /dev/null +++ b/docs/messaging/slim-slimrpc-compiler.md @@ -0,0 +1,393 @@ +# SLIMRPC Compiler + +The Slim RPC Compiler (`protoc-slimrpc-plugin`) is a protoc plugin that +generates Python client stubs and server servicers for [SLIMRPC (Slim +RPC)](./slim-rpc.md) from Protocol Buffer service definitions. This plugin +enables you to build high-performance RPC services using the SLIMRPC framework. + +## Features + +The Slim RPC Compiler has the following features: + +- Generates Python client stubs for calling SlimRPC services +- Generates Python server servicers for implementing SlimRPC services +- Supports all gRPC streaming patterns: unary-unary, unary-stream, stream-unary, + and stream-stream +- Compatible with both `protoc` and `buf` build systems +- Automatic import resolution for Protocol Buffer dependencies + +## Installation + +There are two ways to install the Slim RPC Compiler: + +### Install via Cargo + +```bash +cargo install agntcy-protoc-slimrpc-plugin +``` + +This will install the `protoc-slimrpc-plugin` binary to your Cargo bin directory +(usually `~/.cargo/bin`). + +### Compile from Source + +1. Clone the repository: + +```bash +git clone https://github.com/agntcy/slim.git +cd slim/data-plane/slimrpc-compiler +``` + +2. Build the plugin: + +```bash +cargo build --release +``` + +3. The compiled binary will be available at + `data-plane/target/release/protoc-slimrpc-plugin` + +## Usage + +### Example Protocol Buffer Definition + +Create a file called `example.proto`: + +```proto +syntax = "proto3"; + +package example_service; + +service Test { + rpc ExampleUnaryUnary(ExampleRequest) returns (ExampleResponse); + rpc ExampleUnaryStream(ExampleRequest) returns (stream ExampleResponse); + rpc ExampleStreamUnary(stream ExampleRequest) returns (ExampleResponse); + rpc ExampleStreamStream(stream ExampleRequest) returns (stream ExampleResponse); +} + +message ExampleRequest { + string example_string = 1; + int64 example_integer = 2; +} + +message ExampleResponse { + string example_string = 1; + int64 example_integer = 2; +} +``` + +### Using with protoc + +#### Prerequisites + +Make sure you have: + +- `protoc` (Protocol Buffer compiler) installed +- The `protoc-slimrpc-plugin` binary in your PATH or specify its full path + +#### Generate Python Files + +```bash +# Generate both the protobuf Python files and SLIMRPC files +protoc \ + --python_out=. \ + --pyi_out=. \ + --plugin=protoc-gen-slimrpc=${HOME}/.cargo/bin/protoc-slimrpc-plugin \ + --slimrpc_out=. \ + example.proto +``` + +This will generate: + +- `example_pb2.py` - Standard protobuf Python bindings +- `example_pb2_slimrpc.py` - SLIMRPC client stubs and server servicers + +#### With Custom Types Import + +You can specify a custom import for the types module. This allows to import the +types from an external package. + +For instance, if you don't want to generate the types and you want to import +them from a2a.grpc.a2a_pb2`, you can do: + +```bash +protoc \ + --plugin=protoc-gen-slimrpc=${HOME}/.cargo/bin/protoc-slimrpc-plugin \ + --slimrpc_out=types_import="from a2a.grpc import a2a_pb2 as a2a__pb2":. \ + example.proto +``` + +### Using with buf + +#### Prerequisites + +- `buf` CLI [installed](https://buf.build/docs/cli/installation/) +- `protoc-slimrpc-plugin` binary in your PATH, or specify the full path in the + `buf.gen.yaml` file + +#### Create buf.gen.yaml + +Create a `buf.gen.yaml` file in your project root: + +```yaml +version: v2 +managed: + enabled: true +inputs: + - proto_file: example.proto +plugins: + - local: /path/to/protoc-slimrpc-plugin + out: . + - remote: buf.build/protocolbuffers/python + out: . +``` + +#### Generate Code + +```bash +buf generate +``` + +#### Advanced buf Configuration + +As before, you can customize the types import. For example, to use existing +types from `a2a.grpc.a2a_pb2`, you can modify the `buf.gen.yaml` as follows: + +```yaml +version: v2 +managed: + enabled: true +plugins: + - local: protoc-slimrpc-plugin + out: generated + opt: + - types_import=from a2a.grpc import a2a_pb2 as a2a__pb2 + - remote: buf.build/protocolbuffers/python + out: generated +``` + +## Generated Code Structure + +For the example above, the generated `example_pb2_slimrpc.py` will contain: + +### Client Stub + +```python +class TestStub: + """Client stub for Test.""" + def __init__(self, channel): + """Constructor. + + Args: + channel: A slimrpc.Channel. + """ + self.ExampleUnaryUnary = channel.unary_unary(...) + self.ExampleUnaryStream = channel.unary_stream(...) + # ... other methods +``` + +### Server Servicer + +```python +class TestServicer(): + """Server servicer for Test. Implement this class to provide your service logic.""" + + def ExampleUnaryUnary(self, request, context): + """Method for ExampleUnaryUnary. Implement your service logic here.""" + raise slimrpc_rpc.SLIMRPCResponseError( + code=code__pb2.UNIMPLEMENTED, message="Method not implemented!" + ) + # ... other methods +``` + +### Registration Function + +```python +def add_TestServicer_to_server(servicer, server: slimrpc.Server): + # Registers the servicer with the SLIMRPC server + pass +``` + +## Plugin Parameters + +The plugin supports the following parameters: + +- `types_import`: Customize how protobuf types are imported + - Example: `types_import="from my_package import types_pb2 as pb2"` + - Default: Uses local import based on the proto file name + +## Example Usage in Python + +### Client Usage + +```python +import asyncio +import logging +from collections.abc import AsyncGenerator + +import slimrpc +from slimrpc.examples.simple.types.example_pb2 import ExampleRequest +from slimrpc.examples.simple.types.example_pb2_slimrpc import TestStub + +logger = logging.getLogger(__name__) + + +async def amain() -> None: + channel = slimrpc.Channel( + local="agntcy/grpc/client", + slim={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + enable_opentelemetry=False, + shared_secret="my_shared_secret", + remote="agntcy/grpc/server", + ) + + # Stubs + stubs = TestStub(channel) + + # Call method + try: + request = ExampleRequest(example_integer=1, example_string="hello") + response = await stubs.ExampleUnaryUnary(request, timeout=2) + + logger.info(f"Response: {response}") + + responses = stubs.ExampleUnaryStream(request, timeout=2) + async for resp in responses: + logger.info(f"Stream Response: {resp}") + + async def stream_requests() -> AsyncGenerator[ExampleRequest]: + for i in range(10): + yield ExampleRequest(example_integer=i, example_string=f"Request {i}") + + response = await stubs.ExampleStreamUnary(stream_requests(), timeout=2) + logger.info(f"Stream Unary Response: {response}") + except asyncio.TimeoutError: + logger.error("timeout while waiting for response") +``` + +### Server Usage + +```python +import asyncio +import logging +from collections.abc import AsyncIterable + +from slimrpc.context import Context +from slimrpc.examples.simple.types.example_pb2 import ExampleRequest, ExampleResponse +from slimrpc.examples.simple.types.example_pb2_slimrpc import ( + TestServicer, + add_TestServicer_to_server, +) +from slimrpc.server import Server + +logger = logging.getLogger(__name__) + + +class TestService(TestServicer): + async def ExampleUnaryUnary( + self, request: ExampleRequest, context: Context + ) -> ExampleResponse: + logger.info(f"Received unary-unary request: {request}") + + return ExampleResponse(example_integer=1, example_string="Hello, World!") + + async def ExampleUnaryStream( + self, request: ExampleRequest, context: Context + ) -> AsyncIterable[ExampleResponse]: + logger.info(f"Received unary-stream request: {request}") + + # generate async responses stream + for i in range(5): + logger.info(f"Sending response {i}") + yield ExampleResponse(example_integer=i, example_string=f"Response {i}") + + async def ExampleStreamUnary( + self, request_iterator: AsyncIterable[ExampleRequest], context: Context + ) -> ExampleResponse: + logger.info(f"Received stream-unary request: {request_iterator}") + + async for request in request_iterator: + logger.info(f"Received stream-unary request: {request}") + response = ExampleResponse( + example_integer=1, example_string="Stream Unary Response" + ) + return response + + async def ExampleStreamStream( + self, request_iterator: AsyncIterable[ExampleRequest], context: Context + ) -> AsyncIterable[ExampleResponse]: + """Missing associated documentation comment in .proto file.""" + raise NotImplementedError("Method not implemented!") + + +def create_server( + local: str, + slim: dict, + enable_opentelemetry: bool = False, + shared_secret: str = "", +) -> Server: + """ + Create a new SLIMRPC server instance. + """ + server = Server( + local=local, + slim=slim, + enable_opentelemetry=enable_opentelemetry, + shared_secret=shared_secret, + ) + + return server + + +async def amain() -> None: + server = create_server( + local="agntcy/grpc/server", + slim={ + "endpoint": "http://localhost:46357", + "tls": { + "insecure": True, + }, + }, + enable_opentelemetry=False, + shared_secret="my_shared_secret", + ) + + # Create RPCs + add_TestServicer_to_server( + TestService(), + server, + ) + + await server.run() +``` + +## Troubleshooting + +### Plugin Not Found + +If you get an error that the plugin is not found: + +- Ensure `protoc-slimrpc-plugin` is in your PATH +- Or specify the full path: + `--plugin=protoc-gen-slimrpc=/full/path/to/protoc-slimrpc-plugin` + +### Import Errors + +If you encounter Python import errors: + +- Make sure the generated `*_pb2.py` files are in your Python path. +- Use the `types_import` parameter to customize import paths. +- Ensure all Protocol Buffer dependencies are generated. + +### Build Errors + +If the plugin fails to build: + +- Ensure you have Rust and Cargo installed. +- Check that all dependencies are available. +- Try cleaning and rebuilding: `cargo clean && cargo build --release`.