{"id":23449117,"url":"https://github.com/oeway/hypha-rpc","last_synced_at":"2026-04-01T18:18:20.196Z","repository":{"id":249193906,"uuid":"830731965","full_name":"oeway/hypha-rpc","owner":"oeway","description":"RPC client for Hypha","archived":false,"fork":false,"pushed_at":"2026-03-19T04:36:42.000Z","size":6169,"stargazers_count":4,"open_issues_count":25,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-19T20:43:44.314Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oeway.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/security.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-07-18T21:50:48.000Z","updated_at":"2026-03-19T04:30:42.000Z","dependencies_parsed_at":"2026-03-01T15:02:07.242Z","dependency_job_id":null,"html_url":"https://github.com/oeway/hypha-rpc","commit_stats":null,"previous_names":["oeway/hypha-rpc"],"tags_count":91,"template":false,"template_full_name":null,"purl":"pkg:github/oeway/hypha-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oeway%2Fhypha-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oeway%2Fhypha-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oeway%2Fhypha-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oeway%2Fhypha-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oeway","download_url":"https://codeload.github.com/oeway/hypha-rpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oeway%2Fhypha-rpc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-23T22:20:06.482Z","updated_at":"2026-04-01T18:18:20.169Z","avatar_url":"https://github.com/oeway.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hypha RPC\n\nHypha RPC is a simple RPC library for Hypha, a framework for building distributed data management and AI model serving systems.\n\n## Usage\n\n`hypha-rpc` is the Python client library for the [Hypha server](https://docs.amun.ai), which allows you to connect to a Hypha server and interact with its services. You can use the library to call remote functions, register services, and exchange data with the server.\n\n### Installation\n\n```bash\npip install -U hypha-rpc\n```\n\n### Connecting to a Hypha server\n\n```python\nfrom hypha_rpc import connect_to_server\nserver = await connect_to_server({\"server_url\": server_url})\n```\n\nYou can also obtain a login token from the server and use it to connect to the server:\n\n```python\nfrom hypha_rpc import login, connect_to_server\ntoken = await login({\"server_url\": server_url})\nserver = await connect_to_server({\"server_url\": server_url, \"token\": token})\n```\n\n## Data type representation\n\nImJoy RPC is built on top of two-way transport layer. Currently, we use `websocket` to implement the transport layer between different peers. Data with different types are encoded into a unified representation and sent over the transport layer. It will then be decoded into the same or corresponding data type on the other side.\n\nThe data representation is a JSON object (but can contain binary data, e.g. `ArrayBuffer` in JS or `bytes` in Python). The goal is to represent more complex data types with primitive types that are commonly supported by many programming language, including strings, numbers, boolean, bytes, list/array and dictionary/object.\n\n\n| Javascript | Python | hypha-rpc representation |\n|------------|--------- | ---- |\n| String   | str        | v |\n| Number   | int/float | v |\n| Boolean  |  bool     | v |\n| null/undefined  | None    | v |\n| Uint8Array | bytes  | v |\n| ArrayBuffer | memoryview  | {_rtype: \"memoryview\", _rvalue: v} |\n| Array([])   | list/tuple |[_encode(v)] |\n| Object({})  | dict  | {_encode(v)} |\n| Set | Set | {_rtype: \"set\", _rvalue: _encode(Array.from(v))} |\n| Map | OrderedDict  |{_rtype: \"orderedmap\", _rvalue: _encode(Array.from(v))} |\n| Error | Exception | { _rtype: \"error\", _rvalue: v.toString() } |\n| Blob/File | BytesIO/StringIO etc.  | { _rtype: \"iostream\", name: v, type: v.type, read: v.read, seek: v.seek, ...} |\n| DataView | memoryview  |  { _rtype: \"memoryview\", _rvalue: v.buffer }|\n| TypedArray | 1-D numpy array*  |{_rtype: \"typedarray\", _rvalue: v.buffer, _rdtype: dtype} |\n| tf.Tensor/nj.array | numpy array  |{_rtype: \"ndarray\", _rvalue: v.buffer, _rshape: shape, _rdtype: _dtype} |\n| Function* | function/callable* | {_rtype: \"method\", _rtarget: _rid, _rmethod: name, _rpromise: true } |\n| Class | class/dotdict()* | {...} |\n| custom | custom | encoder(v) (default `_rtype` = encoder name) |\n\nNotes:\n - `_encode(...)` in the hypha-rpc representation means the type will be recursively encoded (decoded).\n - When sending functions to be used remotely in a remote function call (e.g. passing an object with member functions when calling a remote function), the functions will only be available during the call and will be removed after the call returns. To keep callbacks alive beyond the function return, mark the containing object with `_rintf: true` — the object will be automatically registered as a persistent local service. See [Persistent Interface Objects (`_rintf`)](#persistent-interface-objects-_rintf) for details.\n - For n-D numpy array, there is no established n-D array library in javascript, the current behavior is, if there is `tf`(Tensorflow.js) detected, then it will be decoded into `tf.Tensor`. If `nj`(numjs) is detected, then it will be decoded into `nj.array`.\n - Typed array will be represented as numpy array if available, otherwise it will be converted to raw bytes.    \n    Type Conversion\n    | Javascript | Numpy  | _rdtype |\n    | -- | -- | -- |\n    | Int8Array | int8 | int8 |\n    | Int16Array| int16 |int16 |\n    |  Int32Array| int32 | int32 |\n    |  Uint8Array| uint8 | uint8 |\n    |  Uint16Array| uint16 | uint16 |\n    |  Uint32Array| uint32 | uint32 |\n    |  Float32Array| float32 | float32 |\n    |  Float64Array| float64 | float64 |\n    |  Array| array | array |\n    |note: 64-bit integers (signed or unsigned) are not supported|\n\n - `dotdict` in Python is a simple wrapper over `dict` that support using the dot notation to get item, similar to what you can do with Javascript object.\n - In Python, file instances (inherit from `io.IOBase`) will be automatically encoded.\n\n## Encoding and decoding custom objects\n\n For the data or object types that are not in the table above, for example, a custom class, you can support them by register your own `codec`(i.e. encoder and decoder) with `api.register_codec()`.\n\n You need to provide a `name`, a `type`, `encoder` and `decoder` function. For example: in javascript, you can call `api.register_codec({\"name\": \"my_custom_codec\", \"type\": MyClass, \"encoder\": (obj)=\u003e{ ... return encoded;}, \"decoder\": (obj)=\u003e{... return decoded;})`, or in Python you can do `api.register_codec(name=\"my_custom_codec\", type=MyClass, encoder=my_encoder_func, decoder=my_decoder_func)`.\n \n\n The basic idea of using a custom codec is to use the `encoder` to represent your custom data type into array/dictionary of primitive types (string, number etc.) such that they can be send via the transport layer of hypha-rpc. Then use the `decoder` to reconstruct the object remotely based on the representation.\n\nFor the `name`, it will be assigned as `_rtype` for the data representation, therefore please be aware that you should not use a name that already used internally (see the table above), unless you want to overried the default encoding. Also note that you cannot overried the encoding of primitive types and functions.\n\nThe `encoder` function take an object as input and you need to return the represented object/dictionary. You can only use primitive types plus array/list and object/dict in the represented object. By default, if your returned object does not contain a key `_rtype`, the codec `name` will be used as `_rtype`. You can also assign a different `_rtype` name, that allows the conversion between different types.\n\nThe `decoder` function converts the encoded object into the actual object. It will only be called when the `_rtype` of an object matches the `name` of the codec.\n\n### Example 1: Encode and Decode xarray\n\nHere you can find an example for encoding and decoding [xarray](https://xarray.dev/):\n```python\nimport asyncio\nfrom hypha_rpc import connect_to_server\nimport xarray as xr\nimport numpy as np\n\ndef encode_xarray(obj):\n    \"\"\"Encode the zarr store.\"\"\"\n    assert isinstance(obj, xr.DataArray)\n    return {\n        \"_rtype\": \"xarray\",\n        \"data\": obj.data,\n        \"dims\": obj.dims,\n        \"attrs\": obj.attrs,\n        \"name\": obj.name,\n    }\n\ndef decode_xarray(obj):\n    assert obj[\"_rtype\"] == \"xarray\"\n    return xr.DataArray(\n                data=obj[\"data\"],\n                dims=obj[\"dims\"],\n                attrs=obj.get(\"attrs\", {}),\n                name=obj.get(\"name\", None),\n        )\n\n\nasync def start_server(server_url):\n    server = await connect_to_server({\"server_url\": server_url})\n\n    # Register the codecs\n    server.register_codec(\n        {\"name\": \"xarray\", \"type\": xr.DataArray, \"encoder\": encode_xarray, \"decoder\": decode_xarray}\n    )\n    \n    z = xr.DataArray(data=np.arange(100), dims=[\"x\"], attrs={\"test\": \"test\"}, name=\"mydata\")\n\n    # Use the echo function to do a round-trip with the xarray object\n    # It will first encode z and send it to the server, then the server return the encoded object and decoded it back to a xarray\n    z2 = await server.echo(z)\n\n    assert isinstance(z2, xr.DataArray)\n    assert z2.attrs[\"test\"] == \"test\"\n    assert z2.dims == (\"x\",)\n    assert z2.data[0] == 0\n    assert z2.data[99] == 99\n    assert z2.name == \"mydata\"\n    print(\"Success!\")\n\nif __name__ == \"__main__\":\n    server_url = \"https://hypha.aicell.io\"\n    loop = asyncio.get_event_loop()\n    loop.create_task(start_server(server_url))\n    loop.run_forever()\n\n```\n\n\n### Example 2: Encode zarr store\n\nSince we can include functions in the encoded object, this allows us sending an interface to the remote location and use it as a lazy object.\n\n```python\nimport asyncio\nfrom hypha_rpc import connect_to_server\n\nimport zarr\nimport numpy as np\n\ndef encode_zarr_store(zobj):\n    \"\"\"Encode the zarr store.\"\"\"\n    import zarr\n\n    path_prefix = f\"{zobj.path}/\" if zobj.path else \"\"\n\n    def getItem(key, options=None):\n        return zobj.store[path_prefix + key]\n\n    def setItem(key, value):\n        zobj.store[path_prefix + key] = value\n\n    def containsItem(key, options=None):\n        if path_prefix + key in zobj.store:\n            return True\n\n    return {\n        \"_rintf\": True,\n        \"_rtype\": \"zarr-array\" if isinstance(zobj, zarr.Array) else \"zarr-group\",\n        \"getItem\": getItem,\n        \"setItem\": setItem,\n        \"containsItem\": containsItem,\n    }\n\n\nasync def start_server(server_url):\n    server = await connect_to_server({\"server_url\": server_url})\n\n    # Register the codecs\n    server.register_codec(\n        {\"name\": \"zarr-group\", \"type\": zarr.Group, \"encoder\": encode_zarr_store}\n    )\n\n    z = zarr.array(np.arange(100))\n  \n    # Use the echo function to do a round-trip with the zarr object\n    # Note: Since we didn't create a decoder, so we won't get the zarr object, but a zarr store interface\n    z2 = await server.echo(z)\n    print(z2)\n\nif __name__ == \"__main__\":\n    server_url = \"https://hypha.aicell.io\"\n    loop = asyncio.get_event_loop()\n    loop.create_task(start_server(server_url))\n    loop.run_forever()\n```\n\n\n### Remote function calls and arguments\n\nRemote function call is almost the same as calling a local function. The arguments are mapped directly, for example, you can call a Python function `foo(a, b, c)` from javascript or vise versa. However, since Javascript does not support named arguments as Python does, ImJoy does the following conversion:\n\n * For functions defined in Javascript, there is no difference when calling from Python\n * For functions defined in Python, when calling from Javascript, if the last argument is an object and its `_rkwargs` is set to true, then it will be converted into keyword arguments when calling the Python function. For example, if you have a Python function defined as `def foo(a, b, c=None):`, in Javascript, you should call it as `foo(9, 10, {c: 33, _rkwargs: true})`.\n\n### Persistent Interface Objects (`_rintf`)\n\nBy default, when you pass an object with callable members (functions) as an argument to a remote function call, those functions are only available **during** the call. Once the remote function returns, the callback session is cleaned up and the functions can no longer be called.\n\nIf you need the remote side to store and call your functions **after** the function returns (e.g. a lazy data store interface), mark the object with `_rintf: True`. This tells hypha-rpc to automatically register the object as a persistent local service instead of using an ephemeral callback session.\n\n#### How it works\n\n1. When `_encode` encounters a dict/object with `_rintf: true` and callable members, it:\n   - Extracts all callable members and registers them as a local service (with an auto-generated ID like `_rintf_abc123`)\n   - Sets `_rintf_service_id` on the **original** object so the caller can look it up later\n   - Includes `_rintf_service_id` in the encoded output sent to the remote side\n2. The service persists until explicitly unregistered or the RPC connection is closed.\n3. The caller can clean up with `rpc.unregister_service(service_id)` when the interface is no longer needed (server notification is automatically skipped for `_rintf` services).\n\n#### Python Example\n\n```python\nfrom hypha_rpc import connect_to_server\n\nserver = await connect_to_server({\"server_url\": server_url})\nworkspace = server.config.workspace\ntoken = await server.generate_token()\n\n# --- Server side: a service that stores an interface for later use ---\nstored_store = None\n\nasync def upload_store(store):\n    \"\"\"Receive a store interface and keep it for later use.\"\"\"\n    nonlocal stored_store\n    # Can call store methods during execution\n    value = await store[\"getItem\"](\"key1\")\n    stored_store = store\n    return value\n\nasync def read_from_store(key):\n    \"\"\"Call the stored interface after upload_store has returned.\"\"\"\n    return await stored_store[\"getItem\"](key)\n\nawait server.register_service({\n    \"id\": \"storage-svc\",\n    \"upload_store\": upload_store,\n    \"read_from_store\": read_from_store,\n})\n\n# --- Client side: pass an _rintf object ---\nclient = await connect_to_server({\n    \"server_url\": server_url,\n    \"workspace\": workspace,\n    \"token\": token,\n})\nsvc = await client.get_service(\"storage-svc\")\n\ndata = {\"key1\": \"hello\", \"key2\": \"world\"}\nmy_store = {\n    \"_rintf\": True,\n    \"getItem\": lambda key: data.get(key),\n}\n\n# After this call, my_store[\"_rintf_service_id\"] is set\nresult = await svc.upload_store(my_store)\nassert result == \"hello\"\n\n# The stored interface still works after upload_store returned\nresult2 = await svc.read_from_store(\"key2\")\nassert result2 == \"world\"\n\n# When done, clean up the local service\nservice_id = my_store[\"_rintf_service_id\"]\nawait client.rpc.unregister_service(service_id)\n```\n\n#### JavaScript Example\n\n```javascript\nconst server = await connectToServer({ server_url: serverUrl });\nconst workspace = server.config.workspace;\nconst token = await server.generateToken();\n\n// Server side\nlet storedStore = null;\nawait server.registerService({\n  id: \"storage-svc\",\n  uploadStore: async (store) =\u003e {\n    const value = await store.getItem(\"key1\");\n    storedStore = store;\n    return value;\n  },\n  readFromStore: async (key) =\u003e {\n    return await storedStore.getItem(key);\n  },\n});\n\n// Client side\nconst client = await connectToServer({\n  server_url: serverUrl,\n  workspace,\n  token,\n});\nconst svc = await client.getService(\"storage-svc\");\n\nconst data = { key1: \"hello\", key2: \"world\" };\nconst myStore = {\n  _rintf: true,\n  getItem: (key) =\u003e data[key] || null,\n};\n\n// After this call, myStore._rintf_service_id is set\nconst result = await svc.uploadStore(myStore);\nconsole.log(result); // \"hello\"\n\n// Still works after uploadStore returned\nconst result2 = await svc.readFromStore(\"key2\");\nconsole.log(result2); // \"world\"\n\n// Clean up when done\nconst serviceId = myStore._rintf_service_id;\nawait client.rpc.unregister_service(serviceId);\n```\n\n#### Cleanup API\n\n| Language | Method | Description |\n|----------|--------|-------------|\n| Python | `await rpc.unregister_service(service_id)` | Removes the local `_rintf` service (raises if not found) |\n| JavaScript | `await rpc.unregister_service(serviceId)` | Removes the local `_rintf` service (throws if not found) |\n\n#### When to use `_rintf` vs `register_service`\n\n| | `_rintf: True` | `register_service()` |\n|--|----------------|----------------------|\n| **Use case** | Passing a store/interface as a function argument | Exposing a named service to the workspace |\n| **Registration** | Automatic (during encoding) | Explicit |\n| **Discovery** | Not discoverable; only the receiver has a reference | Discoverable by service ID |\n| **Cleanup** | `unregister_service(id)` or RPC close | `unregister_service(id)` |\n| **Lifecycle** | Tied to the object and the RPC connection | Tied to the RPC connection |\n\n### Context Injection with `require_context`\n\nWhen registering a service, you can set `require_context: true` in the service configuration to automatically inject execution context into your service methods. This is useful for accessing information about the caller, workspace, user permissions, etc.\n\n#### How it works\n\nWhen `require_context` is enabled, Hypha RPC automatically adds a `context` parameter to your method calls:\n\n**Python:**\n```python\ndef my_service_method(arg1, arg2, context=None, **kwargs):\n    \"\"\"Service method that receives context automatically.\"\"\"\n    # context contains: {\"from\": \"...\", \"to\": \"...\", \"ws\": \"...\", \"user\": {...}}\n    workspace = context[\"ws\"]\n    user_info = context[\"user\"]\n    return f\"Hello {user_info.get('id', 'anonymous')} from {workspace}\"\n\n# Register service with require_context\nawait server.register_service({\n    \"id\": \"my-service\",\n    \"config\": {\n        \"require_context\": True, \"visibility\": \"public\"\n    },\n    \"my_method\": my_service_method\n})\n```\n\n**JavaScript:**\n```javascript\nfunction myServiceMethod(arg1, arg2, kwargs) {\n    // For require_context methods, kwargs will have _rkwargs=true and contain context\n    if (kwargs \u0026\u0026 kwargs._rkwargs) {\n        const context = kwargs.context;\n        const workspace = context.ws;\n        const userInfo = context.user;\n        return `Hello ${userInfo.id || 'anonymous'} from ${workspace}`;\n    }\n    throw new Error(\"Context not available\");\n}\n\n// Register service with require_context\nawait server.registerService({\n    id: \"my-service\",\n    config: {\n        require_context: true, visibility: \"public\"\n    },\n    myMethod: myServiceMethod\n});\n```\n\n#### Context Information\n\nThe injected context object contains:\n- `from`: The caller's client ID (e.g., \"workspace/client-id\")\n- `to`: The target service path \n- `ws`: The workspace name\n- `user`: User information object with permissions and identity\n\n#### Usage Notes\n\n- Context injection works consistently across both Python and JavaScript implementations\n- The context is automatically filtered out from function signatures when generating schemas\n- Built-in services (like `get_service`, `ping`, etc.) handle context injection transparently\n- External client services receive context via the kwargs mechanism with the `_rkwargs` flag\n\n### Generators Support\n\nHypha RPC supports both synchronous and asynchronous generators across Python and JavaScript. This allows you to stream data between services efficiently.\n\n#### Python Generators\n\nYou can define both regular and async generators in your Python services:\n\n```python\n# Regular generator\ndef counter(start=0, end=5):\n    \"\"\"Return a generator that counts from start to end.\"\"\"\n    for i in range(start, end):\n        yield i\n\n# Async generator\nasync def async_counter(start=0, end=5):\n    \"\"\"Return an async generator that counts from start to end.\"\"\"\n    for i in range(start, end):\n        yield i\n        await asyncio.sleep(0.01)  # Small delay to simulate async work\n\n# Register service with generators\nawait server.register_service({\n    \"id\": \"generator-service\",\n    \"config\": {\n        \"visibility\": \"public\",\n        \"require_context\": True,\n    },\n    \"get_counter\": counter,\n    \"get_async_counter\": async_counter,\n})\n```\n\n##### Using Generators with Async API\n\nWhen using the async API, all generators (both regular and async) are consumed using async iteration:\n\n```python\n# Connect to the service\ngen_service = await client.get_service(\"generator-service\")\n\n# Using regular generator (becomes async over RPC)\ngen = await gen_service.get_counter(0, 5)\nasync for item in gen:\n    print(item)  # Prints: 0, 1, 2, 3, 4\n\n# Using async generator\nasync_gen = await gen_service.get_async_counter(0, 5)\nasync for item in async_gen:\n    print(item)  # Prints: 0, 1, 2, 3, 4\n```\n\n##### Using Generators with Sync API\n\nThe synchronous API allows you to use generators with regular for loops:\n\n```python\n# Connect using sync API\nclient = connect_to_server_sync({\n    \"server_url\": \"https://hypha.aicell.io\",\n})\ngen_service = client.get_service(\"generator-service\")\n\n# Both regular and async generators can be used with for loops\nfor item in gen_service.get_counter(0, 5):\n    print(item)  # Prints: 0, 1, 2, 3, 4\n\nfor item in gen_service.get_async_counter(0, 5):\n    print(item)  # Prints: 0, 1, 2, 3, 4\n```\n\n#### JavaScript Generators\n\nIn JavaScript, you can define and consume generators in a similar way:\n\n```javascript\n// Define a generator service\nconst generatorService = {\n    *counter(start = 0, end = 5) {\n        for (let i = start; i \u003c end; i++) {\n            yield i;\n        }\n    },\n    \n    async *asyncCounter(start = 0, end = 5) {\n        for (let i = start; i \u003c end; i++) {\n            yield i;\n            await new Promise(resolve =\u003e setTimeout(resolve, 10));\n        }\n    }\n};\n\n// Register the service\nawait server.registerService({\n    id: \"js-generator-service\",\n    config: { visibility: \"public\" },\n    ...generatorService\n});\n\n// Consume generators\nconst service = await client.getService(\"js-generator-service\");\n\n// Using regular generator\nconst gen = await service.counter(0, 5);\nfor await (const item of gen) {\n    console.log(item); // Prints: 0, 1, 2, 3, 4\n}\n\n// Using async generator\nconst asyncGen = await service.asyncCounter(0, 5);\nfor await (const item of asyncGen) {\n    console.log(item); // Prints: 0, 1, 2, 3, 4\n}\n```\n\nNote: When using generators across RPC:\n * All generators become async generators when accessed remotely\n * The sync API in Python automatically handles the async-to-sync conversion\n * Values are streamed one at a time, making it memory efficient for large datasets\n * Generators are great for implementing progress updates or streaming data\n\n## Type Annotations for LLM Function Calling\n\nHypha RPC supports generating standardized function schemas based on type annotations, which is particularly useful for integrating with Large Language Models (LLMs) that support function calling (like OpenAI's models).\n\n### Python\n\nIn Python, you can use standard type hints, docstrings, and Pydantic models along with the `@schema_function` decorator (from `hypha_rpc.utils.schema`) to automatically generate a JSON schema compatible with LLM function calling standards.\n\n**Example with basic types:**\n\n```python\nfrom hypha_rpc.utils.schema import schema_function\n\n@schema_function\ndef get_current_weather(location: str, unit: str = \"fahrenheit\") -\u003e str:\n    \"\"\"Get the current weather in a given location.\n\n    Args:\n        location: The city and state, e.g. San Francisco, CA.\n        unit: The temperature unit, either \"celsius\" or \"fahrenheit\".\n\n    Returns:\n        A JSON string with the weather information.\n    \"\"\"\n    # (Implementation details omitted for brevity)\n    import json\n    if \"tokyo\" in location.lower():\n        return json.dumps({\"location\": \"Tokyo\", \"temperature\": \"10\", \"unit\": unit})\n    # ... other locations ...\n    else:\n        return json.dumps({\"location\": location, \"temperature\": \"unknown\"})\n```\n\n**Example with Pydantic:**\n\n```python\nfrom pydantic import BaseModel, Field\nfrom hypha_rpc.utils.schema import schema_function\n\nclass UserInfo(BaseModel):\n    \"\"\"User information.\"\"\"\n    name: str = Field(..., description=\"Name of the user\")\n    email: str = Field(..., description=\"Email of the user\")\n    age: int = Field(..., description=\"Age of the user\")\n    address: str = Field(..., description=\"Address of the user\")\n\n@schema_function\ndef register_user(user_info: UserInfo) -\u003e str:\n    \"\"\"Register a new user.\"\"\"\n    return f\"User {user_info.name} registered\"\n```\n\nThe decorator attaches the generated schema to the function's `__schema__` attribute. When you register a service containing these decorated functions, the schema information is included in the service registration details, making it available for clients (or LLMs) to understand how to call the functions.\n\n```python\n# Example service registration\nawait server.register_service({\n    \"name\": \"User Service\",\n    \"id\": \"user-service\",\n    \"description\": \"Service for registering users\",\n    \"register_user\": register_user # Decorated function\n})\n```\n\n### JavaScript\n\nJavaScript utilizes the `schemaFunction` utility (imported from `hypha-rpc/utils/schema.js` or re-exported by `hypha-rpc`) to achieve similar results.\n\nYou provide the function implementation and a separate schema object detailing the function's name, description, and parameters (following JSON Schema conventions).\n\n**Example:**\n\n```javascript\nimport { schemaFunction } from \"./hypha-rpc.js\"; // Adjust import path as needed\n\n// Define the function implementation\nconst multiply = (a, b) =\u003e a * b;\n\n// Define the schema\nconst multiplySchema = {\n    name: \"multiply\",\n    description: \"Multiplies two numbers.\",\n    parameters: {\n        type: \"object\",\n        properties: {\n            a: { type: \"number\", description: \"First number\" },\n            b: { type: \"number\", description: \"Second number\" },\n        },\n        required: [\"a\", \"b\"],\n        // Note: Return value schema is not explicitly part of this standard schema,\n        // but can be included in the description or a custom field if needed.\n    },\n};\n\n// Create the annotated function\nconst annotatedMultiply = schemaFunction(multiply, multiplySchema);\n\n// Register the service\nawait server.registerService({\n    id: \"calculator-service\",\n    config: { visibility: \"public\" },\n    multiply: annotatedMultiply, // Use the annotated function\n});\n```\n\nThe `schemaFunction` utility attaches the provided schema to the `__schema__` property of the returned function object (`annotatedMultiply` in the example). When the service is registered, this schema is included, similar to the Python version.\n\n## Peer-to-peer connection via WebRTC\n\nThe current implementation requires all the traffic going through the websocket server. This is not ideal for large data transmission. Therefore, we implemented webRTC support in addition to the websocket connection. You can use the following two functions for enabling peer-to-peer communication between clients:\n\nHere is an example for setting up a webrtc service on the python side:\n\n```python\nfrom hypha_rpc import connect_to_server, register_rtc_service, get_rtc_service\nserver = await connect_to_server({\"server_url\": \"https://hypha.aicell.io\"})\nawait register_rtc_service(server, \"webrtc-service\")\n```\n\nYou can also use the synchronous version:\n\n```python\nfrom hypha_rpc.sync import register_rtc_service, get_rtc_service\n```\n\nNow, in the browser, you can connect to the server and get the webrtc service:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/hypha-rpc@0.5.30/dist/hypha-rpc-websocket.min.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\nconst server = await hyphaWebsocketClient.connectToServer({\"server_url\": \"https://hypha.aicell.io\"})\nconst pc = await hyphaWebsocketClient.getRTCService(server, \"webrtc-service\");\nconst svc = await pc.get_service(\"hello\"); // now you can get service via webrtc\n// ...\n\u003c/script\u003e\n```\n\nIt works by using hypha server as a signaling server, after establishing the connection, the rest goes through webrtc in a peer-to-peer manner. \n\nBoth `register_rtc_service` and `get_rtc_service` take an optional `config` object as the last argument. The `config` object can contain a `on_init(peer_connection)` callback function that will be called when the webrtc connection is established.\n\nYou can setup streaming services inside the `on_init` callback. This is ideally suited for applications such as microscope control. As an example, we generate a random video stream on the python side, and provide a microscope control service (e.g. move stage and snap image): https://github.com/oeway/webrtc-hypha-demo\n\n### Enable WebRTC automatically\n\nYou can also enable webrtc for the `connect_to_server` function, by setting the `webrtc` option to `True` or `auto` in the config object. For example:\n\n```python\nserver = await connect_to_server({\"server_url\": \"https://hypha.aicell.io\", \"webrtc\": True})\n```\n\nOr javascript:\n\n```javascript\nconst server = await hyphaWebsocketClient.connectToServer({\"server_url\": \"https://hypha.aicell.io\", \"webrtc\": true})\n```\n\nThis will automatically register a webrtc service (named as `\u003cclient_id\u003e-rtc`) so that other clients can connect to it.\n\nNow if you register a hypha service, it will be automatically made available through the webrtc connection.\n\nTo get the service via webrtc, you can pass `webrtc=True` and `webrtc_config` to `server.get_service()`:\n\n```python\nsvc = await server.get_service(\"my-service\", webrtc=True, webrtc_config={})\n```\n\nIn the above example, we only show how to enable it in Python, but it also works in Javascript. However, please not that the webrtc won't work directly in pyodide-based environment (e.g. in JupyterLite).\n\n## Synchronous Wrapper\n\nTo make it easier to work with synchronous python code, we provide a synchronous wrapper, which allows for synchronous usage of the asynchronous `hypha_rpc` API.\n\nTo use the synchronous wrapper, you can import the following functions from the `hypha_rpc.sync` module:\n\n```python\nfrom hypha_rpc.sync import login, connect_to_server, get_rtc_service, register_rtc_service\n```\n**connect_to_server**\n\nThe `connect_to_server` function creates a synchronous Hypha server instance and establishes a connection to the server. It takes a configuration object as an argument and returns the server instance.\n\n```python\nserver = connect_to_server(config)\n```\n\n**Example:**\n\n```python\nserver_url = \"https://hypha.aicell.io\"\nserver = connect_to_server({\"server_url\": server_url})\n```\n\n\n**login**\n\nThe `login` function is used to log in to a Hypha server. It takes a configuration object as an argument and returns the token for connecting to the server.\n\n```python\ntoken = login(config)\n```\n\n**Example:**\n\n```python\nserver_url = \"https://hypha.aicell.io\"\n\ndef login_callback(context):\n    print(\"Please open the following URL in your browser to log in:\")\n    print(context[\"login_url\"])\n\nconfig = {\n    \"server_url\": server_url,\n    \"login_callback\": login_callback,\n}\n\ntoken = login(config)\nserver = connect_to_server({\"server_url\": server_url, \"token\": token})\n```\n\nThe `config` object should contain the following properties:\n\n- `server_url`: The URL of the Hypha server.\n- `login_service_id`: The service ID for the login service (default: \"public/*:hypha-login\").\n- `login_timeout`: The timeout duration for the login process (default: 60 seconds).\n- `login_callback`: An optional callback function to handle the login process.\n\nThe `login` function connects to the Hypha server, starts the login service, and initiates the login process. If a `login_callback` function is provided, it will be called with the login context. Otherwise, the login URL will be printed to the console, and the user needs to open their browser and complete the login process.\n\nThe function returns the result of the login process, which is obtained by checking the login key within the specified timeout duration.\n\n\n**get_rtc_service**\n\nThe `get_rtc_service` function retrieves a synchronous Real-Time Communication (RTC) service from the Hypha server. It takes the server instance and a service ID as arguments and returns the synchronous RTC service.\n\n```python\nrtc_service = get_rtc_service(server, service_id, config=None)\n```\n\n**Example:**\n\n```python\nrtc_service = get_rtc_service(server, \"webrtc-service\")\n```\n\n**register_rtc_service**\n\nThe `register_rtc_service` function registers a synchronous RTC service with the Hypha server. It takes the server instance, service ID, and an optional configuration object as arguments.\n\n```python\nregister_rtc_service(server, service_id, config=None)\n```\n\n**Example:**\n\n```python\nregister_rtc_service(\n    server,\n    service_id=\"webrtc-service\",\n    config={\n        \"visibility\": \"public\",\n        # \"ice_servers\": ice_servers,\n    },\n)\n```\n\nPlease note that the synchronous wrapper is designed to provide a convenient synchronous interface for the asynchronous `hypha-rpc` API. It utilizes asyncio and threading under the hood to achieve synchronous behavior.\n\n## End-to-End Encryption\n\nHypha RPC supports opt-in end-to-end encryption (E2E) so that the Hypha server — which acts as a message relay — cannot read or tamper with RPC payloads. Encryption uses **libsodium's `crypto_box`** (Curve25519 + XSalsa20-Poly1305) via [PyNaCl](https://pynacl.readthedocs.io/) in Python and [tweetnacl](https://tweetnacl.js.org/) in JavaScript.\n\nThe encryption libraries are **optional dependencies** — install them only if you need E2E encryption:\n\n```bash\n# Python\npip install hypha-rpc[encryption]\n\n# JavaScript (tweetnacl is installed automatically as an optional dependency)\nnpm install tweetnacl\n```\n\nFor a full security analysis, threat model, and architectural details, see [docs/security.md](docs/security.md).\n\n### Quick Start\n\n**1. Enable encryption when connecting:**\n\n```python\nfrom hypha_rpc import connect_to_server\n\nserver = await connect_to_server({\n    \"server_url\": \"https://hypha.aicell.io\",\n    \"encryption\": True,  # Generates a Curve25519 keypair\n})\n```\n\n```javascript\nconst server = await hyphaWebsocketClient.connectToServer({\n    server_url: \"https://hypha.aicell.io\",\n    encryption: true,\n});\n```\n\n**2. Register an encrypted service with `trusted_keys`:**\n\n```python\n# Get this client's public key (hex string) — share it out-of-band\nmy_pub_key = server.rpc.get_public_key()\n\nawait server.register_service({\n    \"id\": \"secure-analysis\",\n    \"config\": {\n        \"visibility\": \"protected\",\n        \"trusted_keys\": [authorized_caller_pub_key],  # Only these callers allowed\n    },\n    \"analyze\": lambda data: do_analysis(data),\n})\n```\n\n```javascript\nconst myPubKey = server.rpc.getPublicKey();\n\nawait server.registerService({\n    id: \"secure-analysis\",\n    config: {\n        visibility: \"protected\",\n        trusted_keys: [authorizedCallerPubKey],\n    },\n    analyze: (data) =\u003e doAnalysis(data),\n});\n```\n\n**3. Call the encrypted service (caller provides the target's public key):**\n\n```python\n# The caller must know the service's public key (exchanged out-of-band)\nsvc = await client.get_service(\"secure-analysis\",\n    encryption_public_key=service_pub_key\n)\nresult = await svc.analyze(sensitive_data)  # Encrypted transparently\n```\n\n```javascript\nconst svc = await client.getService(\"secure-analysis\", {\n    encryption_public_key: servicePubKey,\n});\nconst result = await svc.analyze(sensitiveData);\n```\n\n### Key Concepts\n\n| Concept | Description |\n|---------|-------------|\n| **Out-of-band key exchange** | Public keys are shared independently of the server (e.g. config file, secure channel). The server never distributes or sees encryption keys. |\n| **`trusted_keys`** | A list of hex-encoded Curve25519 public keys. Only callers whose key is in the list can invoke the service. |\n| **`encryption_public_key`** | Passed by the caller to `get_service()`. Tells hypha-rpc which public key to encrypt payloads for. |\n| **Transparent encryption** | Once configured, all RPC calls and return values are automatically encrypted/decrypted. No changes to service function signatures. |\n| **Selective encryption** | Encryption is opt-in per service. Unencrypted services continue to work as before. |\n\n### Generating and Sharing Keys\n\n```python\nfrom hypha_rpc.crypto import generate_encryption_keypair, public_key_to_hex\n\nprivate_key, public_key = generate_encryption_keypair()\nprint(public_key_to_hex(public_key))  # 64-char hex string to share\n```\n\n```javascript\nimport { generateEncryptionKeypair, publicKeyToHex } from \"hypha-rpc\";\n\nconst { privateKey, publicKey } = await generateEncryptionKeypair();\nconsole.log(publicKeyToHex(publicKey));  // 64-char hex string to share\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foeway%2Fhypha-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foeway%2Fhypha-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foeway%2Fhypha-rpc/lists"}