diff --git a/.gitattributes b/.gitattributes index dfe077042..0ffeae3fe 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Auto detect text files and perform LF normalization -* text=auto +* text=auto eol=lf \ No newline at end of file diff --git a/docker/run/Dockerfile b/docker/run/Dockerfile index 1bb53a946..7c02b0c55 100644 --- a/docker/run/Dockerfile +++ b/docker/run/Dockerfile @@ -36,7 +36,8 @@ RUN bash /ins/install_A0.sh RUN bash /ins/install_searxng.sh # Expose ports -EXPOSE 22 80 +# EXPOSE 22 +EXPOSE 80 # initialize runtime CMD ["/bin/bash", "/exe/initialize.sh"] \ No newline at end of file diff --git a/docker/run/fs/ins/install_A0.sh b/docker/run/fs/ins/install_A0.sh index f34c54cd7..6791083a9 100644 --- a/docker/run/fs/ins/install_A0.sh +++ b/docker/run/fs/ins/install_A0.sh @@ -11,7 +11,10 @@ source /opt/venv/bin/activate # Ensure the virtual environment and pip setup pip install --upgrade pip ipython requests -# Install A0 python packages +# Install some packages in specific variants +pip install torch --index-url https://download.pytorch.org/whl/cpu + +# Install remaining A0 python packages pip install -r /a0/requirements.txt # Preload A0 diff --git a/python/helpers/dotenv.py b/python/helpers/dotenv.py index 60c428ac9..f96f3aba1 100644 --- a/python/helpers/dotenv.py +++ b/python/helpers/dotenv.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv as _load_dotenv KEY_AUTH_LOGIN = "AUTH_LOGIN" KEY_AUTH_PASSWORD = "AUTH_PASSWORD" +KEY_RFC_PASSWORD = "RFC_PASSWORD" def load_dotenv(): _load_dotenv(get_dotenv_file_path(), override=True) diff --git a/python/helpers/rfc.py b/python/helpers/rfc.py index f081e6317..dd5cbb720 100644 --- a/python/helpers/rfc.py +++ b/python/helpers/rfc.py @@ -3,9 +3,16 @@ import inspect import json from typing import Any, TypedDict import aiohttp +import hmac +import hashlib + +from python.helpers import dotenv + # Remote Function Call library # Call function via http request +# Secured by pre-shared key + class RFCInput(TypedDict): module: str @@ -14,19 +21,32 @@ class RFCInput(TypedDict): kwargs: dict[str, Any] -async def call_rfc(url: str, module: str, function_name: str, args: list, kwargs: dict): - input = { - "module": module, - "function_name": function_name, - "args": args, - "kwargs": kwargs, - } - input_json = json.dumps(input) - result = await _send_json_data(url, input_json) +class RFCCall(TypedDict): + rfc_input: str + hash: str + + +async def call_rfc( + url: str, password: str, module: str, function_name: str, args: list, kwargs: dict +): + input = RFCInput( + module=module, + function_name=function_name, + args=args, + kwargs=kwargs, + ) + call = RFCCall( + rfc_input=json.dumps(input), hash=hash_data(json.dumps(input), password) + ) + result = await _send_json_data(url, json.dumps(call)) return result -async def handle_rfc(input: RFCInput): +async def handle_rfc(rfc_call: RFCCall, password: str): + if not verify_data(rfc_call["rfc_input"], rfc_call["hash"], password): + raise Exception("Invalid RFC hash") + + input: RFCInput = json.loads(rfc_call["rfc_input"]) return await _call_function( input["module"], input["function_name"], *input["args"], **input["kwargs"] ) @@ -50,5 +70,16 @@ def _get_function(module: str, function_name: str): async def _send_json_data(url: str, data: str): async with aiohttp.ClientSession() as session: - async with session.post(url, json=data) as response: - return await response.json() \ No newline at end of file + async with session.post( + url, + json=data, + ) as response: + return await response.json() + + +def hash_data(data: str, password: str): + return hmac.new(password.encode(), data.encode(), hashlib.sha256).hexdigest() + + +def verify_data(data: str, hash: str, password: str): + return hash_data(data, password) == hash diff --git a/python/helpers/runtime.py b/python/helpers/runtime.py index 9b9fa21ea..e9b9d7de5 100644 --- a/python/helpers/runtime.py +++ b/python/helpers/runtime.py @@ -1,6 +1,6 @@ import argparse from typing import Any, Callable, Coroutine -from python.helpers import rfc, docker +from python.helpers import dotenv, rfc, docker, settings parser = argparse.ArgumentParser() args = {} @@ -42,8 +42,10 @@ def is_development() -> bool: async def call_development_function(func: Callable, *args, **kwargs): if is_development(): url = _get_rfc_url() + password = _get_rfc_password() return await rfc.call_rfc( url=url, + password=password, module=func.__module__, function_name=func.__name__, args=list(args), @@ -53,17 +55,33 @@ async def call_development_function(func: Callable, *args, **kwargs): return await func(*args, **kwargs) +async def handle_rfc(rfc_call: rfc.RFCCall): + return await rfc.handle_rfc(rfc_call=rfc_call, password=_get_rfc_password()) + + +def _get_rfc_password() -> str: + password = dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD) + if not password: + raise Exception("No RFC password, cannot handle RFC calls.") + return password + + def _get_rfc_url() -> str: - if get_arg("rfc_url"): - return str(get_arg("rfc_url")) - global dockerman - if dockerman is None: - dockerman = docker.DockerContainerManager( - image="agent-zero-run", - name="agent-zero-development", - ports={"55080": 80, "55022": 22}, - volumes={}, - logger=None, - ) - conts = dockerman.get_image_containers() - return f"http://localhost:{conts[0]['web_port']}/rfc" + url = settings.get_settings()["rfc_url"] + if not url.endswith("/"): + url += "/" + url += "url" + return url + # if get_arg("rfc_url"): + # return str(get_arg("rfc_url")) + # global dockerman + # if dockerman is None: + # dockerman = docker.DockerContainerManager( + # image="agent-zero-run", + # name="agent-zero-development", + # ports={"55080": 80, "55022": 22}, + # volumes={}, + # logger=None, + # ) + # conts = dockerman.get_image_containers() + # return f"http://localhost:{conts[0]['web_port']}/rfc" diff --git a/python/helpers/settings.py b/python/helpers/settings.py index d19ae2ff2..c4c7d90bc 100644 --- a/python/helpers/settings.py +++ b/python/helpers/settings.py @@ -34,6 +34,9 @@ class Settings(TypedDict): auth_login: str auth_password: str + rfc_url: str + rfc_password: str + class PartialSettings(Settings, total=False): pass @@ -357,6 +360,34 @@ def convert_out(settings: Settings) -> SettingsOutput: "fields": agent_fields, } + dev_fields: list[SettingsField] = [] + + dev_fields.append( + { + "id": "rfc_url", + "title": "RFC Destination URL", + "description": "URL for remote function calls. RFCs are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker.", + "type": "input", + "value": settings["rfc_url"], + } + ) + + dev_fields.append( + { + "id": "rfc_password", + "title": "RFC Password", + "description": "Password for remote function calls. Passwords must match on both systems. RFCs can not be used with empty password.", + "type": "password", + "value": dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD), + } + ) + + dev_section: SettingsSection = { + "title": "Development", + "description": "Parameters for A0 framework development.", + "fields": dev_fields, + } + result: SettingsOutput = { "sections": [ agent_section, @@ -365,6 +396,7 @@ def convert_out(settings: Settings) -> SettingsOutput: embed_model_section, api_keys_section, auth_section, + dev_section, ] } return result @@ -476,6 +508,7 @@ def _remove_sensitive_settings(settings: Settings): settings["api_keys"] = {} settings["auth_login"] = "" settings["auth_password"] = "" + settings["rfc_password"] = "" def _write_sensitive_settings(settings: Settings): @@ -483,6 +516,7 @@ def _write_sensitive_settings(settings: Settings): dotenv.save_dotenv_value(key.upper(), val) dotenv.save_dotenv_value(dotenv.KEY_AUTH_LOGIN, settings["auth_login"]) dotenv.save_dotenv_value(dotenv.KEY_AUTH_PASSWORD, settings["auth_password"]) + dotenv.save_dotenv_value(dotenv.KEY_RFC_PASSWORD, settings["rfc_password"]) def _get_default_settings() -> Settings: @@ -504,6 +538,8 @@ def _get_default_settings() -> Settings: agent_prompts_subdir="default", agent_memory_subdir="default", agent_knowledge_subdir="custom", + rfc_url="http://localhost:55080", + rfc_password="", ) diff --git a/run_ui.py b/run_ui.py index 0dffb4c54..9e87d2746 100644 --- a/run_ui.py +++ b/run_ui.py @@ -542,8 +542,8 @@ async def handle_rfc(): # data sent to the server input = json.loads(request.get_json()) - # handle RFC call - result = await rfc.handle_rfc(input) + # handle RFC + result = await runtime.handle_rfc(input) return jsonify(result) diff --git a/webui/settings.css b/webui/settings.css index d38a5212b..9ceb790e2 100644 --- a/webui/settings.css +++ b/webui/settings.css @@ -38,6 +38,7 @@ select { .modal-header ul { margin-bottom: 0; + line-height: 2em; } .modal-content {