Skip to content

Commit

Permalink
Refactor into library approach (#7)
Browse files Browse the repository at this point in the history
This PR makes a few sweeping changes to the actor, cli, and overall
structure of the project.

- CLI commands skeleton 
- ``arcade run``, ``arcade show``, and ``arcade new``
- Working package mangement solution (``arcade_`` packages)
- Actor approach for using frameworks other than FastAPI
- Client for calling Engine within ``arcade/core``
- beginning of the config interface.

---------

Co-authored-by: Nate Barbettini <[email protected]>
  • Loading branch information
Spartee and nbarbettini authored Jul 23, 2024
1 parent 4b469ea commit 8964111
Show file tree
Hide file tree
Showing 71 changed files with 2,696 additions and 1,708 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.DS_Store

*.lock

# example data
examples/data
scratch
Expand Down
3 changes: 3 additions & 0 deletions arcade/arcade/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from arcade.core.version import VERSION

__version__ = VERSION
165 changes: 165 additions & 0 deletions arcade/arcade/actor/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import time
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Callable

from arcade.actor.schema import (
InvokeToolRequest,
InvokeToolResponse,
ToolOutput,
ToolOutputError,
)
from arcade.core.catalog import ToolCatalog, Toolkit
from arcade.core.executor import ToolExecutor
from arcade.core.tool import ToolDefinition


class ActorComponent(ABC):
@abstractmethod
def register(self, router: Any) -> None:
"""
Register the component with the given router.
"""
pass

@abstractmethod
async def __call__(self, request: Any) -> Any:
"""
Handle the request.
"""
pass


class BaseActor:
base_path = "/actor" # By default, prefix all our routes with /actor

def __init__(self) -> None:
"""
Initialize the BaseActor with an empty ToolCatalog.
"""
self.catalog = ToolCatalog()

def get_catalog(self) -> list[ToolDefinition]:
"""
Get the catalog as a list of ToolDefinitions.
"""
return [tool.definition for tool in self.catalog]

def register_tool(self, tool: Callable) -> None:
"""
Register a tool to the catalog.
"""
self.catalog.add_tool(tool)

def register_toolkit(self, toolkit: Toolkit) -> None:
"""
Register a toolkit to the catalog.
"""
self.catalog.add_toolkit(toolkit)

async def invoke_tool(self, tool_request: InvokeToolRequest) -> InvokeToolResponse:
"""
Invoke a tool using the ToolExecutor.
"""
tool_name = tool_request.tool.name
tool = self.catalog.get_tool(tool_name)
if not tool:
raise ValueError(f"Tool {tool_name} not found in catalog.")

materialized_tool = self.catalog[tool_name]

start_time = time.time()

response = await ToolExecutor.run(
func=materialized_tool.tool,
input_model=materialized_tool.input_model,
output_model=materialized_tool.output_model,
**tool_request.inputs or {},
)
if response.code == 200:
# TODO remove ignore
output = ToolOutput(value=response.data.result) # type: ignore[union-attr]
else:
output = ToolOutput(error=ToolOutputError(message=response.msg))

end_time = time.time() # End time in seconds
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds

return InvokeToolResponse(
invocation_id=tool_request.invocation_id,
duration=duration_ms,
finished_at=datetime.now().isoformat(),
success=response.code == 200,
output=output,
)

def health_check(self) -> dict[str, Any]:
"""
Provide a health check that serves as a heartbeat of actor health.
"""
return {"status": "ok", "tool_count": len(self.catalog.tools.keys())}

def register_routes(self, router: Any) -> None:
"""
Register the necessary routes to the application.
"""
catalog_component = CatalogComponent(self)
invoke_tool_component = InvokeToolComponent(self)
health_check_component = HealthCheckComponent(self)

catalog_component.register(router)
invoke_tool_component.register(router)
health_check_component.register(router)


class CatalogComponent(ActorComponent):
def __init__(self, actor: BaseActor) -> None:
self.actor = actor

def register(self, router: Any) -> None:
"""
Register the catalog route with the router.
"""
router.add_route(f"{self.actor.base_path}/tools", self, methods=["GET"])

async def __call__(self, request: Any) -> list[ToolDefinition]:
"""
Handle the request to get the catalog.
"""
return self.actor.get_catalog()


class InvokeToolComponent(ActorComponent):
def __init__(self, actor: BaseActor) -> None:
self.actor = actor

def register(self, router: Any) -> None:
"""
Register the invoke tool route with the router.
"""
router.add_route(f"{self.actor.base_path}/tools/invoke", self, methods=["POST"])

async def __call__(self, request: Any) -> InvokeToolResponse:
"""
Handle the request to invoke a tool.
"""
invoke_tool_request_data = await request.json()
invoke_tool_request = InvokeToolRequest.model_validate(invoke_tool_request_data)
return await self.actor.invoke_tool(invoke_tool_request)


class HealthCheckComponent(ActorComponent):
def __init__(self, actor: BaseActor) -> None:
self.actor = actor

def register(self, router: Any) -> None:
"""
Register the health check route with the router.
"""
router.add_route(f"{self.actor.base_path}/health", self, methods=["GET"])

async def __call__(self, request: Any) -> dict[str, Any]:
"""
Handle the request for a health check.
"""
return self.actor.health_check()
58 changes: 0 additions & 58 deletions arcade/arcade/actor/common/log.py

This file was deleted.

102 changes: 0 additions & 102 deletions arcade/arcade/actor/common/response.py

This file was deleted.

13 changes: 0 additions & 13 deletions arcade/arcade/actor/common/serializers.py

This file was deleted.

Loading

0 comments on commit 8964111

Please sign in to comment.