Skip to content

Commit

Permalink
Allow standalone apps source (#724)
Browse files Browse the repository at this point in the history
* allow https source for apps

* Enhance apps source

* Fix documentation

* Fix visibility

* bump hypha-rpc version

* Fix visibility

* increase sleep time

* add disconnect

* mark as not ready immediately

* mark not ready immediately

* wait longer
  • Loading branch information
oeway authored Dec 18, 2024
1 parent 6736fd0 commit f6670f0
Show file tree
Hide file tree
Showing 19 changed files with 90 additions and 62 deletions.
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
* [Launch Service](/launch-service)
* [Operate Files](/operate-files)
* [Artifact Manager](/artifact-manager)
* [Vector Search](/vector-search)
* [Development](/development)
8 changes: 4 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ svc = await get_remote_service("http://localhost:9527/ws-user-scintillating-lawy
Include the following script in your HTML file to load the `hypha-rpc` client:

```html
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-rpc-websocket.min.js"></script>
```

Use the following code in JavaScript to connect to the server and access an existing service:
Expand Down Expand Up @@ -445,13 +445,13 @@ async def start_server(server_url):
# Replace this with your own liveness check
return {"status": "ok"}

# Register a probe for the service
await server.register_probe({
# Register probes for the service
await server.register_probes({
"readiness": check_readiness,
"liveness": check_liveness,
})

# This will register a "probes" service where you can accessed via hypha or the HTTP proxy
# This will register probes service where you can accessed via hypha or the HTTP proxy
print(f"Probes registered at workspace: {server.config.workspace}")
print(f"Test it with the HTTP proxy: {server_url}/{server.config.workspace}/services/probes/readiness")

Expand Down
10 changes: 5 additions & 5 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To connect to the server, instead of installing the `imjoy-rpc` module, you will
pip install -U hypha-rpc # new install
```

We also changed our versioning strategy, we use the same version number for the server and client, so it's easier to match the client and server versions. For example, `hypha-rpc` version `0.20.42` is compatible with Hypha server version `0.20.42`.
We also changed our versioning strategy, we use the same version number for the server and client, so it's easier to match the client and server versions. For example, `hypha-rpc` version `0.20.44` is compatible with Hypha server version `0.20.44`.

#### 2. Change the imports to use `hypha-rpc`

Expand Down Expand Up @@ -128,10 +128,10 @@ loop.run_forever()
To connect to the server, instead of using the `imjoy-rpc` module, you will need to use the `hypha-rpc` module. The `hypha-rpc` module is a standalone module that provides the RPC connection to the Hypha server. You can include it in your HTML using a script tag:

```html
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-rpc-websocket.min.js"></script>
```

We also changed our versioning strategy, we use the same version number for the server and client, so it's easier to match the client and server versions. For example, `hypha-rpc` version `0.20.42` is compatible with Hypha server version `0.20.42`.
We also changed our versioning strategy, we use the same version number for the server and client, so it's easier to match the client and server versions. For example, `hypha-rpc` version `0.20.44` is compatible with Hypha server version `0.20.44`.

#### 2. Change the connection method and use camelCase for service function names

Expand All @@ -149,7 +149,7 @@ Here is a suggested list of search and replace operations to update your code:
Here is an example of how the updated code might look:

```html
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-rpc-websocket.min.js"></script>
<script>
async function main(){
const server = await hyphaWebsocketClient.connectToServer({"server_url": "https://hypha.amun.ai"});
Expand Down Expand Up @@ -197,7 +197,7 @@ We created a tutorial to introduce this new feature: [service type annotation](.
Here is a quick example in JavaScript:

```html
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-rpc-websocket.min.js"></script>

<script>
async function main(){
Expand Down
2 changes: 1 addition & 1 deletion docs/service-type-annotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ if __name__ == "__main__":
**JavaScript Client: Service Usage**

```html
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-rpc-websocket.min.js"></script>
<script>
async function main() {
const server = await hyphaWebsocketClient.connectToServer({"server_url": "https://hypha.amun.ai"});
Expand Down
2 changes: 1 addition & 1 deletion docs/rag.md → docs/vector-search.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tutorial: Using Vector Collections for Retrieval-Augmented Generation
# Tutorial: Vector Search for Retrieval-Augmented Generation

Vector collections are a powerful feature in the `Artifact Manager`, enabling the efficient management and querying of high-dimensional data, such as embeddings for text, images, or other data types. These collections are especially useful in advanced applications like Retrieval-Augmented Generation (RAG) systems.

Expand Down
2 changes: 1 addition & 1 deletion helm-charts/aks-hypha.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ replicaCount: 1
image:
repository: ghcr.io/amun-ai/hypha
pullPolicy: IfNotPresent
tag: "0.20.42"
tag: "0.20.44"
serviceAccount:
create: true
Expand Down
2 changes: 1 addition & 1 deletion helm-charts/hypha-server/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.20.42
version: 0.20.44

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
Expand Down
2 changes: 1 addition & 1 deletion helm-charts/hypha-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ image:
repository: ghcr.io/amun-ai/hypha
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "0.20.42"
tag: "0.20.44"

imagePullSecrets: []
nameOverride: ""
Expand Down
2 changes: 1 addition & 1 deletion hypha/VERSION
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "0.20.42"
"version": "0.20.44"
}
88 changes: 53 additions & 35 deletions hypha/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,6 @@ async def install(
context: Optional[dict] = None,
) -> str:
"""Save a server app."""
if config:
config["entry_point"] = config.get("entry_point", "index.html")
template = config.get("type") + "." + config["entry_point"]
else:
template = "hypha"
if not workspace:
workspace = context["ws"]

Expand All @@ -134,15 +129,25 @@ async def install(
f"User {user_info.id} does not have permission"
f" to install apps in workspace {workspace_info.id}"
)


if config:
config["entry_point"] = config.get("entry_point", "index.html")
template = config.get("type") + "." + config["entry_point"]
else:
template = "hypha"

if source.startswith("http"):
if not source.startswith("https://"):
raise Exception("Only https sources are allowed: " + source)
# download source with httpx
async with httpx.AsyncClient() as client:
response = await client.get(source)
assert response.status_code == 200, f"Failed to download {source}"
source = response.text
if not (source.startswith("https://") or source.startswith("http://localhost") or source.startswith("http://127.0.0.1")):
raise Exception("Only secured https urls are allowed: " + source)
if source.startswith("https://") and (source.split("?")[0].endswith(".imjoy.html") or source.split("?")[0].endswith(".hypha.html")):
# download source with httpx
async with httpx.AsyncClient() as client:
response = await client.get(source)
assert response.status_code == 200, f"Failed to download {source}"
source = response.text
else:
template = None

# Compute multihash of the source code
mhash = multihash.digest(source.encode("utf-8"), "sha2-256")
mhash = mhash.encode("base58").decode("ascii")
Expand All @@ -152,7 +157,12 @@ async def install(
assert target_mhash.verify(
source.encode("utf-8")
), f"App source code verification failed (source_hash: {source_hash})."
if template == "hypha":

if template is None:
config = config or {}
config["entry_point"] = config.get("entry_point", source)
entry_point = config["entry_point"]
elif template == "hypha":
if not source:
raise Exception("Source should be provided for hypha app.")
assert config is None, "Config should not be provided for hypha app."
Expand Down Expand Up @@ -208,14 +218,17 @@ async def install(

app_id = f"{mhash}"

public_url = f"{self.public_base_url}/{workspace_info.id}/artifacts/applications:{app_id}/files/{entry_point}"
artifact_obj = convert_config_to_artifact(config, app_id, public_url)
artifact_obj.update(
{
"local_url": f"{self.local_base_url}/{workspace_info.id}/artifacts/applications:{app_id}/files/{entry_point}",
"public_url": public_url,
}
)
if template:
public_url = f"{self.public_base_url}/{workspace_info.id}/artifacts/applications:{app_id}/files/{entry_point}"
artifact_obj = convert_config_to_artifact(config, app_id, public_url)
artifact_obj.update(
{
"local_url": f"{self.local_base_url}/{workspace_info.id}/artifacts/applications:{app_id}/files/{entry_point}",
"public_url": public_url,
}
)
else:
artifact_obj = convert_config_to_artifact(config, app_id, source)
ApplicationManifest.model_validate(artifact_obj)

try:
Expand All @@ -235,16 +248,17 @@ async def install(
version="stage",
context=context,
)

# Upload the main source file
put_url = await self.artifact_manager.put_file(
artifact["id"], file_path=config["entry_point"], context=context
)
async with httpx.AsyncClient() as client:
response = await client.put(put_url, data=source)
assert (
response.status_code == 200
), f"Failed to upload {config['entry_point']}"

if template:
# Upload the main source file
put_url = await self.artifact_manager.put_file(
artifact["id"], file_path=config["entry_point"], context=context
)
async with httpx.AsyncClient() as client:
response = await client.put(put_url, data=source)
assert (
response.status_code == 200
), f"Failed to upload {config['entry_point']}"

if version != "stage":
# Commit the artifact if stage is not enabled
Expand Down Expand Up @@ -409,10 +423,12 @@ async def start(
)
entry_point = manifest.entry_point
assert entry_point, f"Entry point not found for app {app_id}."
if not entry_point.startswith("http"):
entry_point = f"{self.local_base_url}/{workspace}/artifacts/applications:{app_id}/files/{entry_point}"
server_url = self.local_base_url
local_url = (
f"{self.local_base_url}/{workspace}/artifacts/applications:{app_id}/files/{entry_point}?"
+ f"client_id={client_id}&workspace={workspace}"
f"{entry_point}?"
+ f'server_url={server_url}&client_id={client_id}&workspace={workspace}'
+ f"&app_id={app_id}"
+ f"&server_url={server_url}"
+ (f"&token={token}" if token else "")
Expand Down Expand Up @@ -502,6 +518,8 @@ def service_added(info: dict):
)

# save the services
manifest.name = manifest.name or app_info.get("name", "Untitled App")
manifest.description = manifest.description or app_info.get("description", "")
manifest.services = collected_services
manifest = ApplicationManifest.model_validate(
manifest.model_dump(mode="json")
Expand Down Expand Up @@ -601,7 +619,7 @@ async def list_apps(self, context: Optional[dict] = None):
)
return [app["manifest"] for app in apps]
except KeyError:
raise KeyError(f"Applications collection not found: {ws}")
return []
except Exception as exp:
raise Exception(f"Failed to list apps: {exp}") from exp

Expand Down
2 changes: 1 addition & 1 deletion hypha/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -2412,7 +2412,7 @@ async def list_children(
raise ValueError("Context must include 'ws' (workspace).")
if parent_id:
parent_id = self._validate_artifact_id(parent_id, context)
user_info = UserInfo.model_validate(context["user"])
user_info = UserInfo.model_validate(context.get("user"))

session = await self._get_session(read_only=True)
stage = False
Expand Down
3 changes: 2 additions & 1 deletion hypha/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class StatusEnum(str, Enum):
class ServiceConfig(BaseModel):
"""Represent service config."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", use_enum_values=False)

visibility: VisibilityEnum = VisibilityEnum.protected
require_context: Union[Tuple[str], List[str], bool] = False
Expand Down Expand Up @@ -688,6 +688,7 @@ async def stop(self):
async def _subscribe_redis(self):
cpu_count = os.cpu_count() or 1
concurrent_tasks = cpu_count * 10
logger.info(f"Starting Redis event bus with {concurrent_tasks} concurrent task processing")
pubsub = self._redis.pubsub()
self._stop = False
semaphore = asyncio.Semaphore(concurrent_tasks) # Limit concurrent tasks
Expand Down
15 changes: 11 additions & 4 deletions hypha/core/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
TokenConfig,
UserPermission,
ServiceTypeInfo,
VisibilityEnum,
)
from hypha.vectors import VectorSearchEngine
from hypha.core.auth import generate_presigned_token, create_scope, valid_token
Expand Down Expand Up @@ -1159,7 +1160,8 @@ async def register_service(
return
# Check if the service already exists
service_exists = await self._redis.keys(f"services:*|*:{service.id}@*")
key = f"services:{service.config.visibility.value}|{service.type}:{service.id}@{service.app_id}"
visibility = service.config.visibility.value if isinstance(service.config.visibility, VisibilityEnum) else service.config.visibility
key = f"services:{visibility}|{service.type}:{service.id}@{service.app_id}"

if service_exists:
# remove all the existing services
Expand Down Expand Up @@ -1315,7 +1317,8 @@ async def unregister_service(
assert ":" in service.id, "Service id info must contain ':'"
service.app_id = service.app_id or "*"
service.type = service.type or "*"
key = f"services:{service.config.visibility.value}|{service.type}:{service.id}@{service.app_id}"
visibility = service.config.visibility.value if isinstance(service.config.visibility, VisibilityEnum) else service.config.visibility
key = f"services:{visibility}|{service.type}:{service.id}@{service.app_id}"

# Check if the service exists before removal
service_keys = await self._redis.keys(key)
Expand Down Expand Up @@ -1758,6 +1761,11 @@ async def unload(self, context=None):
logger.warning(f"Workspace {ws} has already been unloaded.")
return
winfo = await self.load_workspace_info(ws)
# Mark the workspace as not ready
winfo.status = None
await self._redis.hset(
"workspaces", winfo.id, winfo.model_dump_json()
)
# list all the clients in the workspace and send a meesage to delete them
client_keys = await self._list_client_keys(winfo.id)
if len(client_keys) > 0:
Expand All @@ -1771,8 +1779,7 @@ async def unload(self, context=None):
)
await self._event_bus.emit(f"unload:{ws}", "Unloading workspace: " + ws)

# Mark the workspace as not ready
winfo.status = None


if not winfo.persistent:
# delete all the items in redis starting with `workspaces_name:`
Expand Down
2 changes: 1 addition & 1 deletion hypha/templates/hypha-core-app/hypha-app-webpython.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ loadPyodide().then(async (pyodide) => {
pyodide.setStderr({ batched: (msg) => console.error(msg) });
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('hypha-rpc==0.20.42');
await micropip.install('hypha-rpc==0.20.44');
const isWindow = typeof window !== "undefined";

setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion hypha/templates/ws/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
<body class="bg-black text-white font-poppins">
<div id="app"></div>
<script type="module">
import { HyphaCore } from "https://cdn.jsdelivr.net/npm/[email protected].42/dist/hypha-core.mjs";
import { HyphaCore } from "https://cdn.jsdelivr.net/npm/[email protected].44/dist/hypha-core.mjs";

const defaultService = {
getServerConfig(context){
Expand Down
2 changes: 1 addition & 1 deletion hypha/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ async def handle_disconnection(
finally:
await self.disconnect(
websocket,
"Client disconnected",
f"Client {workspace}/{client_id} disconnected",
code,
)

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ aiofiles==23.2.1
websockets==13.1
base58==2.1.1
fastapi==0.106.0
hypha-rpc==0.20.42
hypha-rpc==0.20.44
jinja2==3.1.4
lxml==4.9.3
msgpack==1.0.8
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
REQUIREMENTS = [
"aiofiles",
"fastapi>=0.70.0,<=0.106.0",
"hypha-rpc>=0.20.42",
"hypha-rpc>=0.20.44",
"msgpack>=1.0.2",
"numpy",
"pydantic[email]>=2.6.1",
Expand Down
3 changes: 2 additions & 1 deletion tests/test_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ async def test_load_dump_vector_collections(
artifact_id=vector_collection.id,
vectors=vectors,
)
await asyncio.sleep(10)
await api.disconnect()
await asyncio.sleep(6)
async with connect_to_server(
{
"name": "test deploy client",
Expand Down

0 comments on commit f6670f0

Please sign in to comment.