RFC & SSH exchange for development

frdel committed Dec 3, 2024 at 14:22 UTC 46689d6477d51966b9876b7d51b180e871569ebb
22 files changed +422 -161
agent.py
+3 -7
@@ -129,13 +129,9 @@ class AgentConfig:
129 rate_limit_requests: int = 15
130 rate_limit_input_tokens: int = 0
131 rate_limit_output_tokens: int = 0
132 - msgs_keep_max: int = 25
133 - msgs_keep_start: int = 5
134 - msgs_keep_end: int = 10
132 response_timeout_seconds: int = 60
136 - max_tool_response_length: int = 3000
137 - code_exec_docker_enabled: bool = True
138 - code_exec_docker_name: str = "agent-zero-dev"
133 + code_exec_docker_enabled: bool = False
134 + code_exec_docker_name: str = "A0-dev"
135 code_exec_docker_image: str = "frdel/agent-zero-run:development"
136 code_exec_docker_ports: dict[str, int] = field(
137 default_factory=lambda: {"22/tcp": 55022, "80/tcp": 55080}
@@ -150,7 +146,7 @@ class AgentConfig:
146 code_exec_ssh_addr: str = "localhost"
147 code_exec_ssh_port: int = 55022
148 code_exec_ssh_user: str = "root"
153 - code_exec_ssh_pass: str = "toor"
149 + code_exec_ssh_pass: str = ""
150 additional: Dict[str, Any] = field(default_factory=dict)
151
152
docker/run/Dockerfile
+8 -24
@@ -4,31 +4,16 @@ FROM debian:bookworm-slim
4 # Check if the argument is provided, else throw an error
5 ARG BRANCH
6 RUN if [ -z "$BRANCH" ]; then echo "ERROR: BRANCH is not set!" >&2; exit 1; fi
7 -
8 -# Update and install necessary packages
9 -RUN apt-get update && apt-get install -y \
10 - python3 \
11 - python3-pip \
12 - python3-venv \
13 - nodejs \
14 - npm \
15 - openssh-server \
16 - sudo \
17 - curl \
18 - wget \
19 - git \
20 - ffmpeg
21 -
22 -# Set up SSH
23 -RUN mkdir /var/run/sshd && \
24 - echo 'root:toor' | chpasswd && \
25 - sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
7 +ENV BRANCH=$BRANCH
8
9 # Copy contents of the project to /a0
10 COPY ./fs/ /
11
12 +# pre installation steps
13 +RUN bash /ins/pre_install.sh $BRANCH
14 +
15 # install additional software
31 -RUN bash /ins/install_searxng.sh
16 +RUN bash /ins/install_additional.sh $BRANCH
17
18 # install A0
19 RUN bash /ins/install_A0.sh $BRANCH
@@ -37,12 +22,11 @@ RUN bash /ins/install_A0.sh $BRANCH
22 ARG CACHE_DATE=none
23 RUN echo "cache buster $CACHE_DATE" && bash /ins/install_A02.sh $BRANCH
24
40 -# Cleanup package list
41 -RUN rm -rf /var/lib/apt/lists/*
42 -RUN apt-get clean
25 +# post installation steps
26 +RUN bash /ins/post_install.sh $BRANCH
27
28 # Expose ports
29 EXPOSE 22 80
30
31 # initialize runtime
48 -CMD ["/bin/bash", "/exe/initialize.sh", "$BRANCH"]
\ No newline at end of file
32 +CMD ["/bin/bash", "-c", "/bin/bash /exe/initialize.sh $BRANCH"]
\ No newline at end of file
docker/run/fs/exe/initialize.sh
+1 -5
@@ -18,11 +18,7 @@ chmod 444 /root/.profile
18 apt-get update &
19
20 # Start SSH service in background
21 -if [ "$BRANCH" != "development" ]; then
22 - /usr/sbin/sshd -D -o ListenAddress=127.0.0.1 &
23 -else
24 - /usr/sbin/sshd -D &
25 -fi
21 +/usr/sbin/sshd -D &
22
23 # Start searxng server in background
24 sudo -H -u searxng -i bash /exe/run_searxng.sh &
docker/run/fs/exe/run_A0.sh
+6 -4
@@ -28,6 +28,7 @@ function clone_repo() {
28 # setup and preload A0
29 setup_venv
30 clone_repo
31 +python /a0/prepare.py
32 python /a0/preload.py
33
34 # Loop to restart the Python script when it finishes
@@ -38,14 +39,15 @@ while true; do
39
40 echo "Starting A0..."
41 python /a0/run_ui.py \
42 + --dockerized=true \
43 --port=80 \
44 --host="0.0.0.0" \
45 --code_exec_docker_enabled=false \
46 --code_exec_ssh_enabled=true \
45 - --code_exec_ssh_addr="localhost" \
46 - --code_exec_ssh_port=22 \
47 - --code_exec_ssh_user="root" \
48 - --code_exec_ssh_pass="toor"
47 + # --code_exec_ssh_addr="localhost" \
48 + # --code_exec_ssh_port=22 \
49 + # --code_exec_ssh_user="root" \
50 + # --code_exec_ssh_pass="toor"
51
52 # Check the exit status
53 if [ $? -ne 0 ]; then
docker/run/fs/ins/install_additional.sh new
+4
@@ -0,0 +1,4 @@
1 +#!/bin/bash
2 +
3 +# run the original install script again
4 +bash /ins/install_searxng.sh "$@"
\ No newline at end of file
docker/run/fs/ins/post_install.sh new
+5
@@ -0,0 +1,5 @@
1 +#!/bin/bash
2 +
3 +# Cleanup package list
4 +rm -rf /var/lib/apt/lists/*
5 +apt-get clean
\ No newline at end of file
docker/run/fs/ins/pre_install.sh new
+18
@@ -0,0 +1,18 @@
1 +#!/bin/bash
2 +
3 +# Update and install necessary packages
4 +apt-get update && apt-get install -y \
5 + python3 \
6 + python3-pip \
7 + python3-venv \
8 + nodejs \
9 + npm \
10 + openssh-server \
11 + sudo \
12 + curl \
13 + wget \
14 + git \
15 + ffmpeg
16 +
17 +# prepare SSH daemon
18 +bash /ins/setup_ssh.sh "$@"
\ No newline at end of file
docker/run/fs/ins/setup_ssh.sh new
+6
@@ -0,0 +1,6 @@
1 +#!/bin/bash
2 +
3 +# Set up SSH
4 +mkdir /var/run/sshd && \
5 + # echo 'root:toor' | chpasswd && \
6 + sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
\ No newline at end of file
initialize.py
+41 -13
@@ -1,6 +1,7 @@
1 +import asyncio
2 import models
3 from agent import AgentConfig
3 -from python.helpers import files, runtime, settings
4 +from python.helpers import dotenv, files, rfc_exchange, runtime, settings, docker, log
5
6
7 def initialize():
@@ -49,28 +50,35 @@ def initialize():
50 rate_limit_requests=30,
51 # rate_limit_input_tokens = 0,
52 # rate_limit_output_tokens = 0,
52 - # msgs_keep_max = 25,
53 - # msgs_keep_start = 5,
54 - # msgs_keep_end = 10,
55 - max_tool_response_length=3000,
53 # response_timeout_seconds = 60,
57 - # code_exec_docker_enabled = True,
58 - # code_exec_docker_name = "agent-zero-dev",
54 + code_exec_docker_enabled = False,
55 + # code_exec_docker_name = "A0-dev",
56 # code_exec_docker_image = "frdel/agent-zero-run:development",
57 # code_exec_docker_ports = { "22/tcp": 55022, "80/tcp": 55080 }
58 # code_exec_docker_volumes = {
62 - # files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
63 - # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
59 + # files.get_base_dir(): {"bind": "/a0", "mode": "rw"},
60 + # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
61 # },
62 # code_exec_ssh_enabled = True,
63 # code_exec_ssh_addr = "localhost",
64 # code_exec_ssh_port = 55022,
65 # code_exec_ssh_user = "root",
69 - # code_exec_ssh_pass = "toor",
66 + # code_exec_ssh_pass = "",
67 # additional = {},
68 )
69
73 - # update config with kwargs
70 + # update SSH and docker settings
71 + set_runtime_config(config, current_settings)
72 +
73 + # update config with runtime args
74 + args_override(config)
75 +
76 + # return config object
77 + return config
78 +
79 +
80 +def args_override(config):
81 + # update config with runtime args
82 for key, value in runtime.args.items():
83 if hasattr(config, key):
84 # conversion based on type of config[key]
@@ -93,5 +101,25 @@ def initialize():
101
102 setattr(config, key, value)
103
96 - # return config object
97 - return config
104 +
105 +def set_runtime_config(config: AgentConfig, set: settings.Settings):
106 + ssh_conf = settings.get_runtime_config(set)
107 + for key, value in ssh_conf.items():
108 + if hasattr(config, key):
109 + setattr(config, key, value)
110 +
111 + # if config.code_exec_docker_enabled:
112 + # config.code_exec_docker_ports["22/tcp"] = ssh_conf["code_exec_ssh_port"]
113 + # config.code_exec_docker_ports["80/tcp"] = ssh_conf["code_exec_http_port"]
114 + # config.code_exec_docker_name = f"{config.code_exec_docker_name}-{ssh_conf['code_exec_ssh_port']}-{ssh_conf['code_exec_http_port']}"
115 +
116 + # dman = docker.DockerContainerManager(
117 + # logger=log.Log(),
118 + # name=config.code_exec_docker_name,
119 + # image=config.code_exec_docker_image,
120 + # ports=config.code_exec_docker_ports,
121 + # volumes=config.code_exec_docker_volumes,
122 + # )
123 + # dman.start_container()
124 +
125 + # config.code_exec_ssh_pass = asyncio.run(rfc_exchange.get_root_password())
preload.py
+4 -1
@@ -1,3 +1,6 @@
1 from python.helpers import whisper
2
3 -whisper.preload() # preload transcription model
\ No newline at end of file
3 +print("Running preload...")
4 +
5 +# preload transcription model
6 +whisper.preload()
\ No newline at end of file
prepare.py new
+14
@@ -0,0 +1,14 @@
1 +import subprocess
2 +from python.helpers import dotenv, runtime, settings
3 +import string
4 +import random
5 +
6 +print("Preparing environment...")
7 +
8 +# generate random root password if not set (for SSH)
9 +root_pass = dotenv.get_dotenv_value(dotenv.KEY_ROOT_PASSWORD)
10 +if not root_pass:
11 + root_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
12 + dotenv.save_dotenv_value(dotenv.KEY_ROOT_PASSWORD, root_pass)
13 +print("Changing root password...")
14 +subprocess.run(f"echo 'root:{root_pass}' | chpasswd", shell=True, check=True)
python/helpers/crypto.py new
+66
@@ -0,0 +1,66 @@
1 +import hashlib
2 +import hmac
3 +from cryptography.hazmat.primitives.asymmetric import rsa, padding
4 +from cryptography.hazmat.primitives import serialization, hashes
5 +import os
6 +
7 +
8 +def hash_data(data: str, password: str):
9 + return hmac.new(password.encode(), data.encode(), hashlib.sha256).hexdigest()
10 +
11 +
12 +def verify_data(data: str, hash: str, password: str):
13 + return hash_data(data, password) == hash
14 +
15 +
16 +def _generate_private_key():
17 + return rsa.generate_private_key(
18 + public_exponent=65537,
19 + key_size=2048,
20 + )
21 +
22 +
23 +def _generate_public_key(private_key: rsa.RSAPrivateKey):
24 + return (
25 + private_key.public_key()
26 + .public_bytes(
27 + encoding=serialization.Encoding.PEM,
28 + format=serialization.PublicFormat.SubjectPublicKeyInfo,
29 + )
30 + .hex()
31 + )
32 +
33 +def _decode_public_key(public_key: str) -> rsa.RSAPublicKey:
34 + # Decode hex string back to bytes
35 + pem_bytes = bytes.fromhex(public_key)
36 + # Load the PEM public key
37 + key = serialization.load_pem_public_key(pem_bytes)
38 + if not isinstance(key, rsa.RSAPublicKey):
39 + raise TypeError("The provided key is not an RSAPublicKey")
40 + return key
41 +
42 +def encrypt_data(data: str, public_key_pem: str):
43 + return _encrypt_data(data.encode("utf-8"), _decode_public_key(public_key_pem))
44 +
45 +def _encrypt_data(data: bytes, public_key: rsa.RSAPublicKey):
46 + b = public_key.encrypt(
47 + data,
48 + padding.OAEP(
49 + mgf=padding.MGF1(algorithm=hashes.SHA256()),
50 + algorithm=hashes.SHA256(),
51 + label=None,
52 + ),
53 + )
54 + return b.hex()
55 +
56 +def decrypt_data(data: str, private_key: rsa.RSAPrivateKey):
57 + b = private_key.decrypt(
58 + bytes.fromhex(data),
59 + padding.OAEP(
60 + mgf=padding.MGF1(algorithm=hashes.SHA256()),
61 + algorithm=hashes.SHA256(),
62 + label=None,
63 + ),
64 + )
65 + return b.decode("utf-8")
66 +
python/helpers/docker.py
+1 -1
@@ -93,7 +93,7 @@ class DockerContainerManager:
93 name=self.name,
94 volumes=self.volumes, # type: ignore
95 )
96 - atexit.register(self.cleanup_container)
96 + # atexit.register(self.cleanup_container)
97 print(f"Started container with ID: {self.container.id}")
98 if self.logger: self.logger.log(type="info", content=f"Started container with ID: {self.container.id}")
99 time.sleep(5) # this helps to get SSH ready
python/helpers/dotenv.py
+3 -1
@@ -1,11 +1,13 @@
1 import os
2 import re
3 +
4 from .files import get_abs_path
5 from dotenv import load_dotenv as _load_dotenv
6
7 KEY_AUTH_LOGIN = "AUTH_LOGIN"
8 KEY_AUTH_PASSWORD = "AUTH_PASSWORD"
9 KEY_RFC_PASSWORD = "RFC_PASSWORD"
10 +KEY_ROOT_PASSWORD = "ROOT_PASSWORD"
11
12 def load_dotenv():
13 _load_dotenv(get_dotenv_file_path(), override=True)
@@ -15,7 +17,7 @@ def get_dotenv_file_path():
17 return get_abs_path(".env")
18
19 def get_dotenv_value(key: str):
18 - # load_dotenv()
20 + # load_dotenv()
21 return os.getenv(key)
22
23 def save_dotenv_value(key: str, value: str):
python/helpers/messages.py
+1 -1
@@ -5,7 +5,7 @@ import json
5
6 def truncate_text(agent, output, threshold=1000):
7 threshold = int(threshold)
8 - if len(output) <= threshold:
8 + if not threshold or len(output) <= threshold:
9 return output
10
11 # Adjust the file path as needed
python/helpers/rfc.py
+3 -12
@@ -3,8 +3,7 @@ import inspect
3 import json
4 from typing import Any, TypedDict
5 import aiohttp
6 -import hmac
7 -import hashlib
6 +from python.helpers import crypto
7
8 from python.helpers import dotenv
9
@@ -36,14 +35,14 @@ async def call_rfc(
35 kwargs=kwargs,
36 )
37 call = RFCCall(
39 - rfc_input=json.dumps(input), hash=hash_data(json.dumps(input), password)
38 + rfc_input=json.dumps(input), hash=crypto.hash_data(json.dumps(input), password)
39 )
40 result = await _send_json_data(url, call)
41 return result
42
43
44 async def handle_rfc(rfc_call: RFCCall, password: str):
46 - if not verify_data(rfc_call["rfc_input"], rfc_call["hash"], password):
45 + if not crypto.verify_data(rfc_call["rfc_input"], rfc_call["hash"], password):
46 raise Exception("Invalid RFC hash")
47
48 input: RFCInput = json.loads(rfc_call["rfc_input"])
@@ -80,11 +79,3 @@ async def _send_json_data(url: str, data):
79 else:
80 error = await response.text()
81 raise Exception(error)
83 -
84 -
85 -def hash_data(data: str, password: str):
86 - return hmac.new(password.encode(), data.encode(), hashlib.sha256).hexdigest()
87 -
88 -
89 -def verify_data(data: str, hash: str, password: str):
90 - return hash_data(data, password) == hash
python/helpers/rfc_exchange.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers import runtime, crypto, dotenv
2 +
3 +async def get_root_password():
4 + if runtime.is_dockerized():
5 + pswd = _get_root_password()
6 + else:
7 + priv = crypto._generate_private_key()
8 + pub = crypto._generate_public_key(priv)
9 + enc = await runtime.call_development_function(_provide_root_password, pub)
10 + pswd = crypto.decrypt_data(enc, priv)
11 + return pswd
12 +
13 +def _provide_root_password(public_key_pem: str):
14 + pswd = _get_root_password()
15 + enc = crypto.encrypt_data(pswd, public_key_pem)
16 + return enc
17 +
18 +def _get_root_password():
19 + return dotenv.get_dotenv_value(dotenv.KEY_ROOT_PASSWORD) or ""
\ No newline at end of file
python/helpers/runtime.py
+15 -6
@@ -35,10 +35,15 @@ def get_arg(name: str):
35 global args
36 return args.get(name, None)
37
38 +def has_arg(name: str):
39 + global args
40 + return name in args
41
39 -def is_development() -> bool:
40 - return get_arg("development") == True
42 +def is_dockerized() -> bool:
43 + return get_arg("dockerized")
44
45 +def is_development() -> bool:
46 + return not is_dockerized()
47
48 async def call_development_function(func: Callable, *args, **kwargs):
49 if is_development():
@@ -71,8 +76,12 @@ def _get_rfc_password() -> str:
76
77
78 def _get_rfc_url() -> str:
74 - url = settings.get_settings()["rfc_url"]
75 - if not url.endswith("/"):
76 - url += "/"
77 - url += "rfc"
79 + set = settings.get_settings()
80 + url = set["rfc_url"]
81 + if not "://" in url:
82 + url = "http://"+url
83 + if url.endswith("/"):
84 + url = url[:-1]
85 + url = url+":"+str(set["rfc_port_http"])
86 + url += "/rfc"
87 return url
python/helpers/settings.py
+196 -80
@@ -1,10 +1,12 @@
1 +import asyncio
2 import json
3 import os
4 import re
5 +import subprocess
6 from typing import Any, Literal, Optional, TypedDict
7
8 import models
7 -from python.helpers import whisper
9 +from python.helpers import rfc_exchange, runtime, whisper
10 from . import files, dotenv
11 from models import get_model, ModelProvider, ModelType
12 from langchain_core.language_models.chat_models import BaseChatModel
@@ -36,9 +38,13 @@ class Settings(TypedDict):
38
39 auth_login: str
40 auth_password: str
41 + root_password: str
42
43 + rfc_auto_docker: bool
44 rfc_url: str
45 rfc_password: str
46 + rfc_port_http: int
47 + rfc_port_ssh: int
48
49 stt_model_size: str
50 stt_language: str
@@ -46,6 +52,7 @@ class Settings(TypedDict):
52 stt_silence_duration: int
53 stt_waiting_timeout: int
54
55 +
56 class PartialSettings(Settings, total=False):
57 pass
58
@@ -77,6 +84,8 @@ class SettingsOutput(TypedDict):
84 sections: list[SettingsSection]
85
86
87 +PASSWORD_PLACEHOLDER = "****PSWD****"
88 +
89 SETTINGS_FILE = files.get_abs_path("tmp/settings.json")
90 _settings: Settings | None = None
91
@@ -290,8 +299,8 @@ def convert_out(settings: Settings) -> SettingsOutput:
299 auth_fields.append(
300 {
301 "id": "auth_login",
293 - "title": "Login",
294 - "description": "User name",
302 + "title": "UI Login",
303 + "description": "Set user name for web UI",
304 "type": "input",
305 "value": dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or "",
306 }
@@ -300,13 +309,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
309 auth_fields.append(
310 {
311 "id": "auth_password",
303 - "title": "Password",
304 - "description": "User password",
312 + "title": "UI Password",
313 + "description": "Set user password for web UI",
314 "type": "password",
306 - "value": dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) or "",
315 + "value": (
316 + PASSWORD_PLACEHOLDER
317 + if dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD)
318 + else ""
319 + ),
320 }
321 )
322
323 + if runtime.is_dockerized():
324 + auth_fields.append(
325 + {
326 + "id": "root_password",
327 + "title": "root Password",
328 + "description": "Change linux root password in docker container. This password can be used for SSH access. Original password was randomly generated during setup.",
329 + "type": "password",
330 + "value": "",
331 + }
332 + )
333 +
334 auth_section: SettingsSection = {
335 "title": "Authentication",
336 "description": "Settings for authentication to use Agent Zero Web UI.",
@@ -393,90 +417,136 @@ def convert_out(settings: Settings) -> SettingsOutput:
417
418 dev_fields: list[SettingsField] = []
419
396 - dev_fields.append(
397 - {
398 - "id": "rfc_url",
399 - "title": "RFC Destination URL",
400 - "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.",
401 - "type": "input",
402 - "value": settings["rfc_url"],
403 - }
404 - )
420 + if runtime.is_development():
421 + # dev_fields.append(
422 + # {
423 + # "id": "rfc_auto_docker",
424 + # "title": "RFC Auto Docker Management",
425 + # "description": "Automatically create dockerized instance of A0 for RFCs using this instance's code base and, settings and .env.",
426 + # "type": "input",
427 + # "value": settings["rfc_auto_docker"],
428 + # }
429 + # )
430 +
431 + dev_fields.append(
432 + {
433 + "id": "rfc_url",
434 + "title": "RFC Destination URL",
435 + "description": "URL of dockerized A0 instance for remote function calls. Do not specify port here.",
436 + "type": "input",
437 + "value": settings["rfc_url"],
438 + }
439 + )
440
441 dev_fields.append(
442 {
443 "id": "rfc_password",
444 "title": "RFC Password",
410 - "description": "Password for remote function calls. Passwords must match on both systems. RFCs can not be used with empty password.",
445 + "description": "Password for remote function calls. Passwords must match on both instances. RFCs can not be used with empty password.",
446 "type": "password",
412 - "value": dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD) or "",
447 + "value": (
448 + PASSWORD_PLACEHOLDER
449 + if dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD)
450 + else ""
451 + ),
452 }
453 )
454
455 + if runtime.is_development():
456 + dev_fields.append(
457 + {
458 + "id": "rfc_port_http",
459 + "title": "RFC HTTP port",
460 + "description": "HTTP port for dockerized instance of A0.",
461 + "type": "input",
462 + "value": settings["rfc_port_http"],
463 + }
464 + )
465 +
466 + dev_fields.append(
467 + {
468 + "id": "rfc_port_ssh",
469 + "title": "RFC SSH port",
470 + "description": "SSH port for dockerized instance of A0.",
471 + "type": "input",
472 + "value": settings["rfc_port_ssh"],
473 + }
474 + )
475 +
476 dev_section: SettingsSection = {
477 "title": "Development",
418 - "description": "Parameters for A0 framework development.",
478 + "description": "Parameters for A0 framework development. RFCs (remote function calls) 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. This is crucial for development as A0 needs to run in standardized environment to support all features.",
479 "fields": dev_fields,
480 }
481
482 # Speech to text section
483 stt_fields: list[SettingsField] = []
424 -
425 - stt_fields.append({
426 - "id": "stt_model_size",
427 - "title": "Model Size",
428 - "description": "Select the speech recognition model size",
429 - "type": "select",
430 - "value": settings["stt_model_size"],
431 - "options": [
432 - {"value": "tiny", "label": "Tiny (39M, English)"},
433 - {"value": "base", "label": "Base (74M, English)"},
434 - {"value": "small", "label": "Small (244M, English)"},
435 - {"value": "medium", "label": "Medium (769M, English)"},
436 - {"value": "large", "label": "Large (1.5B, Multilingual)"},
437 - {"value": "turbo", "label": "Turbo (Multilingual)"}
438 - ]
439 - })
440 -
441 - stt_fields.append({
442 - "id": "stt_language",
443 - "title": "Language Code",
444 - "description": "Language code (e.g. en, fr, it)",
445 - "type": "input",
446 - "value": settings["stt_language"]
447 - })
448 -
449 - stt_fields.append({
450 - "id": "stt_silence_threshold",
451 - "title": "Silence threshold",
452 - "description": "Silence detection threshold. Lower values are more sensitive.",
453 - "type": "range",
454 - "min": 0,
455 - "max": 1,
456 - "step": 0.01,
457 - "value": settings["stt_silence_threshold"]
458 - })
459 -
460 - stt_fields.append({
461 - "id": "stt_silence_duration",
462 - "title": "Silence duration (ms)",
463 - "description": "Duration of silence before the server considers speaking to have ended.",
464 - "type": "input",
465 - "value": settings["stt_silence_duration"]
466 - })
467 -
468 - stt_fields.append({
469 - "id": "stt_waiting_timeout",
470 - "title": "Waiting timeout (ms)",
471 - "description": "Duration before the server closes the microphone.",
472 - "type": "input",
473 - "value": settings["stt_waiting_timeout"]
474 - })
484 +
485 + stt_fields.append(
486 + {
487 + "id": "stt_model_size",
488 + "title": "Model Size",
489 + "description": "Select the speech recognition model size",
490 + "type": "select",
491 + "value": settings["stt_model_size"],
492 + "options": [
493 + {"value": "tiny", "label": "Tiny (39M, English)"},
494 + {"value": "base", "label": "Base (74M, English)"},
495 + {"value": "small", "label": "Small (244M, English)"},
496 + {"value": "medium", "label": "Medium (769M, English)"},
497 + {"value": "large", "label": "Large (1.5B, Multilingual)"},
498 + {"value": "turbo", "label": "Turbo (Multilingual)"},
499 + ],
500 + }
501 + )
502 +
503 + stt_fields.append(
504 + {
505 + "id": "stt_language",
506 + "title": "Language Code",
507 + "description": "Language code (e.g. en, fr, it)",
508 + "type": "input",
509 + "value": settings["stt_language"],
510 + }
511 + )
512 +
513 + stt_fields.append(
514 + {
515 + "id": "stt_silence_threshold",
516 + "title": "Silence threshold",
517 + "description": "Silence detection threshold. Lower values are more sensitive.",
518 + "type": "range",
519 + "min": 0,
520 + "max": 1,
521 + "step": 0.01,
522 + "value": settings["stt_silence_threshold"],
523 + }
524 + )
525 +
526 + stt_fields.append(
527 + {
528 + "id": "stt_silence_duration",
529 + "title": "Silence duration (ms)",
530 + "description": "Duration of silence before the server considers speaking to have ended.",
531 + "type": "input",
532 + "value": settings["stt_silence_duration"],
533 + }
534 + )
535 +
536 + stt_fields.append(
537 + {
538 + "id": "stt_waiting_timeout",
539 + "title": "Waiting timeout (ms)",
540 + "description": "Duration before the server closes the microphone.",
541 + "type": "input",
542 + "value": settings["stt_waiting_timeout"],
543 + }
544 + )
545
546 stt_section: SettingsSection = {
547 "title": "Speech to Text",
548 "description": "Voice transcription preferences and server turn detection settings.",
479 - "fields": stt_fields
549 + "fields": stt_fields,
550 }
551
552 # Add the section to the result
@@ -501,7 +571,7 @@ def _get_api_key_field(settings: Settings, provider: str, title: str) -> Setting
571 "id": f"api_key_{provider}",
572 "title": title,
573 "type": "password",
504 - "value": key if key != "None" else "",
574 + "value": (PASSWORD_PLACEHOLDER if key and key != "None" else ""),
575 }
576
577
@@ -510,12 +580,13 @@ def convert_in(settings: dict) -> Settings:
580 for section in settings["sections"]:
581 if "fields" in section:
582 for field in section["fields"]:
513 - if field["id"].endswith("_kwargs"):
514 - current[field["id"]] = _env_to_dict(field["value"])
515 - elif field["id"].startswith("api_key_"):
516 - current["api_keys"][field["id"]] = field["value"]
517 - else:
518 - current[field["id"]] = field["value"]
583 + if field["value"] != PASSWORD_PLACEHOLDER:
584 + if field["id"].endswith("_kwargs"):
585 + current[field["id"]] = _env_to_dict(field["value"])
586 + elif field["id"].startswith("api_key_"):
587 + current["api_keys"][field["id"]] = field["value"]
588 + else:
589 + current[field["id"]] = field["value"]
590 return current
591
592
@@ -584,6 +655,7 @@ def get_embedding_model(settings: Settings | None = None) -> Embeddings:
655 **settings["embed_model_kwargs"],
656 )
657
658 +
659 def _read_settings_file() -> Settings | None:
660 if os.path.exists(SETTINGS_FILE):
661 content = files.read_file(SETTINGS_FILE)
@@ -605,14 +677,23 @@ def _remove_sensitive_settings(settings: Settings):
677 settings["auth_login"] = ""
678 settings["auth_password"] = ""
679 settings["rfc_password"] = ""
680 + settings["root_password"] = ""
681
682
683 def _write_sensitive_settings(settings: Settings):
684 for key, val in settings["api_keys"].items():
685 dotenv.save_dotenv_value(key.upper(), val)
686 +
687 dotenv.save_dotenv_value(dotenv.KEY_AUTH_LOGIN, settings["auth_login"])
614 - dotenv.save_dotenv_value(dotenv.KEY_AUTH_PASSWORD, settings["auth_password"])
615 - dotenv.save_dotenv_value(dotenv.KEY_RFC_PASSWORD, settings["rfc_password"])
688 + if settings["auth_password"]:
689 + dotenv.save_dotenv_value(dotenv.KEY_AUTH_PASSWORD, settings["auth_password"])
690 + if settings["rfc_password"]:
691 + dotenv.save_dotenv_value(dotenv.KEY_RFC_PASSWORD, settings["rfc_password"])
692 +
693 + if settings["root_password"]:
694 + dotenv.save_dotenv_value(dotenv.KEY_ROOT_PASSWORD, settings["root_password"])
695 + if settings["root_password"]:
696 + dotenv.save_dotenv_value(dotenv.KEY_ROOT_PASSWORD, settings["root_password"])
697
698
699 def _get_default_settings() -> Settings:
@@ -633,11 +714,15 @@ def _get_default_settings() -> Settings:
714 api_keys={},
715 auth_login="",
716 auth_password="",
717 + root_password="",
718 agent_prompts_subdir="default",
719 agent_memory_subdir="default",
720 agent_knowledge_subdir="custom",
639 - rfc_url="http://localhost:55080",
721 + rfc_auto_docker=True,
722 + rfc_url="localhost",
723 rfc_password="",
724 + rfc_port_http=55080,
725 + rfc_port_ssh=55022,
726 stt_model_size="base",
727 stt_language="en",
728 stt_silence_threshold=0.3,
@@ -663,6 +748,7 @@ def _apply_settings():
748 # reload whisper model if necessary
749 whisper.preload()
750
751 +
752 def _env_to_dict(data: str):
753 env_dict = {}
754 line_pattern = re.compile(r"\s*([^#][^=]*)\s*=\s*(.*)")
@@ -685,3 +771,33 @@ def _dict_to_env(data_dict):
771 value = f'"{value}"'
772 lines.append(f"{key}={value}")
773 return "\n".join(lines)
774 +
775 +
776 +def set_root_password(password: str):
777 + if not runtime.is_dockerized():
778 + raise Exception("root password can only be set in dockerized environments")
779 + subprocess.run(["echo", "root:" + password, "|", "chpasswd"], shell=True)
780 +
781 +
782 +def get_runtime_config(set: Settings):
783 + if runtime.is_dockerized():
784 + return {
785 + "code_exec_ssh_addr": "localhost",
786 + "code_exec_ssh_port": 22,
787 + "code_exec_http_port": 80,
788 + "code_exec_ssh_user": "root",
789 + }
790 + else:
791 + host = set["rfc_url"]
792 + if "//" in host:
793 + host = host.split("//")[1]
794 + if ":" in host:
795 + host, port = host.split(":")
796 + if host.endswith("/"):
797 + host = host[:-1]
798 + return {
799 + "code_exec_ssh_addr": host,
800 + "code_exec_ssh_port": set["rfc_port_ssh"],
801 + "code_exec_http_port": set["rfc_port_http"],
802 + "code_exec_ssh_user": "root",
803 + }
python/helpers/tool.py
+1 -1
@@ -31,7 +31,7 @@ class Tool:
31 PrintStyle().print()
32
33 async def after_execution(self, response: Response, **kwargs):
34 - text = messages.truncate_text(self.agent, response.message.strip(), self.agent.config.max_tool_response_length)
34 + text = response.message.strip()
35 await self.agent.hist_add_tool_result(self.name, text)
36 PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'")
37 PrintStyle(font_color="#85C1E9").print(response.message)
python/tools/code_execution_tool.py
+3 -2
@@ -3,7 +3,7 @@ from dataclasses import dataclass
3 import shlex
4 import time
5 from python.helpers.tool import Tool, Response
6 -from python.helpers import files
6 +from python.helpers import files, rfc_exchange
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.shell_local import LocalInteractiveSession
9 from python.helpers.shell_ssh import SSHInteractiveSession
@@ -93,12 +93,13 @@ class CodeExecution(Tool):
93
94 # initialize local or remote interactive shell insterface
95 if self.agent.config.code_exec_ssh_enabled:
96 + pswd = self.agent.config.code_exec_ssh_pass if self.agent.config.code_exec_ssh_pass else await rfc_exchange.get_root_password()
97 shell = SSHInteractiveSession(
98 self.agent.context.log,
99 self.agent.config.code_exec_ssh_addr,
100 self.agent.config.code_exec_ssh_port,
101 self.agent.config.code_exec_ssh_user,
101 - self.agent.config.code_exec_ssh_pass,
102 + pswd,
103 )
104 else:
105 shell = LocalInteractiveSession()
run_cli.py
+4 -3
@@ -3,7 +3,7 @@ import sys
3 import threading, time, models, os
4 from ansio import application_keypad, mouse_input, raw_input
5 from ansio.input import InputEvent, get_input_event
6 -from agent import AgentContext
6 +from agent import AgentContext, UserMessage
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.files import read_file
9 from python.helpers import files
@@ -51,7 +51,7 @@ async def chat(context: AgentContext):
51 if user_input.lower() == 'e': break
52
53 # send message to agent0,
54 - assistant_response = await context.communicate(user_input).result()
54 + assistant_response = await context.communicate(UserMessage(user_input, [])).result()
55
56 # print agent0 response
57 PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{context.agent0.agent_name}: reponse:")
@@ -69,7 +69,7 @@ def intervention():
69 PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
70
71 if user_input.lower() == 'e': os._exit(0) # exit the conversation when the user types 'exit'
72 - if user_input: context.streaming_agent.intervention_message = user_input # set intervention message if non-empty
72 + if user_input: context.streaming_agent.intervention = UserMessage(user_input, []) # set intervention message if non-empty
73 context.paused = False # continue agent streaming
74
75
@@ -112,4 +112,5 @@ def run():
112 asyncio.run(chat(context))
113
114 if __name__ == "__main__":
115 + print("\n\n!!! run_cli.py is now discontinued. run_ui.py serves as both UI and API endpoint !!!\n\n")
116 run()
\ No newline at end of file