mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
Add port mappings support (#5577)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: tofarr <tofarr@gmail.com> Co-authored-by: Robert Brennan <accounts@rbren.io> Co-authored-by: Robert Brennan <contact@rbren.io>
This commit is contained in:
19
frontend/src/components/features/served-host/path-form.tsx
Normal file
19
frontend/src/components/features/served-host/path-form.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
interface PathFormProps {
|
||||
ref: React.RefObject<HTMLFormElement | null>;
|
||||
onBlur: () => void;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export function PathForm({ ref, onBlur, defaultValue }: PathFormProps) {
|
||||
return (
|
||||
<form ref={ref} onSubmit={(e) => e.preventDefault()} className="flex-1">
|
||||
<input
|
||||
name="url"
|
||||
type="text"
|
||||
defaultValue={defaultValue}
|
||||
className="w-full bg-transparent"
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
12
frontend/src/components/layout/served-app-label.tsx
Normal file
12
frontend/src/components/layout/served-app-label.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useActiveHost } from "#/hooks/query/use-active-host";
|
||||
|
||||
export function ServedAppLabel() {
|
||||
const { activeHost } = useActiveHost();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">App</div>
|
||||
{activeHost && <div className="w-2 h-2 bg-green-500 rounded-full" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,10 +50,13 @@ async function prepareApp() {
|
||||
}
|
||||
}
|
||||
|
||||
const QUERY_KEYS_TO_IGNORE = ["authenticated", "hosts"];
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error, query) => {
|
||||
if (!query.queryKey.includes("authenticated")) toast.error(error.message);
|
||||
if (!QUERY_KEYS_TO_IGNORE.some((key) => query.queryKey.includes(key))) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
|
||||
51
frontend/src/hooks/query/use-active-host.ts
Normal file
51
frontend/src/hooks/query/use-active-host.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { RootState } from "#/store";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
|
||||
export const useActiveHost = () => {
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const [activeHost, setActiveHost] = React.useState<string | null>(null);
|
||||
|
||||
const { conversationId } = useConversation();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [conversationId, "hosts"],
|
||||
queryFn: async () => {
|
||||
const response = await openHands.get<{ hosts: string[] }>(
|
||||
`/api/conversations/${conversationId}/web-hosts`,
|
||||
);
|
||||
return { hosts: Object.keys(response.data.hosts) };
|
||||
},
|
||||
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
|
||||
initialData: { hosts: [] },
|
||||
});
|
||||
|
||||
const apps = useQueries({
|
||||
queries: data.hosts.map((host) => ({
|
||||
queryKey: [conversationId, "hosts", host],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
await axios.get(host);
|
||||
return host;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
})),
|
||||
});
|
||||
|
||||
const appsData = apps.map((app) => app.data);
|
||||
|
||||
React.useEffect(() => {
|
||||
const successfulApp = appsData.find((app) => app);
|
||||
setActiveHost(successfulApp || "");
|
||||
}, [appsData]);
|
||||
|
||||
return { activeHost };
|
||||
};
|
||||
@@ -12,6 +12,7 @@ export default [
|
||||
index("routes/_oh.app._index/route.tsx"),
|
||||
route("browser", "routes/_oh.app.browser.tsx"),
|
||||
route("jupyter", "routes/_oh.app.jupyter.tsx"),
|
||||
route("served", "routes/app.tsx"),
|
||||
]),
|
||||
]),
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FaServer } from "react-icons/fa";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
ConversationProvider,
|
||||
@@ -31,6 +32,7 @@ import Security from "#/components/shared/modals/security/security";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { CountBadge } from "#/components/layout/count-badge";
|
||||
import { ServedAppLabel } from "#/components/layout/served-app-label";
|
||||
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
@@ -126,6 +128,11 @@ function AppContent() {
|
||||
labels={[
|
||||
{ label: "Workspace", to: "", icon: <CodeIcon /> },
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: <ServedAppLabel />,
|
||||
to: "served",
|
||||
icon: <FaServer />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
95
frontend/src/routes/app.tsx
Normal file
95
frontend/src/routes/app.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { FaArrowRotateRight } from "react-icons/fa6";
|
||||
import { FaExternalLinkAlt, FaHome } from "react-icons/fa";
|
||||
import { useActiveHost } from "#/hooks/query/use-active-host";
|
||||
import { PathForm } from "#/components/features/served-host/path-form";
|
||||
|
||||
function ServedApp() {
|
||||
const { activeHost } = useActiveHost();
|
||||
const [refreshKey, setRefreshKey] = React.useState(0);
|
||||
const [currentActiveHost, setCurrentActiveHost] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [path, setPath] = React.useState<string>("hello");
|
||||
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
const handleOnBlur = () => {
|
||||
if (formRef.current) {
|
||||
const formData = new FormData(formRef.current);
|
||||
const urlInputValue = formData.get("url")?.toString();
|
||||
|
||||
if (urlInputValue) {
|
||||
const url = new URL(urlInputValue);
|
||||
|
||||
setCurrentActiveHost(url.origin);
|
||||
setPath(url.pathname);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetUrl = () => {
|
||||
setCurrentActiveHost(activeHost);
|
||||
setPath("");
|
||||
|
||||
if (formRef.current) {
|
||||
formRef.current.reset();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
resetUrl();
|
||||
}, [activeHost]);
|
||||
|
||||
const fullUrl = `${currentActiveHost}/${path}`;
|
||||
|
||||
if (!currentActiveHost) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full p-10">
|
||||
<span className="text-neutral-400 font-bold">
|
||||
If you tell OpenHands to start a web server, the app will appear here.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="w-full p-2 flex items-center gap-4 border-b border-neutral-600">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(fullUrl, "_blank")}
|
||||
className="text-sm"
|
||||
>
|
||||
<FaExternalLinkAlt className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRefreshKey((prev) => prev + 1)}
|
||||
className="text-sm"
|
||||
>
|
||||
<FaArrowRotateRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button type="button" onClick={() => resetUrl()} className="text-sm">
|
||||
<FaHome className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-full flex">
|
||||
<PathForm
|
||||
ref={formRef}
|
||||
onBlur={handleOnBlur}
|
||||
defaultValue={fullUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
key={refreshKey}
|
||||
title="Served App"
|
||||
src={fullUrl}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServedApp;
|
||||
@@ -3,9 +3,21 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
||||
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
|
||||
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
|
||||
{{ runtime_info }}
|
||||
</IMPORTANT>
|
||||
{% if repo_instructions %}
|
||||
{% if repo_instructions -%}
|
||||
<REPOSITORY_INSTRUCTIONS>
|
||||
{{ repo_instructions }}
|
||||
</REPOSITORY_INSTRUCTIONS>
|
||||
{% endif %}
|
||||
{% if runtime_info and runtime_info.available_hosts -%}
|
||||
<RUNTIME_INFORMATION>
|
||||
The user has access to the following hosts for accessing a web application,
|
||||
each of which has a corresponding port:
|
||||
{% for host, port in runtime_info.available_hosts.items() -%}
|
||||
* {{ host }} (port {{ port }})
|
||||
{% endfor %}
|
||||
When starting a web server, use the corresponding ports. You should also
|
||||
set any options to allow iframes and CORS requests.
|
||||
</RUNTIME_INFORMATION>
|
||||
{% endif %}
|
||||
|
||||
@@ -57,7 +57,6 @@ from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCode
|
||||
from openhands.runtime.utils.bash import BashSession
|
||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
|
||||
from openhands.runtime.utils.system import check_port_available
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
from openhands.utils.async_utils import call_sync_from_async, wait_all
|
||||
|
||||
@@ -435,8 +434,6 @@ if __name__ == '__main__':
|
||||
)
|
||||
# example: python client.py 8000 --working-dir /workspace --plugins JupyterRequirement
|
||||
args = parser.parse_args()
|
||||
os.environ['VSCODE_PORT'] = str(int(args.port) + 1)
|
||||
assert check_port_available(int(os.environ['VSCODE_PORT']))
|
||||
|
||||
plugins_to_load: list[Plugin] = []
|
||||
if args.plugins:
|
||||
|
||||
@@ -400,3 +400,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
@property
|
||||
def vscode_url(self) -> str | None:
|
||||
raise NotImplementedError('This method is not implemented in the base class.')
|
||||
|
||||
@property
|
||||
def web_hosts(self) -> dict[str, int]:
|
||||
return {}
|
||||
|
||||
@@ -28,6 +28,11 @@ from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
CONTAINER_NAME_PREFIX = 'openhands-runtime-'
|
||||
|
||||
EXECUTION_SERVER_PORT_RANGE = (30000, 39999)
|
||||
VSCODE_PORT_RANGE = (40000, 49999)
|
||||
APP_PORT_RANGE_1 = (50000, 54999)
|
||||
APP_PORT_RANGE_2 = (55000, 59999)
|
||||
|
||||
|
||||
def remove_all_runtime_containers():
|
||||
remove_all_containers(CONTAINER_NAME_PREFIX)
|
||||
@@ -65,13 +70,17 @@ class DockerRuntime(ActionExecutionClient):
|
||||
atexit.register(remove_all_runtime_containers)
|
||||
|
||||
self.config = config
|
||||
self._host_port = 30000 # initial dummy value
|
||||
self._container_port = 30001 # initial dummy value
|
||||
self._runtime_initialized: bool = False
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
self.status_callback = status_callback
|
||||
|
||||
self._host_port = -1
|
||||
self._container_port = -1
|
||||
self._vscode_port = -1
|
||||
self._app_ports: list[int] = []
|
||||
|
||||
self.docker_client: docker.DockerClient = self._init_docker_client()
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
|
||||
self.base_container_image = self.config.sandbox.base_container_image
|
||||
self.runtime_container_image = self.config.sandbox.runtime_container_image
|
||||
self.container_name = CONTAINER_NAME_PREFIX + sid
|
||||
@@ -182,22 +191,35 @@ class DockerRuntime(ActionExecutionClient):
|
||||
plugin_arg = (
|
||||
f'--plugins {" ".join([plugin.name for plugin in self.plugins])} '
|
||||
)
|
||||
self._host_port = self._find_available_port()
|
||||
self._container_port = (
|
||||
self._host_port
|
||||
) # in future this might differ from host port
|
||||
self._host_port = self._find_available_port(EXECUTION_SERVER_PORT_RANGE)
|
||||
self._container_port = self._host_port
|
||||
self._vscode_port = self._find_available_port(VSCODE_PORT_RANGE)
|
||||
self._app_ports = [
|
||||
self._find_available_port(APP_PORT_RANGE_1),
|
||||
self._find_available_port(APP_PORT_RANGE_2),
|
||||
]
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
|
||||
use_host_network = self.config.sandbox.use_host_network
|
||||
network_mode: str | None = 'host' if use_host_network else None
|
||||
|
||||
port_mapping: dict[str, list[dict[str, str]]] | None = (
|
||||
None
|
||||
if use_host_network
|
||||
else {f'{self._container_port}/tcp': [{'HostPort': str(self._host_port)}]}
|
||||
)
|
||||
use_host_network = self.config.sandbox.use_host_network
|
||||
|
||||
if use_host_network:
|
||||
# Initialize port mappings
|
||||
port_mapping: dict[str, list[dict[str, str]]] | None = None
|
||||
if not use_host_network:
|
||||
port_mapping = {
|
||||
f'{self._container_port}/tcp': [{'HostPort': str(self._host_port)}],
|
||||
}
|
||||
|
||||
if self.vscode_enabled:
|
||||
port_mapping[f'{self._vscode_port}/tcp'] = [
|
||||
{'HostPort': str(self._vscode_port)}
|
||||
]
|
||||
|
||||
for port in self._app_ports:
|
||||
port_mapping[f'{port}/tcp'] = [{'HostPort': str(port)}]
|
||||
else:
|
||||
self.log(
|
||||
'warn',
|
||||
'Using host network mode. If you are using MacOS, please make sure you have the latest version of Docker Desktop and enabled host network feature: https://docs.docker.com/network/drivers/host/#docker-desktop',
|
||||
@@ -207,17 +229,11 @@ class DockerRuntime(ActionExecutionClient):
|
||||
environment = {
|
||||
'port': str(self._container_port),
|
||||
'PYTHONUNBUFFERED': 1,
|
||||
'VSCODE_PORT': str(self._vscode_port),
|
||||
}
|
||||
if self.config.debug or DEBUG:
|
||||
environment['DEBUG'] = 'true'
|
||||
|
||||
if self.vscode_enabled:
|
||||
# vscode is on port +1 from container port
|
||||
if isinstance(port_mapping, dict):
|
||||
port_mapping[f'{self._container_port + 1}/tcp'] = [
|
||||
{'HostPort': str(self._host_port + 1)}
|
||||
]
|
||||
|
||||
self.log('debug', f'Workspace Base: {self.config.workspace_base}')
|
||||
if (
|
||||
self.config.workspace_mount_path is not None
|
||||
@@ -291,6 +307,8 @@ class DockerRuntime(ActionExecutionClient):
|
||||
'error',
|
||||
f'Error: Instance {self.container_name} FAILED to start container!\n',
|
||||
)
|
||||
self.log('error', str(e))
|
||||
raise e
|
||||
except Exception as e:
|
||||
self.log(
|
||||
'error',
|
||||
@@ -301,11 +319,20 @@ class DockerRuntime(ActionExecutionClient):
|
||||
raise e
|
||||
|
||||
def _attach_to_container(self):
|
||||
self._container_port = 0
|
||||
self.container = self.docker_client.containers.get(self.container_name)
|
||||
for port in self.container.attrs['NetworkSettings']['Ports']: # type: ignore
|
||||
self._container_port = int(port.split('/')[0])
|
||||
break
|
||||
port = int(port.split('/')[0])
|
||||
if (
|
||||
port >= EXECUTION_SERVER_PORT_RANGE[0]
|
||||
and port <= EXECUTION_SERVER_PORT_RANGE[1]
|
||||
):
|
||||
self._container_port = port
|
||||
if port >= VSCODE_PORT_RANGE[0] and port <= VSCODE_PORT_RANGE[1]:
|
||||
self._vscode_port = port
|
||||
elif port >= APP_PORT_RANGE_1[0] and port <= APP_PORT_RANGE_1[1]:
|
||||
self._app_ports.append(port)
|
||||
elif port >= APP_PORT_RANGE_2[0] and port <= APP_PORT_RANGE_2[1]:
|
||||
self._app_ports.append(port)
|
||||
self._host_port = self._container_port
|
||||
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
|
||||
self.log(
|
||||
@@ -363,10 +390,10 @@ class DockerRuntime(ActionExecutionClient):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _find_available_port(self, max_attempts=5):
|
||||
port = 39999
|
||||
def _find_available_port(self, port_range, max_attempts=5):
|
||||
port = port_range[1]
|
||||
for _ in range(max_attempts):
|
||||
port = find_available_tcp_port(30000, 39999)
|
||||
port = find_available_tcp_port(port_range[0], port_range[1])
|
||||
if not self._is_port_in_use_docker(port):
|
||||
return port
|
||||
# If no port is found after max_attempts, return the last tried port
|
||||
@@ -377,5 +404,15 @@ class DockerRuntime(ActionExecutionClient):
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
return None
|
||||
vscode_url = f'http://localhost:{self._host_port + 1}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
|
||||
|
||||
vscode_url = f'http://localhost:{self._vscode_port}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
|
||||
return vscode_url
|
||||
|
||||
@property
|
||||
def web_hosts(self):
|
||||
hosts: dict[str, int] = {}
|
||||
|
||||
for port in self._app_ports:
|
||||
hosts[f'http://localhost:{port}'] = port
|
||||
|
||||
return hosts
|
||||
|
||||
@@ -70,6 +70,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
)
|
||||
self.runtime_id: str | None = None
|
||||
self.runtime_url: str | None = None
|
||||
self.available_hosts: dict[str, int] = {}
|
||||
self._runtime_initialized: bool = False
|
||||
|
||||
def _get_action_execution_server_host(self):
|
||||
@@ -257,6 +258,8 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
start_response = response.json()
|
||||
self.runtime_id = start_response['runtime_id']
|
||||
self.runtime_url = start_response['url']
|
||||
self.available_hosts = start_response.get('work_hosts', {})
|
||||
|
||||
if 'session_api_key' in start_response:
|
||||
self.session.headers.update(
|
||||
{'X-Session-API-Key': start_response['session_api_key']}
|
||||
@@ -278,6 +281,10 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
)
|
||||
return vscode_url
|
||||
|
||||
@property
|
||||
def web_hosts(self) -> dict[str, int]:
|
||||
return self.available_hosts
|
||||
|
||||
def _wait_until_alive(self):
|
||||
retry_decorator = tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(
|
||||
|
||||
@@ -52,6 +52,45 @@ async def get_vscode_url(request: Request):
|
||||
)
|
||||
|
||||
|
||||
@app.get('/web-hosts')
|
||||
async def get_hosts(request: Request):
|
||||
"""Get the hosts used by the runtime.
|
||||
|
||||
This endpoint allows getting the hosts used by the runtime.
|
||||
|
||||
Args:
|
||||
request (Request): The incoming FastAPI request object.
|
||||
|
||||
Returns:
|
||||
JSONResponse: A JSON response indicating the success of the operation.
|
||||
"""
|
||||
try:
|
||||
if not hasattr(request.state, 'conversation'):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={'error': 'No conversation found in request state'},
|
||||
)
|
||||
|
||||
if not hasattr(request.state.conversation, 'runtime'):
|
||||
return JSONResponse(
|
||||
status_code=500, content={'error': 'No runtime found in conversation'}
|
||||
)
|
||||
|
||||
runtime: Runtime = request.state.conversation.runtime
|
||||
logger.debug(f'Runtime type: {type(runtime)}')
|
||||
logger.debug(f'Runtime hosts: {runtime.web_hosts}')
|
||||
return JSONResponse(status_code=200, content={'hosts': runtime.web_hosts})
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting runtime hosts: {e}', exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
'hosts': None,
|
||||
'error': f'Error getting runtime hosts: {e}',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get('/events/search')
|
||||
async def search_events(
|
||||
request: Request,
|
||||
|
||||
@@ -216,7 +216,9 @@ class AgentSession:
|
||||
await call_sync_from_async(
|
||||
self.runtime.clone_repo, github_token, selected_repository
|
||||
)
|
||||
|
||||
if agent.prompt_manager:
|
||||
agent.prompt_manager.set_runtime_info(self.runtime)
|
||||
microagents: list[BaseMicroAgent] = await call_sync_from_async(
|
||||
self.runtime.get_microagents_from_selected_repo, selected_repository
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from itertools import islice
|
||||
|
||||
from jinja2 import Template
|
||||
@@ -11,6 +12,12 @@ from openhands.microagent import (
|
||||
RepoMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeInfo:
|
||||
available_hosts: dict[str, int]
|
||||
|
||||
|
||||
class PromptManager:
|
||||
@@ -38,6 +45,7 @@ class PromptManager:
|
||||
|
||||
self.system_template: Template = self._load_template('system_prompt')
|
||||
self.user_template: Template = self._load_template('user_prompt')
|
||||
self.runtime_info = RuntimeInfo(available_hosts={})
|
||||
|
||||
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
|
||||
self.repo_microagents: dict[str, RepoMicroAgent] = {}
|
||||
@@ -72,6 +80,9 @@ class PromptManager:
|
||||
elif isinstance(microagent, RepoMicroAgent):
|
||||
self.repo_microagents[microagent.name] = microagent
|
||||
|
||||
def set_runtime_info(self, runtime: Runtime):
|
||||
self.runtime_info.available_hosts = runtime.web_hosts
|
||||
|
||||
def _load_template(self, template_name: str) -> Template:
|
||||
if self.prompt_dir is None:
|
||||
raise ValueError('Prompt directory is not set')
|
||||
@@ -91,7 +102,9 @@ class PromptManager:
|
||||
if repo_instructions:
|
||||
repo_instructions += '\n\n'
|
||||
repo_instructions += microagent.content
|
||||
return self.system_template.render(repo_instructions=repo_instructions).strip()
|
||||
return self.system_template.render(
|
||||
runtime_info=self.runtime_info, repo_instructions=repo_instructions
|
||||
).strip()
|
||||
|
||||
def get_example_user_message(self) -> str:
|
||||
"""This is the initial user message provided to the agent
|
||||
@@ -103,6 +116,7 @@ class PromptManager:
|
||||
These additional context will convert the current generic agent
|
||||
into a more specialized agent that is tailored to the user's task.
|
||||
"""
|
||||
|
||||
return self.user_template.render().strip()
|
||||
|
||||
def enhance_message(self, message: Message) -> None:
|
||||
|
||||
Reference in New Issue
Block a user