update readme, add option to run without auth (#2)

update readme add option to run without auth
This commit is contained in:
Eugene Yurtsev
2025-03-12 15:02:46 -04:00
committed by GitHub
parent 4ed169cc31
commit 4be952e978
4 changed files with 140 additions and 25 deletions
+107 -23
View File
@@ -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`
+13 -1
View File
@@ -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")
+1
View File
@@ -11,4 +11,5 @@ dependencies = [
[dependency-groups]
test = [
"ruff>=0.9.9",
"universal-tool-client>=0.0.2",
]
Generated
+19 -1
View File
@@ -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"