Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"es2020": true,
"node": true
},
"root": true,
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-module-boundary-types": "off",
Expand Down
82 changes: 55 additions & 27 deletions lc_conductor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,59 @@
## SPDX-License-Identifier: Apache-2.0
###############################################################################

from lc_conductor.backend_manager import ActionManager, TaskManager
from lc_conductor.callback_logger import CallbackLogger
from lc_conductor.tool_registration import (
ToolList,
list_server_urls,
list_server_tools,
validate_and_register_mcp_server,
check_registered_servers,
delete_registered_server,
get_registered_servers,
try_get_public_hostname,
)
from lc_conductor.backend_helper_function import RunSettings
from importlib import import_module

__all__ = [
"ActionManager",
"TaskManager",
"CallbackLogger",
"ToolList",
"list_server_urls",
"list_server_tools",
"validate_and_register_mcp_server",
"check_registered_servers",
"delete_registered_server",
"get_registered_servers",
"try_get_public_hostname",
"RunSettings",
]

_EXPORTS = {
"ActionManager": ("lc_conductor.backend_manager", "ActionManager"),
"TaskManager": ("lc_conductor.backend_manager", "TaskManager"),
"CallbackLogger": ("lc_conductor.callback_logger", "CallbackLogger"),
"ToolList": ("lc_conductor.tool_registration", "ToolList"),
"list_server_urls": ("lc_conductor.tool_registration", "list_server_urls"),
"list_server_tools": ("lc_conductor.tool_registration", "list_server_tools"),
"validate_and_register_mcp_server": (
"lc_conductor.tool_registration",
"validate_and_register_mcp_server",
),
"check_registered_servers": (
"lc_conductor.tool_registration",
"check_registered_servers",
),
"delete_registered_server": (
"lc_conductor.tool_registration",
"delete_registered_server",
),
"get_registered_servers": (
"lc_conductor.tool_registration",
"get_registered_servers",
),
"try_get_public_hostname": (
"lc_conductor.tool_registration",
"try_get_public_hostname",
),
"RunSettings": ("lc_conductor.backend_helper_function", "RunSettings"),
"parse_curl_command": ("lc_conductor.curl_parser", "parse_curl_command"),
"execute_curl_command": ("lc_conductor.curl_executor", "execute_curl_command"),
"execute_http_request": ("lc_conductor.curl_executor", "execute_http_request"),
"build_hpc_allocation_request": (
"lc_conductor.hpc_allocation",
"build_hpc_allocation_request",
),
"execute_hpc_allocation_from_env": (
"lc_conductor.hpc_allocation",
"execute_hpc_allocation_from_env",
),
}

__all__ = list(_EXPORTS)


def __getattr__(name: str):
if name not in _EXPORTS:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

module_name, attr_name = _EXPORTS[name]
module = import_module(module_name)
value = getattr(module, attr_name)
globals()[name] = value
return value
178 changes: 170 additions & 8 deletions lc_conductor/backend_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,35 @@
from fastapi import WebSocket
import asyncio
import os
import re
import traceback
from loguru import logger
from lc_conductor.callback_logger import CallbackLogger
from concurrent.futures import ProcessPoolExecutor
from charge.experiments.experiment import Experiment
from charge.clients.agent_factory import AgentFactory
from charge.clients.autogen import AutoGenBackend

from functools import partial
from lc_conductor.tool_registration import (
ToolList,
list_server_urls,
list_server_tools,
)
from lc_conductor.backend_helper_function import RunSettings
from lc_conductor.hpc_allocation import execute_hpc_allocation_from_env

try:
from charge.experiments.experiment import Experiment
from charge.clients.agent_factory import AgentFactory
from charge.clients.autogen import AutoGenBackend
except ImportError:
Experiment = Any # type: ignore[assignment]
AgentFactory = None
AutoGenBackend = None

try:
from lc_conductor.tool_registration import (
ToolList,
list_server_urls,
list_server_tools,
)
except ImportError:
ToolList = None # type: ignore[assignment]
list_server_urls = None
list_server_tools = None

# Mapping from backend name to human-readable labels. Mirrored from the frontend
BACKEND_LABELS = {
Expand Down Expand Up @@ -320,3 +334,151 @@ async def handle_get_username(self, _: dict) -> None:
"username": self.username,
}
)

async def handle_allocate_hpc_resources(self, data: dict) -> None:
request_id = data.get("requestId")
allocation = data.get("allocation") or {}

# timestamp: keep consistent with curl executor tests ("...Z")
from datetime import datetime

def _result_payload(
*,
executed_request: str | None,
result: dict,
allocation_echo: dict,
) -> dict:
payload = {
"type": "allocate-hpc-resources-result",
"requestId": request_id,
"allocation": allocation_echo,
"result": result,
}
if executed_request:
payload["executedRequest"] = executed_request
return payload

if not isinstance(request_id, str) or not request_id.strip():
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": "Missing or invalid requestId",
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo=allocation if isinstance(allocation, dict) else {},
)
)
return

if not isinstance(allocation, dict):
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": "Missing or invalid allocation object",
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo={},
)
)
return

system = allocation.get("system")
nodes = allocation.get("nodes")
wall_time = allocation.get("time")
bank = allocation.get("bank")

if not isinstance(system, str) or not system.strip():
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": "Missing or invalid system",
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo=allocation,
)
)
return

try:
# Accept int or numeric string.
if isinstance(nodes, str):
if not re.fullmatch(r"\d+", nodes.strip()):
raise ValueError("nodes must be a positive integer")
nodes_int = int(nodes.strip())
elif isinstance(nodes, int):
nodes_int = nodes
else:
raise ValueError("nodes must be a positive integer")

if nodes_int < 1:
raise ValueError("nodes must be >= 1")
except ValueError as e:
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": str(e),
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo=allocation,
)
)
return

if not isinstance(wall_time, str) or not wall_time.strip():
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": "Missing or invalid time",
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo=allocation,
)
)
return

if not isinstance(bank, str) or not bank.strip():
await self.websocket.send_json(
_result_payload(
executed_request=None,
result={
"success": False,
"error": "Missing or invalid bank",
"timestamp": datetime.utcnow().isoformat() + "Z",
},
allocation_echo=allocation,
)
)
return

allocation_echo = {
"system": system.strip(),
"nodes": nodes_int,
"time": wall_time.strip(),
"bank": bank.strip(),
}

executed_request, result = await execute_hpc_allocation_from_env(
system=allocation_echo["system"],
nodes=allocation_echo["nodes"],
time=allocation_echo["time"],
bank=allocation_echo["bank"],
client_info=f"ws:{self.username}",
)

await self.websocket.send_json(
_result_payload(
executed_request=executed_request,
result=result,
allocation_echo=allocation_echo,
)
)
Loading