Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support browser runner and enhance vector store #723

Merged
merged 44 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7e2027f
Use redis as vector search
oeway Nov 27, 2024
fba1605
Fix publish_to
oeway Nov 27, 2024
eaf312d
Fix syntax
oeway Nov 27, 2024
3c438db
add runner; bump hypha-rpc
oeway Nov 29, 2024
287e4ec
Merge branch 'main' into support-browser-runner
oeway Nov 29, 2024
52feb2a
Enhance vector store, support persistence to s3
oeway Dec 3, 2024
c9efcef
bump version
oeway Dec 3, 2024
1e87684
rename service
oeway Dec 3, 2024
c5c3c8a
small fix
oeway Dec 3, 2024
c091b2c
Fix score return
oeway Dec 3, 2024
e03f36e
Enhance vector store, support persistence to s3
oeway Dec 3, 2024
d128fc6
bump version
oeway Dec 3, 2024
5840a29
rename service
oeway Dec 3, 2024
779f2e7
small fix
oeway Dec 3, 2024
139a0c7
Fix score return
oeway Dec 3, 2024
e4026e9
Merge branch 'support-browser-runner' of github.com:bioimage-io/hypha…
oeway Dec 3, 2024
2c4a648
Fix unload
oeway Dec 3, 2024
50a4a73
Update workspace.py
oeway Dec 3, 2024
cc3defa
Update workspace.py
oeway Dec 3, 2024
80fd1b1
Merge branch 'support-browser-runner' of github.com:amun-ai/hypha int…
oeway Dec 3, 2024
256f3ad
refine clean up logic
oeway Dec 3, 2024
7b93e43
Print error
oeway Dec 4, 2024
2bacdf9
Improve task scheduling
oeway Dec 4, 2024
06cc9d7
add activity tracker
oeway Dec 4, 2024
e0a8b7f
Fix teardown
oeway Dec 4, 2024
f3d3b7e
Improve lifetime management
oeway Dec 6, 2024
1b7319b
Fix applications and daemon
oeway Dec 6, 2024
38d7a57
Fix time
oeway Dec 6, 2024
9a99274
Fix activity monitor
oeway Dec 6, 2024
4a9950b
Fix event bus; add timeout
oeway Dec 6, 2024
68ff4b5
dump vector store
oeway Dec 6, 2024
914e120
Fix vector dump
oeway Dec 6, 2024
cd3a5b4
Fix errors
oeway Dec 6, 2024
4ff56a1
Fix tests
oeway Dec 6, 2024
3b8f732
wait long enough
oeway Dec 6, 2024
867be63
Fix artifact unload and load
oeway Dec 6, 2024
d145729
Fix workspaces unload
oeway Dec 6, 2024
408d6e2
Disable schechdule check
oeway Dec 6, 2024
8124cb7
Add update
oeway Dec 6, 2024
491017d
Add update
oeway Dec 6, 2024
f717b59
Add delay
oeway Dec 6, 2024
c17fb0e
Fix test
oeway Dec 6, 2024
707f5c0
Long delay
oeway Dec 6, 2024
0ba9d0f
Default to 60s
oeway Dec 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### 0.20.41

- Add `time_limit` for server apps to limit the running time
- Add `stop_after_inactive` option for server apps to stop the server app after a period of inactivity.
- Support launching server apps worker in a separate service

### 0.20.40
Expand Down
2 changes: 1 addition & 1 deletion 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].41/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/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
6 changes: 3 additions & 3 deletions docs/hypha-quick-tour.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"close_after_inactive: "markdown",
"metadata": {},
"source": [
"# Hypha Demos\n",
"\n",
"## Install client library"
]
},
{
{close_after_inactive
"cell_type": "code",
"execution_count": null,
"metadata": {},
Expand All @@ -24,7 +24,7 @@
" # For native python with pip\n",
" import subprocess\n",
" subprocess.call(['pip', 'install', 'hypha-rpc'])"
]
]close_after_inactive
},
{
"attachments": {},
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.41` is compatible with Hypha server version `0.20.41`.
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`.

#### 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].41/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/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.41` is compatible with Hypha server version `0.20.41`.
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`.

#### 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].41/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/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].41/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/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].41/dist/hypha-rpc-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected].42/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 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.41"
tag: "0.20.42"

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.41
version: 0.20.42

# 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.41"
tag: "0.20.42"

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.41"
"version": "0.20.42"
}
146 changes: 117 additions & 29 deletions hypha/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from hypha import main_version
from jinja2 import Environment, PackageLoader, select_autoescape
from typing import Any, Dict, List, Optional, Union
from hypha.core import UserInfo, UserPermission, ServiceInfo, ApplicationArtifact
from hypha.runner.browser import BrowserAppRunner
from hypha.core import UserInfo, UserPermission, ServiceInfo, ApplicationManifest
from hypha.utils import (
random_id,
PLUGIN_CONFIG_FIELDS,
Expand All @@ -20,6 +19,7 @@
import base58
import random
from hypha.plugin_parser import convert_config_to_artifact, parse_imjoy_plugin
from hypha.core import WorkspaceInfo

logging.basicConfig(stream=sys.stdout)
logger = logging.getLogger("apps")
Expand Down Expand Up @@ -54,27 +54,38 @@ def __init__(
loader=PackageLoader("hypha"), autoescape=select_autoescape()
)
self.templates_dir = Path(__file__).parent / "templates"
self._runners = None

def shutdown(_) -> None:
asyncio.ensure_future(self.shutdown())

self.event_bus.on_local("shutdown", shutdown)

async def client_disconnected(info: dict) -> None:
"""Handle client disconnected event."""
# {"id": client_id, "workspace": ws}
client_id = info["id"]
full_client_id = info["workspace"] + "/" + client_id
if full_client_id in self._sessions:
app_info = self._sessions.pop(full_client_id, None)
try:
await app_info["_runner"].stop(full_client_id)
except Exception as exp:
logger.warning(f"Failed to stop browser tab: {exp}")

self.event_bus.on_local("client_disconnected", client_disconnected)
store.set_server_app_controller(self)

async def get_runners(self):
# start the browser runner
server = await self.store.get_public_api()
svcs = await server.list_services("public/server-app-worker")
if not svcs:
return []
runners = [await server.get_service(svc["id"]) for svc in svcs]
if runners:
return runners
elif self._runners:
return self._runners
self._runners = [
BrowserAppRunner(in_docker=self.in_docker),
BrowserAppRunner(in_docker=self.in_docker),
]
return self._runners
else:
[]

async def setup_applications_collection(self, overwrite=True, context=None):
"""Set up the workspace."""
Expand Down Expand Up @@ -205,7 +216,7 @@ async def install(
"public_url": public_url,
}
)
ApplicationArtifact.model_validate(artifact_obj)
ApplicationManifest.model_validate(artifact_obj)

try:
artifact = await self.artifact_manager.read("applications", context=context)
Expand Down Expand Up @@ -352,7 +363,7 @@ async def start(
timeout: float = 60,
version: str = None,
wait_for_service: Union[str, bool] = None,
time_limit: Optional[int] = 600,
stop_after_inactive: Optional[int] = None,
context: Optional[dict] = None,
):
"""Start the app and keep it alive."""
Expand All @@ -379,10 +390,24 @@ async def start(
artifact_info = await self.artifact_manager.read(
f"applications:{app_id}", version=version, context=context
)
artifact = artifact_info.get("manifest", {})
artifact = ApplicationArtifact.model_validate(artifact)

entry_point = artifact.entry_point
manifest = artifact_info.get("manifest", {})
manifest = ApplicationManifest.model_validate(manifest)
if manifest.singleton:
# check if the app is already running
for session_info in self._sessions.values():
if session_info["app_id"] == app_id:
raise RuntimeError(
f"App {app_id} is a singleton app and already running (id: {session_info['id']})"
)
if manifest.daemon and stop_after_inactive and stop_after_inactive > 0:
raise ValueError("Daemon apps should not have stop_after_inactive set.")
if stop_after_inactive is None:
stop_after_inactive = (
600
if manifest.stop_after_inactive is None
else manifest.stop_after_inactive
)
entry_point = manifest.entry_point
assert entry_point, f"Entry point not found for app {app_id}."
server_url = self.local_base_url
local_url = (
Expand All @@ -404,21 +429,46 @@ async def start(
+ (f"&version={version}" if version else "")
+ (f"&use_proxy=true")
)

runner = random.choice(await self.get_runners())
runners = await self.get_runners()
if not runners:
raise Exception("No server app worker found")
runner = random.choice(runners)

full_client_id = workspace + "/" + client_id
await runner.start(
url=local_url, session_id=full_client_id, time_limit=time_limit
)
self._sessions[full_client_id] = {
metadata = {
"id": full_client_id,
"app_id": app_id,
"workspace": workspace,
"local_url": local_url,
"public_url": public_url,
"_runner": runner,
}

await runner.start(
url=local_url,
session_id=full_client_id,
metadata=metadata,
)
self._sessions[full_client_id] = metadata

# test activity tracker
tracker = self.store.get_activity_tracker()
if not manifest.daemon and stop_after_inactive and stop_after_inactive > 0:

async def _stop_after_inactive():
if full_client_id in self._sessions:
await runner.stop(full_client_id)
logger.info(
f"App {full_client_id} stopped because of inactive for {stop_after_inactive}s."
)

tracker.register(
full_client_id,
inactive_period=stop_after_inactive,
on_inactive=_stop_after_inactive,
entity_type="client",
)

# collecting services registered during the startup of the script
collected_services: List[ServiceInfo] = []
app_info = {
Expand Down Expand Up @@ -452,14 +502,14 @@ def service_added(info: dict):
)

# save the services
artifact.services = collected_services
artifact = ApplicationArtifact.model_validate(
artifact.model_dump(mode="json")
manifest.services = collected_services
manifest = ApplicationManifest.model_validate(
manifest.model_dump(mode="json")
)
await self.artifact_manager.edit(
f"applications:{app_id}",
version=version,
manifest=artifact.model_dump(mode="json"),
manifest=manifest.model_dump(mode="json"),
context=context,
)

Expand Down Expand Up @@ -495,7 +545,9 @@ async def stop(
f"User {user_info.id} does not have permission"
f" to stop app {session_id} in workspace {workspace}."
)
await self._stop(session_id, raise_exception=raise_exception)

async def _stop(self, session_id: str, raise_exception=True):
if session_id in self._sessions:
app_info = self._sessions.pop(session_id, None)
try:
Expand Down Expand Up @@ -543,20 +595,56 @@ async def list_running(self, context: Optional[dict] = None) -> List[str]:
async def list_apps(self, context: Optional[dict] = None):
"""List applications in the workspace."""
try:
ws = context["ws"]
apps = await self.artifact_manager.list_children(
"applications", context=context
f"{ws}/applications", context=context
)
return [app["manifest"] for app in apps]
except KeyError:
return []
raise KeyError(f"Applications collection not found: {ws}")
except Exception as exp:
raise Exception(f"Failed to list apps: {exp}") from exp

async def shutdown(self) -> None:
"""Shutdown the app controller."""
logger.info("Closing the server app controller...")
for app in self._sessions.values():
await self.stop(app["id"])
await self.stop(app["id"], raise_exception=False)

async def prepare_workspace(self, workspace_info: WorkspaceInfo):
"""Prepare the workspace."""
context = {
"ws": workspace_info.id,
"user": self.store.get_root_user().model_dump(),
}
apps = await self.list_apps(context=context)
# start daemon apps
for app in apps:
if app.get("daemon"):
try:
await self.start(app["id"], context=context)
except Exception as exp:
logger.error(
f"Failed to start daemon app: {app['id']}, error: {exp}"
)

async def close_workspace(self, workspace_info: WorkspaceInfo):
"""Archive the workspace."""
# Stop all running apps
for app in list(self._sessions.values()):
if app["workspace"] == workspace_info.id:
await self._stop(app["id"], raise_exception=False)
# Send to all runners
runners = await self.get_runners()
if not runners:
return
for runner in runners:
try:
await runner.close_workspace(workspace_info.id)
except Exception as exp:
logger.warning(
f"Worker failed to close workspace: {workspace_info.id}, error: {exp}"
)

def get_service_api(self) -> Dict[str, Any]:
"""Get a list of service API endpoints."""
Expand Down
Loading
Loading