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