From 4be952e978b4621392b75c10a80fd34cb62a3c1b Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Wed, 12 Mar 2025 15:02:46 -0400 Subject: [PATCH] update readme, add option to run without auth (#2) update readme add option to run without auth --- README.md | 130 ++++++++++++++++++++++++++++++++++++++++--------- app/server.py | 14 +++++- pyproject.toml | 1 + uv.lock | 20 +++++++- 4 files changed, 140 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d689799..39618b7 100644 --- a/README.md +++ b/README.md @@ -13,33 +13,117 @@ This server implements the following example tools: ## Usage +### Local + +#### No Authentication + +If you want to spin the server up locally to test it out without authentication, you can run the following command: + +```shell +DISABLE_AUTH=true uv run uvicorn app.server:app +``` + +You'll need to have `uv` installed: https://docs.astral.sh/uv/ + +Once the server is running, if auth is disabled, you'll be able to access the API documentation at: http://localhost:8000/docs + +#### With Authentication + +The server implements a very basic form of authentication that supports a single user. To use it, you'll need to set an `APP_SECRET` environment variable. + + +1. Generate a secret using your favorite random number generator + + ```shell + export APP_SECRET=$(openssl rand -base64 32 ) + ``` + + or + + ```shell + export APP_SECRET=$(head -c 32 /dev/urandom | base64) + ``` + + or + + ```shell + export APP_SECRET="some_super_secure_password" + ``` + +2. Run with `uv` + + ```shell + APP_SECRET=$APP_SECRET uv run uvicorn app.server:app + ```` + +### Docker + 1. Build with docker - -```shell -docker build -t example-tool-server . -``` - -2. Generate a secret using your favorite random number generator - - -```shell -export APP_SECRET=$(openssl rand -base64 32 ) -``` - -```shell -export APP_SECRET=$(head -c 32 /dev/urandom | base64) -``` - -```shell -export APP_SECRET=$( let your cat walk across your keyboard) -``` - - + + ```shell + docker build -t example-tool-server . + ``` +2. Generate a secret key 3. Run the image locally + ```shell + docker run -e APP_SECRET=$APP_SECRET -p 8080:8080 example-tool-server + ``` + +Alternatively, deploy to your favorite cloud provider. + + +## Client + +Once the server is running you can use the universal-tool-client to interact with it. ```shell -docker run -e APP_SECRET=$APP_SECRET -p 8080:8080 example-tool-server +pip install universal-tool-client ``` +### Client -or deploy to your favorite cloud provider +```python +from universal_tool_client import get_sync_client + +url = "http://localhost:8000" +headers = { + "Authorization": "YOUR SECRET GOES HERE", +} + +client = get_sync_client(url=url, headers=headers) + +print(client.health()) # Health check +print(client.info()) # Server version and other information + +# List tools +print(client.tools.list()) # List of tools +# Call a tool +print(client.tools.call("echo", {"msg": "hello"})) # hello! + +# Get the tools as a LangchainTools object +tools = client.tools.as_langchain_tools() +``` + +#### Curl + +No authentication: + +```shell +curl -X 'POST' \ + 'http://127.0.0.1:8000/tools/call' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "$schema": "otc://1.0", + "request": { + "tool_id": "get_weather@1.0.0", + "input": { "city": "san francisco"} + } +}' +``` + +With authentication: + +Add the `Authorization` header with the secret you generated earlier. + +e.g., `-H 'Authorization: YOUR SECRET` \ No newline at end of file diff --git a/app/server.py b/app/server.py index 8322c60..46c9bbd 100644 --- a/app/server.py +++ b/app/server.py @@ -11,6 +11,8 @@ from app.tools.github import get_github_issues from app.tools.hackernews import search_hackernews from app.tools.reddit import search_reddit_news +DISABLE_AUTH = os.environ.get("DISABLE_AUTH", "").lower() in ("true", "1") + def _get_app_secret() -> str: """Get the app secret from the environment. @@ -19,6 +21,12 @@ def _get_app_secret() -> str: a single "user" with a single secret key. """ secret = os.environ.get("APP_SECRET") + + if DISABLE_AUTH: + if secret: + raise ValueError("APP_SECRET is not needed when DISABLE_AUTH is enabled.") + return "" + if not secret: raise ValueError("APP_SECRET environment variable is required.") if secret != secret.strip(): @@ -32,7 +40,7 @@ APP_SECRET = _get_app_secret() app = Server() -@app.tool() +@app.add_tool() async def echo(msg: str) -> str: """Echo a message appended with an exclamation mark.""" return msg + "!" @@ -66,6 +74,10 @@ app.add_auth(auth) @auth.authenticate async def authenticate(authorization: str) -> dict: """Authenticate the user based on the Authorization header.""" + if DISABLE_AUTH: + return { + "identity": "unauthenticated-user", + } if not authorization or not hmac.compare_digest(authorization, APP_SECRET): raise Auth.exceptions.HTTPException(status_code=401, detail="Unauthorized") diff --git a/pyproject.toml b/pyproject.toml index 1b13f30..850356b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,4 +11,5 @@ dependencies = [ [dependency-groups] test = [ "ruff>=0.9.9", + "universal-tool-client>=0.0.2", ] diff --git a/uv.lock b/uv.lock index 0e67a8f..677ca20 100644 --- a/uv.lock +++ b/uv.lock @@ -137,13 +137,17 @@ dependencies = [ [package.dev-dependencies] test = [ { name = "ruff" }, + { name = "universal-tool-client" }, ] [package.metadata] requires-dist = [{ name = "universal-tool-server", specifier = ">=0.0.3" }] [package.metadata.requires-dev] -test = [{ name = "ruff", specifier = ">=0.9.9" }] +test = [ + { name = "ruff", specifier = ">=0.9.9" }, + { name = "universal-tool-client", specifier = ">=0.0.2" }, +] [[package]] name = "fastapi" @@ -578,6 +582,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "universal-tool-client" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/8a/b4a1e9db2a958b40498a13547a32b878eef3a54981292d185e97d1d1fe05/universal_tool_client-0.0.2.tar.gz", hash = "sha256:8b78ae67b439b824430b61b6974fa1ec488fe346cfc1dbe3909149894579991b", size = 56609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/ad/cd0bbdb5cb9dc329ab187d35494170cddb84c76e3da861f37910a1c33db8/universal_tool_client-0.0.2-py3-none-any.whl", hash = "sha256:b7cd9763634ac4064b2e8b5dd44292e868406d7102cabb486e53fc4236d31643", size = 8348 }, +] + [[package]] name = "universal-tool-server" version = "0.0.3"