Skip to content

Commit

Permalink
Add hypha-cli
Browse files Browse the repository at this point in the history
  • Loading branch information
oeway committed Jan 31, 2025
1 parent 7f26c1f commit 3ce93c7
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 31 deletions.
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.47.post5"
"version": "0.20.47.post6"
}
26 changes: 22 additions & 4 deletions hypha/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,38 @@
import asyncio
from hypha.server import get_argparser, create_application
from hypha.interactive import start_interactive_shell
import uvicorn
import os
import sys


def run_interactive_cli():
"""Run the interactive CLI."""

# Create the app instance
arg_parser = get_argparser(add_help=True)
opt = arg_parser.parse_args(sys.argv[1:])

# Force interactive server mode
if not opt.interactive_server:
opt.interactive = True

app = create_application(opt)

# Start the interactive shell
asyncio.run(start_interactive_shell(app, opt))


if __name__ == "__main__":
# Create the app instance
arg_parser = get_argparser()
opt = arg_parser.parse_args()
app = create_application(opt)

if opt.interactive:
if opt.interactive or opt.interactive_server:
asyncio.run(start_interactive_shell(app, opt))
else:
import uvicorn

uvicorn.run(app, host=opt.host, port=int(opt.port))

else:
# Create the app instance when imported by uvicorn
arg_parser = get_argparser(add_help=False)
Expand Down
46 changes: 36 additions & 10 deletions hypha/core/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ class WorkspaceManagerWrapper:
def __init__(self, store):
self._store = store
self._interface = None
self._interface_cm = None

def __call__(self, user_info=None, workspace=None):
"""Support both wm() and wm(context) syntax for context manager."""
Expand All @@ -989,21 +990,46 @@ def __call__(self, user_info=None, workspace=None):
workspace or "public",
)

async def _ensure_interface(self):
"""Ensure we have an active interface, creating one if needed."""
if self._interface is None:
self._interface_cm = self._store.get_workspace_interface(
self._store._root_user,
"public",
client_id=None,
silent=True,
)
self._interface = await self._interface_cm.__aenter__()
return self._interface

async def _cleanup_interface(self):
"""Clean up the interface if it exists."""
if self._interface_cm is not None:
await self._interface_cm.__aexit__(None, None, None)
self._interface = None
self._interface_cm = None

def __getattr__(self, name):
"""Support direct method access by creating a temporary interface."""

async def wrapper(*args, **kwargs):
if self._interface is None:
self._interface = self._store.get_workspace_interface(
self._store._root_user,
"public",
client_id=None,
silent=True,
)
interface = await self._interface.__aenter__()
method = getattr(interface, name)
return await method(*args, **kwargs)
try:
interface = await self._ensure_interface()
method = getattr(interface, name)
result = await method(*args, **kwargs)
return result
except Exception as e:
await self._cleanup_interface()
raise e

return wrapper

async def __aenter__(self):
"""Support using the wrapper itself as a context manager."""
return await self._ensure_interface()

async def __aexit__(self, exc_type, exc, tb):
"""Clean up when used as a context manager."""
await self._cleanup_interface()

return WorkspaceManagerWrapper(self)
48 changes: 34 additions & 14 deletions hypha/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from fastapi import FastAPI
import uvicorn

from hypha.server import get_argparser
from hypha.server import get_argparser, create_application


def configure_ptpython(repl: PythonRepl) -> None:
Expand All @@ -35,7 +35,7 @@ def configure_ptpython(repl: PythonRepl) -> None:
You can run async code directly using top-level await.
Type 'exit' to quit.
Type 'exit()' or press Ctrl+D to quit.
"""
print(welcome_message)

Expand All @@ -46,21 +46,37 @@ async def start_interactive_shell(app: FastAPI, args: Any) -> None:
# Get the store from the app state
store = app.state.store

# Start the server in the background
config = uvicorn.Config(app, host=args.host, port=int(args.port))
server = uvicorn.Server(config)
# Run the server in a separate task
server_task = asyncio.create_task(server.serve())
await store.get_event_bus().wait_for_local("startup")
print(f"\nServer started at http://{args.host}:{args.port}")
server = None
server_task = None

print("Initializing interactive shell...\n")

async def start_server():
nonlocal server, server_task
config = uvicorn.Config(app, host=args.host, port=int(args.port))
server = uvicorn.Server(config)
# Run the server in a separate task
server_task = asyncio.create_task(server.serve())
await store.get_event_bus().wait_for_local("startup")
print(f"\nServer started at http://{args.host}:{args.port}")

if args.interactive_server:
# Start the server in the background
await start_server()

# Prepare the local namespace
local_ns = {
"app": app,
"store": store,
"server": server,
}
if server:
local_ns["server"] = server
else:
args.startup_functions = args.startup_functions or []
await store.init(
reset_redis=args.reset_redis, startup_functions=args.startup_functions
)
local_ns["start_server"] = start_server

# Start the interactive shell with stdout patching for better async support
with patch_stdout():
Expand All @@ -73,9 +89,10 @@ async def start_interactive_shell(app: FastAPI, args: Any) -> None:
)

# Cleanup when shell exits
print("\nShutting down server...")
server.should_exit = True
await server_task
if server:
print("\nShutting down server...")
server.should_exit = True
await server_task

if store.is_ready():
print("Cleaning up store...")
Expand All @@ -88,8 +105,11 @@ def main() -> None:
arg_parser = get_argparser()
args = arg_parser.parse_args()

# Create the FastAPI app instance
app = create_application(args)

try:
asyncio.run(start_interactive_shell(args))
asyncio.run(start_interactive_shell(app, args))
except (KeyboardInterrupt, EOFError):
print("\nExiting Hypha Interactive Shell...")
except Exception as e:
Expand Down
16 changes: 15 additions & 1 deletion hypha/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,24 @@ def get_argparser(add_help=True):
return parser


def add_interactive_arguments(parser):
"""Add interactive-specific arguments to the parser."""
parser.add_argument(
"--interactive-server",
action="store_true",
help="start an interactive server with the hypha store",
)
return parser


if __name__ == "__main__":
import uvicorn

arg_parser = get_argparser()

# Only add interactive server argument when running as main script
add_interactive_arguments(arg_parser)

opt = arg_parser.parse_args()

# Apply database migrations
Expand All @@ -527,7 +541,7 @@ def get_argparser(add_help=True):
command.upgrade(alembic_cfg, "head")

app = create_application(opt)
if opt.interactive:
if opt.interactive or opt.interactive_server:
from hypha.interactive import start_interactive_shell

asyncio.run(start_interactive_shell(app, opt))
Expand Down
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,10 @@
],
},
zip_safe=False,
entry_points={"console_scripts": ["hypha = hypha.__main__:main"]},
entry_points={
"console_scripts": [
"hypha = hypha.__main__:main",
"hypha-cli = hypha.__main__:run_interactive_cli",
]
},
)

0 comments on commit 3ce93c7

Please sign in to comment.