v0.8.1 release
frdel committed
Jan 17, 2025 at 22:50 UTC
c161529e9035c18a9e97635379a4871a0551c8d6
44 files changed
+1009
-557
agent.py
+4
@@ -618,6 +618,10 @@ class Agent:
618
await self.hist_add_user_message(msg, intervention=True)
619
raise InterventionException(msg)
620
621
+ async def wait_if_paused(self):
622
+ while self.context.paused:
623
+ await asyncio.sleep(0.1)
624
+
625
async def process_tools(self, msg: str):
626
# search for tool usage requests in agent message
627
tool_request = extract_tools.json_parse_dirty(msg)
docker/run/fs/exe/initialize.sh
+1
-1
@@ -21,7 +21,7 @@ apt-get update > /dev/null 2>&1 &
21
/usr/sbin/sshd -D &
22
23
# Start searxng server in background
24
-su - searxng -c "bash /exe/run_searxng.sh" &
24
+su - searxng -c "bash /exe/run_searxng.sh \"$@\"" &
25
26
# Start A0 and restart on exit
27
bash /exe/run_A0.sh "$@"
docker/run/fs/exe/run_A0.sh
+5
-30
@@ -1,38 +1,13 @@
1
#!/bin/bash
2
3
-# Paths
4
-SOURCE_DIR="/git/agent-zero"
5
-TARGET_DIR="/a0"
6
-
7
-
8
-function setup_venv() {
9
- . "/ins/setup_venv.sh" "$@"
10
-}
11
-
12
-function clone_and_install() {
13
- # Copy repository files if run_ui.py is missing in /a0 (if the volume is mounted)
14
- if [ ! -f "$TARGET_DIR/run_ui.py" ]; then
15
-
16
- echo "Cloning and installing A0..."
17
- . "/ins/install_A0.sh" "$@"
18
-
19
- echo "Copying files from $SOURCE_DIR to $TARGET_DIR..."
20
- cp -rn --no-preserve=ownership,mode "$SOURCE_DIR/." "$TARGET_DIR"
21
-
22
- fi
23
-}
24
-
25
-# setup and preload A0
26
-setup_venv
27
-clone_and_install
28
-python /a0/prepare.py --dockerized=true
29
-python /a0/preload.py --dockerized=true
30
-
3
# Loop to restart the Python script when it finishes
4
while true; do
5
34
- setup_venv
35
- clone_repo
6
+ . "/ins/setup_venv.sh" "$@"
7
+ . "/ins/copy_A0.sh" "$@"
8
+
9
+ python /a0/prepare.py --dockerized=true
10
+ python /a0/preload.py --dockerized=true
11
12
echo "Starting A0..."
13
python /a0/run_ui.py \
docker/run/fs/ins/copy_A0.sh
new
+11
@@ -0,0 +1,11 @@
1
+#!/bin/bash
2
+
3
+# Paths
4
+SOURCE_DIR="/git/agent-zero"
5
+TARGET_DIR="/a0"
6
+
7
+# Copy repository files if run_ui.py is missing in /a0 (if the volume is mounted)
8
+if [ ! -f "$TARGET_DIR/run_ui.py" ]; then
9
+ echo "Copying files from $SOURCE_DIR to $TARGET_DIR..."
10
+ cp -rn --no-preserve=ownership,mode "$SOURCE_DIR/." "$TARGET_DIR"
11
+fi
\ No newline at end of file
models.py
+1
-1
@@ -46,9 +46,9 @@ class ModelType(Enum):
46
class ModelProvider(Enum):
47
ANTHROPIC = "Anthropic"
48
DEEPSEEK = "DeepSeek"
49
- HUGGINGFACE = "HuggingFace"
49
GOOGLE = "Google"
50
GROQ = "Groq"
51
+ HUGGINGFACE = "HuggingFace"
52
LMSTUDIO = "LM Studio"
53
MISTRALAI = "Mistral AI"
54
OLLAMA = "Ollama"
prompts/default/agent.system.main.tips.md
+5
-1
@@ -4,7 +4,11 @@
4
reason step-by-step execute tasks
5
avoid repetition ensure progress
6
never assume success
7
-memory refers to knowledge_tool and memorize_tool not own knowledge
7
+memory refers to knowledge_tool and memory tools not own knowledge
8
+
9
+## Files
10
+save files in /root
11
+don't use spaces in file names
12
13
## Instruments
14
prompts/default/agent.system.tool.browser.md
+15
-1
@@ -1,18 +1,32 @@
1
### browser_agent:
2
+
3
subordinate agent controls playwright browser
4
message argument talks to agent give clear instructions credentials task based
5
reset argument spawns new agent
6
do not reset if iterating
7
be precise descriptive like: open google login and end task, log in using ... and end task
8
+when following up start: considering open pages
9
dont use phrase wait for instructions use end task
10
11
+usage:
12
```json
13
{
14
"thoughts": ["I need to log in to..."],
15
"tool_name": "browser_agent",
16
"tool_args": {
17
"message": "Open and log me into...",
18
+ "reset": "true"
19
+ }
20
+}
21
+```
22
+
23
+```json
24
+{
25
+ "thoughts": ["I need to log in to..."],
26
+ "tool_name": "browser_agent",
27
+ "tool_args": {
28
+ "message": "Considering open pages, click...",
29
"reset": "false"
30
}
31
}
18
-```
\ No newline at end of file
32
+```
prompts/default/agent.system.tool.response.md
+1
-3
@@ -2,9 +2,7 @@
2
final answer to user
3
ends task processing use only when done or no task active
4
put result in text arg
5
-use memory for guidance
6
-online sources for current info
7
-verify memory with online
5
+always write full file paths
6
usage:
7
~~~json
8
{
prompts/default/browser_agent.system.md
+3
-1
@@ -2,4 +2,6 @@
2
do not overdo task
3
when told go to website open website and stop
4
do not interact unless told to
5
-waiting for instructions means ending task as done
\ No newline at end of file
5
+waiting for instructions means ending task as done
6
+accept any cookies do not go to cokkie settings
7
+in page_summary respond with one paragraph of main content plus brief overview of page elements
\ No newline at end of file
python/api/chat_export.py
+2
-3
@@ -1,10 +1,9 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
3
from python.helpers import persist_chat
4
5
class ExportChat(ApiHandler):
7
- async def process(self, input: dict, request: Request) -> dict | Response:
6
+ async def process(self, input: Input, request: Request) -> Output:
7
ctxid = input.get("ctxid", "")
8
if not ctxid:
9
raise Exception("No context id provided")
python/api/chat_load.py
+3
-3
@@ -1,10 +1,10 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
+
3
4
from python.helpers import persist_chat
5
6
class LoadChats(ApiHandler):
7
- async def process(self, input: dict, request: Request) -> dict | Response:
7
+ async def process(self, input: Input, request: Request) -> Output:
8
chats = input.get("chats", [])
9
if not chats:
10
raise Exception("No chats provided")
python/api/chat_remove.py
+3
-3
@@ -1,12 +1,12 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
+
3
4
from agent import AgentContext
5
from python.helpers import persist_chat
6
7
8
class RemoveChat(ApiHandler):
9
- async def process(self, input: dict, request: Request) -> dict | Response:
9
+ async def process(self, input: Input, request: Request) -> Output:
10
ctxid = input.get("context", "")
11
12
# context instance - get or create
python/api/chat_reset.py
+4
-3
@@ -1,10 +1,11 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
+
3
4
from python.helpers import persist_chat
5
6
+
7
class Reset(ApiHandler):
7
- async def process(self, input: dict, request: Request) -> dict | Response:
8
+ async def process(self, input: Input, request: Request) -> Output:
9
ctxid = input.get("context", "")
10
11
# context instance - get or create
python/api/ctx_window_get.py
+3
-3
@@ -1,10 +1,10 @@
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
+
3
from python.helpers import tokens
2
-from python.helpers.api import ApiHandler
3
-from flask import Request, Response
4
5
6
class GetCtxWindow(ApiHandler):
7
- async def process(self, input: dict, request: Request) -> dict | Response:
7
+ async def process(self, input: Input, request: Request) -> Output:
8
ctxid = input.get("context", [])
9
context = self.get_context(ctxid)
10
agent = context.streaming_agent or context.agent0
python/api/delete_work_dir_file.py
+24
-15
@@ -1,22 +1,31 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response
1
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
2
+
3
4
from python.helpers.file_browser import FileBrowser
5
-from python.helpers import files
5
+from python.helpers import files, runtime
6
+from python.api import get_work_dir_files
7
8
9
class DeleteWorkDirFile(ApiHandler):
9
- async def process(self, input: dict, request: Request) -> dict | Response:
10
- file_path = input.get('path', '')
11
- current_path = input.get('currentPath', '')
12
-
13
- browser = FileBrowser()
14
-
15
- if browser.delete_file(file_path):
10
+ async def process(self, input: Input, request: Request) -> Output:
11
+ file_path = input.get("path", "")
12
+ if not file_path.startswith("/"):
13
+ file_path = f"/{file_path}"
14
+
15
+ current_path = input.get("currentPath", "")
16
+
17
+ # browser = FileBrowser()
18
+ res = await runtime.call_development_function(delete_file, file_path)
19
+
20
+ if res:
21
# Get updated file list
17
- result = browser.get_files(current_path)
18
- return {
19
- "data": result
20
- }
22
+ # result = browser.get_files(current_path)
23
+ result = await runtime.call_development_function(get_work_dir_files.get_files, current_path)
24
+ return {"data": result}
25
else:
22
- raise Exception("File not found or could not be deleted")
\ No newline at end of file
26
+ raise Exception("File not found or could not be deleted")
27
+
28
+
29
+async def delete_file(file_path: str):
30
+ browser = FileBrowser()
31
+ return browser.delete_file(file_path)
python/api/download_work_dir_file.py
+57
-22
@@ -1,29 +1,64 @@
1
-from python.helpers.api import ApiHandler
2
-from flask import Request, Response, send_file
1
+import base64
2
+from io import BytesIO
3
4
-from python.helpers.file_browser import FileBrowser
5
-from python.helpers import files
4
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
5
+from flask import send_file
6
+
7
+from python.helpers import files, runtime
8
+from python.api import file_info
9
import os
10
11
9
-class DownloadWorkDirFile(ApiHandler):
10
- async def process(self, input: dict, request: Request) -> dict | Response:
11
- file_path = request.args.get("path", "")
12
+class DownloadFile(ApiHandler):
13
+ async def process(self, input: Input, request: Request) -> Output:
14
+ file_path = request.args.get("path", input.get("path", ""))
15
if not file_path:
16
raise ValueError("No file path provided")
17
+ if not file_path.startswith("/"):
18
+ file_path = f"/{file_path}"
19
+
20
+ file = await runtime.call_development_function(
21
+ file_info.get_file_info, file_path
22
+ )
23
+
24
+ if not file["exists"]:
25
+ raise Exception(f"File {file_path} not found")
26
+
27
+ if file["is_dir"]:
28
+ zip_file = await runtime.call_development_function(files.zip_dir, file["abs_path"])
29
+ if runtime.is_development():
30
+ b64 = await runtime.call_development_function(fetch_file, zip_file)
31
+ file_data = BytesIO(base64.b64decode(b64))
32
+ return send_file(
33
+ file_data,
34
+ as_attachment=True,
35
+ download_name=os.path.basename(zip_file),
36
+ )
37
+ else:
38
+ return send_file(
39
+ zip_file,
40
+ as_attachment=True,
41
+ download_name=f"{os.path.basename(file_path)}.zip",
42
+ )
43
+ elif file["is_file"]:
44
+ if runtime.is_development():
45
+ b64 = await runtime.call_development_function(fetch_file, file["abs_path"])
46
+ file_data = BytesIO(base64.b64decode(b64))
47
+ return send_file(
48
+ file_data,
49
+ as_attachment=True,
50
+ download_name=os.path.basename(file_path),
51
+ )
52
+ else:
53
+ return send_file(
54
+ file["abs_path"],
55
+ as_attachment=True,
56
+ download_name=os.path.basename(file["file_name"]),
57
+ )
58
+ raise Exception(f"File {file_path} not found")
59
+
60
15
- browser = FileBrowser()
16
-
17
- full_path = browser.get_full_path(file_path, True)
18
- if os.path.isdir(full_path):
19
- zip_file = browser.zip_dir(full_path)
20
- return send_file(
21
- zip_file,
22
- as_attachment=True,
23
- download_name=f"{os.path.basename(file_path)}.zip",
24
- )
25
- if full_path:
26
- return send_file(
27
- full_path, as_attachment=True, download_name=os.path.basename(file_path)
28
- )
29
- raise Exception("File not found")
61
+async def fetch_file(path):
62
+ with open(path, "rb") as file:
63
+ file_content = file.read()
64
+ return base64.b64encode(file_content).decode("utf-8")
python/api/file_info.py
new
+51
@@ -0,0 +1,51 @@
1
+import os
2
+from python.helpers.api import ApiHandler, Input, Output, Request, Response
3
+from python.helpers import files, runtime
4
+from typing import TypedDict
5
+
6
+class FileInfoApi(ApiHandler):
7
+ async def process(self, input: Input, request: Request) -> Output:
8
+ path = input.get("path", "")
9
+ info = await runtime.call_development_function(get_file_info, path)
10
+ return info
11
+
12
+class FileInfo(TypedDict):
13
+ input_path: str
14
+ abs_path: str
15
+ exists: bool
16
+ is_dir: bool
17
+ is_file: bool
18
+ is_link: bool
19
+ size: int
20
+ modified: float
21
+ created: float
22
+ permissions: int
23
+ dir_path: str
24
+ file_name: str
25
+ file_ext: str
26
+ message: str
27
+
28
+async def get_file_info(path: str) -> FileInfo:
29
+ abs_path = files.get_abs_path(path)
30
+ exists = os.path.exists(abs_path)
31
+ message = ""
32
+
33
+ if not exists:
34
+ message = f"File {path} not found."
35
+
36
+ return {
37
+ "input_path": path,
38
+ "abs_path": abs_path,
39
+ "exists": exists,
40
+ "is_dir": os.path.isdir(abs_path) if exists else False,
41
+ "is_file": os.path.isfile(abs_path) if exists else False,
42
+ "is_link": os.path.islink(abs_path) if exists else False,
43
+ "size": os.path.getsize(abs_path) if exists else 0,
44
+ "modified": os.path.getmtime(abs_path) if exists else 0,
45
+ "created": os.path.getctime(abs_path) if exists else 0,
46
+ "permissions": os.stat(abs_path).st_mode if exists else 0,
47
+ "dir_path": os.path.dirname(abs_path),
48
+ "file_name": os.path.basename(abs_path),
49
+ "file_ext": os.path.splitext(abs_path)[1],
50
+ "message": message
51
+ }
\ No newline at end of file
python/api/get_work_dir_files.py
+13
-6
@@ -9,11 +9,18 @@ class GetWorkDirFiles(ApiHandler):
9
async def process(self, input: dict, request: Request) -> dict | Response:
10
current_path = request.args.get("path", "")
11
if current_path == "$WORK_DIR":
12
- if runtime.is_development():
13
- current_path = "work_dir"
14
- else:
15
- current_path = "root"
16
- browser = FileBrowser()
17
- result = browser.get_files(current_path)
12
+ # if runtime.is_development():
13
+ # current_path = "work_dir"
14
+ # else:
15
+ # current_path = "root"
16
+ current_path = "root"
17
+
18
+ # browser = FileBrowser()
19
+ # result = browser.get_files(current_path)
20
+ result = await runtime.call_development_function(get_files, current_path)
21
22
return {"data": result}
23
+
24
+async def get_files(path):
25
+ browser = FileBrowser()
26
+ return browser.get_files(path)
\ No newline at end of file
python/api/health.py
+9
-2
@@ -1,10 +1,17 @@
1
from python.helpers.api import ApiHandler
2
from flask import Request, Response
3
+from python.helpers import errors
4
5
from python.helpers import git
6
7
class HealthCheck(ApiHandler):
8
9
async def process(self, input: dict, request: Request) -> dict | Response:
9
- gitinfo = git.get_git_info()
10
- return {"gitinfo": gitinfo}
10
+ gitinfo = None
11
+ error = None
12
+ try:
13
+ gitinfo = git.get_git_info()
14
+ except Exception as e:
15
+ error = errors.error_text(e)
16
+
17
+ return {"gitinfo": gitinfo, "error": error}
python/api/upload_work_dir_files.py
+49
-16
@@ -1,33 +1,66 @@
1
+import base64
2
+from werkzeug.datastructures import FileStorage
3
from python.helpers.api import ApiHandler
4
from flask import Request, Response, send_file
5
6
from python.helpers.file_browser import FileBrowser
5
-from python.helpers import files
7
+from python.helpers import files, runtime
8
+from python.api import get_work_dir_files
9
import os
10
11
9
-
10
-
12
class UploadWorkDirFiles(ApiHandler):
13
async def process(self, input: dict, request: Request) -> dict | Response:
14
if "files[]" not in request.files:
15
raise Exception("No files uploaded")
16
16
- current_path = request.form.get('path', '')
17
- uploaded_files = request.files.getlist("files[]")
18
-
19
- browser = FileBrowser()
20
-
21
- successful, failed = browser.save_files(uploaded_files, current_path)
22
-
17
+ current_path = request.form.get("path", "")
18
+ uploaded_files = request.files.getlist("files[]")
19
+
20
+ # browser = FileBrowser()
21
+ # successful, failed = browser.save_files(uploaded_files, current_path)
22
+
23
+ successful, failed = await upload_files(uploaded_files, current_path)
24
+
25
if not successful and failed:
26
raise Exception("All uploads failed")
25
-
26
- result = browser.get_files(current_path)
27
-
27
+
28
+ # result = browser.get_files(current_path)
29
+ result = await runtime.call_development_function(get_work_dir_files.get_files, current_path)
30
+
31
return {
29
- "message": "Files uploaded successfully" if not failed else "Some files failed to upload",
32
+ "message": (
33
+ "Files uploaded successfully"
34
+ if not failed
35
+ else "Some files failed to upload"
36
+ ),
37
"data": result,
38
"successful": successful,
32
- "failed": failed
33
- }
\ No newline at end of file
39
+ "failed": failed,
40
+ }
41
+
42
+
43
+async def upload_files(uploaded_files: list[FileStorage], current_path: str):
44
+ if runtime.is_development():
45
+ successful = []
46
+ failed = []
47
+ for file in uploaded_files:
48
+ file_content = file.stream.read()
49
+ base64_content = base64.b64encode(file_content).decode("utf-8")
50
+ if await runtime.call_development_function(
51
+ upload_file, current_path, file.filename, base64_content
52
+ ):
53
+ successful.append(file.filename)
54
+ else:
55
+ failed.append(file.filename)
56
+ else:
57
+ browser = FileBrowser()
58
+ successful, failed = browser.save_files(uploaded_files, current_path)
59
+
60
+ return successful, failed
61
+
62
+
63
+async def upload_file(current_path: str, filename: str, base64_content: str):
64
+ browser = FileBrowser()
65
+ return browser.save_file_b64(current_path, filename, base64_content)
66
+
python/helpers/api.py
+10
-6
@@ -1,6 +1,8 @@
1
from abc import abstractmethod
2
import json
3
import threading
4
+from typing import Union, TypedDict, Dict, Any
5
+from attr import dataclass
6
from flask import Request, Response, jsonify, Flask
7
from agent import AgentContext
8
from initialize import initialize
@@ -9,6 +11,8 @@ from python.helpers.errors import format_error
11
from werkzeug.serving import make_server
12
13
14
+Input = dict
15
+Output = Union[Dict[str, Any], Response, TypedDict]
16
17
18
class ApiHandler:
@@ -17,7 +21,7 @@ class ApiHandler:
21
self.thread_lock = thread_lock
22
23
@abstractmethod
20
- async def process(self, input: dict, request: Request) -> dict | Response:
24
+ async def process(self, input: Input, request: Request) -> Output:
25
pass
26
27
async def handle_request(self, request: Request) -> Response:
@@ -31,12 +35,14 @@ class ApiHandler:
35
# process via handler
36
output = await self.process(input, request)
37
34
- # return output based on type
38
+ # return output based on type
39
if isinstance(output, Response):
40
return output
41
else:
42
response_json = json.dumps(output)
39
- return Response(response=response_json, status=200, mimetype="application/json")
43
+ return Response(
44
+ response=response_json, status=200, mimetype="application/json"
45
+ )
46
47
# return exceptions with 500
48
except Exception as e:
@@ -44,8 +50,6 @@ class ApiHandler:
50
PrintStyle.error(error)
51
return Response(response=error, status=500, mimetype="text/plain")
52
47
-
48
-
53
# get context to run agent zero in
54
def get_context(self, ctxid: str):
55
with self.thread_lock:
@@ -57,4 +61,4 @@ class ApiHandler:
61
got = AgentContext.get(ctxid)
62
if got:
63
return got
60
- return AgentContext(config=initialize(), id=ctxid)
\ No newline at end of file
64
+ return AgentContext(config=initialize(), id=ctxid)
python/helpers/browser_use.py
+2
-1
@@ -1,3 +1,4 @@
1
from python.helpers import dotenv
2
dotenv.save_dotenv_value("ANONYMIZED_TELEMETRY", "false")
3
-import browser_use
\ No newline at end of file
3
+import browser_use
4
+import browser_use.utils
\ No newline at end of file
python/helpers/defer.py
+3
-4
@@ -1,12 +1,11 @@
1
import asyncio
2
from dataclasses import dataclass
3
import threading
4
-from concurrent.futures import Future, ThreadPoolExecutor
5
-from typing import Any, Callable, Optional, Coroutine, TypeVar, Union, Awaitable
4
+from concurrent.futures import Future
5
+from typing import Any, Callable, Optional, Coroutine, TypeVar, Awaitable
6
7
T = TypeVar("T")
8
9
-
9
class EventLoopThread:
10
_instances = {}
11
_lock = threading.Lock()
@@ -155,7 +154,7 @@ class DeferredTask:
154
return self._future and not self._future.done() # type: ignore
155
156
def restart(self, terminate_thread: bool = False) -> None:
158
- self.kill()
157
+ self.kill(terminate_thread=terminate_thread)
158
self._start_task()
159
160
def add_child_task(
python/helpers/file_browser.py
+23
-18
@@ -2,6 +2,7 @@ import os
2
from pathlib import Path
3
import shutil
4
import tempfile
5
+import base64
6
from typing import Dict, List, Tuple, Optional, Any
7
import zipfile
8
from werkzeug.utils import secure_filename
@@ -20,10 +21,11 @@ class FileBrowser:
21
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
22
23
def __init__(self):
23
- if runtime.is_development():
24
- base_dir = files.get_base_dir()
25
- else:
26
- base_dir = "/"
24
+ # if runtime.is_development():
25
+ # base_dir = files.get_base_dir()
26
+ # else:
27
+ # base_dir = "/"
28
+ base_dir = "/"
29
self.base_dir = Path(base_dir)
30
31
def _check_file_size(self, file) -> bool:
@@ -35,6 +37,22 @@ class FileBrowser:
37
except (AttributeError, IOError):
38
return False
39
40
+ def save_file_b64(self, current_path: str, filename:str, base64_content: str):
41
+ try:
42
+ # Resolve the target directory path
43
+ target_file = (self.base_dir / current_path / filename).resolve()
44
+ if not str(target_file).startswith(str(self.base_dir)):
45
+ raise ValueError("Invalid target directory")
46
+
47
+ os.makedirs(target_file.parent, exist_ok=True)
48
+ # Save file
49
+ with open(target_file, "wb") as file:
50
+ file.write(base64.b64decode(base64_content))
51
+ return True
52
+ except Exception as e:
53
+ PrintStyle.error(f"Error saving file {filename}: {e}")
54
+ return False
55
+
56
def save_files(self, files: List, current_path: str = "") -> Tuple[List[str], List[str]]:
57
"""Save uploaded files and return successful and failed filenames"""
58
successful = []
@@ -176,17 +194,4 @@ class FileBrowser:
194
for file_type, extensions in self.ALLOWED_EXTENSIONS.items():
195
if ext in extensions:
196
return file_type
179
- return 'unknown'
180
-
181
- def zip_dir(self, dir_path: str):
182
- full_path = self.get_full_path(dir_path, allow_dir=True)
183
- zip_file_path = tempfile.NamedTemporaryFile(suffix='.zip', delete=False).name
184
- base_name = os.path.basename(full_path)
185
- with zipfile.ZipFile(zip_file_path, "w", compression=zipfile.ZIP_DEFLATED) as zip:
186
- for root, _, files in os.walk(full_path):
187
- for file in files:
188
- file_path = os.path.join(root, file)
189
- rel_path = os.path.relpath(file_path, full_path)
190
- zip.write(file_path, os.path.join(base_name, rel_path))
191
- return zip_file_path
192
-
\ No newline at end of file
197
+ return 'unknown'
\ No newline at end of file
python/helpers/files.py
+38
@@ -3,6 +3,9 @@ import json
3
import os, re
4
5
import re
6
+import shutil
7
+import tempfile
8
+import zipfile
9
10
11
def parse_file(_relative_path, _backup_dirs=None, _encoding="utf-8", **kwargs):
@@ -50,6 +53,7 @@ def replace_placeholders_text(_content: str, **kwargs):
53
_content = _content.replace(placeholder, strval)
54
return _content
55
56
+
57
def replace_placeholders_json(_content: str, **kwargs):
58
# Replace placeholders with values from kwargs
59
for key, value in kwargs.items():
@@ -58,6 +62,7 @@ def replace_placeholders_json(_content: str, **kwargs):
62
_content = _content.replace(placeholder, strval)
63
return _content
64
65
+
66
def replace_placeholders_dict(_content: dict, **kwargs):
67
def replace_value(value):
68
if isinstance(value, str):
@@ -162,18 +167,26 @@ def write_file(relative_path: str, content: str, encoding: str = "utf-8"):
167
with open(abs_path, "w", encoding=encoding) as f:
168
f.write(content)
169
170
+
171
def write_file_bin(relative_path: str, content: bytes):
172
abs_path = get_abs_path(relative_path)
173
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
174
with open(abs_path, "wb") as f:
175
f.write(content)
176
177
+
178
def delete_file(relative_path: str):
179
abs_path = get_abs_path(relative_path)
180
if os.path.exists(abs_path):
181
os.remove(abs_path)
182
183
184
+def delete_dir(relative_path: str):
185
+ abs_path = get_abs_path(relative_path)
186
+ if os.path.exists(abs_path):
187
+ shutil.rmtree(abs_path)
188
+
189
+
190
def list_files(relative_path: str, filter: str = "*"):
191
abs_path = get_abs_path(relative_path)
192
if not os.path.exists(abs_path):
@@ -181,6 +194,11 @@ def list_files(relative_path: str, filter: str = "*"):
194
return [file for file in os.listdir(abs_path) if fnmatch(file, filter)]
195
196
197
+def make_dirs(relative_path: str):
198
+ abs_path = get_abs_path(relative_path)
199
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
200
+
201
+
202
def get_abs_path(*relative_paths):
203
return os.path.join(get_base_dir(), *relative_paths)
204
@@ -207,3 +225,23 @@ def get_subdirectories(relative_path: str, include: str = "*", exclude=None):
225
and fnmatch(subdir, include)
226
and (exclude is None or not fnmatch(subdir, exclude))
227
]
228
+
229
+
230
+def zip_dir(dir_path: str):
231
+ full_path = get_abs_path(dir_path)
232
+ zip_file_path = tempfile.NamedTemporaryFile(suffix=".zip", delete=False).name
233
+ base_name = os.path.basename(full_path)
234
+ with zipfile.ZipFile(zip_file_path, "w", compression=zipfile.ZIP_DEFLATED) as zip:
235
+ for root, _, files in os.walk(full_path):
236
+ for file in files:
237
+ file_path = os.path.join(root, file)
238
+ rel_path = os.path.relpath(file_path, full_path)
239
+ zip.write(file_path, os.path.join(base_name, rel_path))
240
+ return zip_file_path
241
+
242
+
243
+def move_file(relative_path: str, new_path: str):
244
+ abs_path = get_abs_path(relative_path)
245
+ new_abs_path = get_abs_path(new_path)
246
+ os.makedirs(os.path.dirname(new_abs_path), exist_ok=True)
247
+ os.rename(abs_path, new_abs_path)
python/helpers/git.py
+2
@@ -30,6 +30,8 @@ def get_git_info():
30
tag_split = tag.split('-')
31
if len(tag_split) >= 3:
32
short_tag = "-".join(tag_split[:-1])
33
+ else:
34
+ short_tag = tag
35
except:
36
tag = ""
37
python/helpers/persist_chat.py
+37
-14
@@ -1,7 +1,7 @@
1
from collections import OrderedDict
2
from typing import Any
3
import uuid
4
-from agent import Agent, AgentConfig, AgentContext, HumanMessage, AIMessage
4
+from agent import Agent, AgentConfig, AgentContext
5
from python.helpers import files, history
6
import json
7
from initialize import initialize
@@ -10,27 +10,53 @@ from python.helpers.log import Log, LogItem
10
11
CHATS_FOLDER = "tmp/chats"
12
LOG_SIZE = 1000
13
+CHAT_FILE_NAME = "chat.json"
14
15
16
+def get_chat_folder_path(ctxid: str):
17
+ return files.get_abs_path(CHATS_FOLDER, ctxid)
18
+
19
def save_tmp_chat(context: AgentContext):
16
- relative_path = _get_file_path(context.id)
20
+ path = _get_chat_file_path(context.id)
21
+ files.make_dirs(path)
22
data = _serialize_context(context)
23
js = _safe_json_serialize(data, ensure_ascii=False)
19
- files.write_file(relative_path, js)
24
+ files.write_file(path, js)
25
26
27
def load_tmp_chats():
23
- json_files = files.list_files("tmp/chats", "*.json")
28
+ _convert_v080_chats()
29
+ folders = files.list_files("tmp/chats/", "*")
30
+ json_files = []
31
+ for folder in folders:
32
+ json_files.append(_get_chat_file_path(folder))
33
+
34
ctxids = []
35
for file in json_files:
26
- path = files.get_abs_path(CHATS_FOLDER, file)
27
- js = files.read_file(path)
28
- data = json.loads(js)
29
- ctx = _deserialize_context(data)
30
- ctxids.append(ctx.id)
36
+ try:
37
+ js = files.read_file(file)
38
+ data = json.loads(js)
39
+ ctx = _deserialize_context(data)
40
+ ctxids.append(ctx.id)
41
+ except Exception as e:
42
+ print(f"Error loading chat {file}: {e}")
43
return ctxids
44
45
46
+def _get_chat_file_path(ctxid: str):
47
+ return files.get_abs_path(CHATS_FOLDER, ctxid, CHAT_FILE_NAME)
48
+
49
+
50
+def _convert_v080_chats():
51
+ json_files = files.list_files("tmp/chats", "*.json")
52
+ for file in json_files:
53
+ path = files.get_abs_path(CHATS_FOLDER, file)
54
+ name = file.rstrip(".json")
55
+ fold = files.get_abs_path(CHATS_FOLDER, name)
56
+ new = _get_chat_file_path(name)
57
+ files.move_file(path, new)
58
+
59
+
60
def load_json_chats(jsons: list[str]):
61
ctxids = []
62
for js in jsons:
@@ -49,11 +75,8 @@ def export_json_chat(context: AgentContext):
75
76
77
def remove_chat(ctxid):
52
- files.delete_file(_get_file_path(ctxid))
53
-
78
+ files.delete_dir(get_chat_folder_path(ctxid))
79
55
-def _get_file_path(ctxid: str):
56
- return f"{CHATS_FOLDER}/{ctxid}.json"
80
81
82
def _serialize_context(context: AgentContext):
@@ -174,7 +197,7 @@ def _deserialize_log(data: dict[str, Any]) -> "Log":
197
log.logs.append(
198
LogItem(
199
log=log, # restore the log reference
177
- no=i, #item_data["no"],
200
+ no=i, # item_data["no"],
201
type=item_data["type"],
202
heading=item_data.get("heading", ""),
203
content=item_data.get("content", ""),
python/helpers/rag.py
new
+60
@@ -0,0 +1,60 @@
1
+from typing import List
2
+
3
+from langchain_core.documents import Document
4
+from python.helpers import files
5
+
6
+from langchain_community.document_loaders import (
7
+ CSVLoader,
8
+ JSONLoader,
9
+ PyPDFLoader,
10
+ TextLoader,
11
+ UnstructuredHTMLLoader,
12
+ UnstructuredMarkdownLoader,
13
+)
14
+
15
+# def extract_file(path: str) -> List[Document]:
16
+# pass # TODO finish implementing
17
+
18
+def extract_text(content: bytes, chunk_size: int = 128) -> List[str]:
19
+ result = []
20
+
21
+ def is_binary_chunk(chunk: bytes) -> bool:
22
+ # Check for high concentration of control chars
23
+ try:
24
+ text = chunk.decode("utf-8", errors="ignore")
25
+ control_chars = sum(1 for c in text if ord(c) < 32 and c not in "\n\r\t")
26
+ return control_chars / len(text) > 0.3
27
+ except UnicodeDecodeError:
28
+ return True
29
+
30
+ # Process the content in overlapping chunks to handle boundaries
31
+ pos = 0
32
+ while pos < len(content):
33
+ # Get current chunk with overlap
34
+ chunk_end = min(pos + chunk_size, len(content))
35
+
36
+ # Add overlap to catch word boundaries, unless at end of content
37
+ if chunk_end < len(content):
38
+ # Look ahead for next newline or space to avoid splitting words
39
+ for i in range(chunk_end, min(chunk_end + 100, len(content))):
40
+ if content[i : i + 1] in [b" ", b"\n", b"\r"]:
41
+ chunk_end = i + 1
42
+ break
43
+
44
+ chunk = content[pos:chunk_end]
45
+
46
+ if is_binary_chunk(chunk):
47
+ if not result or result[-1] != "[BINARY]":
48
+ result.append("[BINARY]")
49
+ else:
50
+ try:
51
+ text = chunk.decode("utf-8", errors="ignore").strip()
52
+ if text: # Only add non-empty text chunks
53
+ result.append(text)
54
+ except UnicodeDecodeError:
55
+ if not result or result[-1] != "[BINARY]":
56
+ result.append("[BINARY]")
57
+
58
+ pos = chunk_end
59
+
60
+ return result
python/helpers/runtime.py
+14
-4
@@ -1,8 +1,11 @@
1
import argparse
2
import inspect
3
-from typing import Any, Callable
3
+from typing import TypeVar, Callable, Awaitable, Union, overload, cast
4
from python.helpers import dotenv, rfc, settings
5
6
+T = TypeVar('T')
7
+R = TypeVar('R')
8
+
9
parser = argparse.ArgumentParser()
10
args = {}
11
dockerman = None
@@ -52,11 +55,17 @@ def get_local_url():
55
return "host.docker.internal"
56
return "127.0.0.1"
57
55
-async def call_development_function(func: Callable, *args, **kwargs):
58
+@overload
59
+async def call_development_function(func: Callable[..., Awaitable[T]], *args, **kwargs) -> T: ...
60
+
61
+@overload
62
+async def call_development_function(func: Callable[..., T], *args, **kwargs) -> T: ...
63
+
64
+async def call_development_function(func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs) -> T:
65
if is_development():
66
url = _get_rfc_url()
67
password = _get_rfc_password()
59
- return await rfc.call_rfc(
68
+ result = await rfc.call_rfc(
69
url=url,
70
password=password,
71
module=func.__module__,
@@ -64,11 +73,12 @@ async def call_development_function(func: Callable, *args, **kwargs):
73
args=list(args),
74
kwargs=kwargs,
75
)
76
+ return cast(T, result)
77
else:
78
if inspect.iscoroutinefunction(func):
79
return await func(*args, **kwargs)
80
else:
71
- return func(*args, **kwargs)
81
+ return func(*args, **kwargs) # type: ignore
82
83
84
async def handle_rfc(rfc_call: rfc.RFCCall):
python/helpers/settings.py
+20
-20
@@ -381,7 +381,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
381
{
382
"id": "browser_model_provider",
383
"title": "Web Browser model provider",
384
- "description": "Select provider for web browser model used by browser-use framework",
384
+ "description": "Select provider for web browser model used by <a href='https://github.com/browser-use/browser-use' target='_blank'>browser-use</a> framework",
385
"type": "select",
386
"value": settings["browser_model_provider"],
387
"options": [{"value": p.name, "label": p.value} for p in ModelProvider],
@@ -433,28 +433,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
433
browser_model_section: SettingsSection = {
434
"id": "browser_model",
435
"title": "Web Browser Model",
436
- "description": "Settings for the web browser model used by browser-use framework.",
436
+ "description": "Settings for the web browser model. Agent Zero uses <a href='https://github.com/browser-use/browser-use' target='_blank'>browser-use</a> agentic framework to handle web interactions.",
437
"fields": browser_model_fields,
438
}
439
440
- # Memory settings section
441
- memory_fields: list[SettingsField] = []
442
- memory_fields.append(
443
- {
444
- "id": "memory_settings",
445
- "title": "Memory Settings",
446
- "description": "<settings for memory>",
447
- "type": "text",
448
- "value": "",
449
- }
450
- )
440
+ # # Memory settings section
441
+ # memory_fields: list[SettingsField] = []
442
+ # memory_fields.append(
443
+ # {
444
+ # "id": "memory_settings",
445
+ # "title": "Memory Settings",
446
+ # "description": "<settings for memory>",
447
+ # "type": "text",
448
+ # "value": "",
449
+ # }
450
+ # )
451
452
- memory_section: SettingsSection = {
453
- "id": "memory",
454
- "title": "Memory Settings",
455
- "description": "<settings for memory management here>",
456
- "fields": memory_fields,
457
- }
452
+ # memory_section: SettingsSection = {
453
+ # "id": "memory",
454
+ # "title": "Memory Settings",
455
+ # "description": "<settings for memory management here>",
456
+ # "fields": memory_fields,
457
+ # }
458
459
# basic auth section
460
auth_fields: list[SettingsField] = []
@@ -728,7 +728,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
728
util_model_section,
729
embed_model_section,
730
browser_model_section,
731
- memory_section,
731
+ # memory_section,
732
stt_section,
733
api_keys_section,
734
auth_section,
python/helpers/strings.py
+3
-1
@@ -2,6 +2,8 @@ import re
2
import sys
3
import time
4
5
+from python.helpers import files
6
+
7
def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
8
deviation_threshold: int = 5,
9
deviation_reset: int = 5,
@@ -113,4 +115,4 @@ def dict_to_text(d: dict) -> str:
115
parts.append(f"{value}")
116
parts.append("") # Add empty line between entries
117
116
- return "\n".join(parts).rstrip() # rstrip to remove trailing newline
118
+ return "\n".join(parts).rstrip() # rstrip to remove trailing newline
\ No newline at end of file
python/helpers/vector_db.py
new
+116
@@ -0,0 +1,116 @@
1
+from typing import Any, List, Sequence
2
+import uuid
3
+from langchain_community.vectorstores import FAISS
4
+import faiss
5
+from langchain_core.documents import Document
6
+from langchain.storage import InMemoryByteStore
7
+from langchain_community.docstore.in_memory import InMemoryDocstore
8
+from langchain_community.vectorstores.utils import (
9
+ DistanceStrategy,
10
+)
11
+from langchain.embeddings import CacheBackedEmbeddings
12
+
13
+from agent import Agent
14
+
15
+
16
+class MyFaiss(FAISS):
17
+ # override aget_by_ids
18
+ def get_by_ids(self, ids: Sequence[str], /) -> List[Document]:
19
+ # return all self.docstore._dict[id] in ids
20
+ return [self.docstore._dict[id] for id in (ids if isinstance(ids, list) else [ids]) if id in self.docstore._dict] # type: ignore
21
+
22
+ async def aget_by_ids(self, ids: Sequence[str], /) -> List[Document]:
23
+ return self.get_by_ids(ids)
24
+
25
+
26
+class VectorDB:
27
+ def __init__(self, agent: Agent):
28
+ self.agent = agent
29
+ self.store = InMemoryByteStore()
30
+ self.model = agent.get_embedding_model()
31
+
32
+ self.embedder = CacheBackedEmbeddings.from_bytes_store(
33
+ self.model,
34
+ self.store,
35
+ namespace=getattr(
36
+ self.model,
37
+ "model",
38
+ getattr(self.model, "model_name", "default"),
39
+ ),
40
+ )
41
+
42
+ self.index = faiss.IndexFlatIP(len(self.embedder.embed_query("example")))
43
+
44
+ self.db = MyFaiss(
45
+ embedding_function=self.embedder,
46
+ index=self.index,
47
+ docstore=InMemoryDocstore(),
48
+ index_to_docstore_id={},
49
+ distance_strategy=DistanceStrategy.COSINE,
50
+ # normalize_L2=True,
51
+ relevance_score_fn=cosine_normalizer,
52
+ )
53
+
54
+ async def search_similarity_threshold(
55
+ self, query: str, limit: int, threshold: float, filter: str = ""
56
+ ):
57
+ comparator = get_comparator(filter) if filter else None
58
+
59
+ # rate limiter
60
+ await self.agent.rate_limiter(
61
+ model_config=self.agent.config.embeddings_model, input=query
62
+ )
63
+
64
+ return await self.db.asearch(
65
+ query,
66
+ search_type="similarity_score_threshold",
67
+ k=limit,
68
+ score_threshold=threshold,
69
+ filter=comparator,
70
+ )
71
+
72
+ async def insert_documents(self, docs: list[Document]):
73
+ ids = [str(uuid.uuid4()) for _ in range(len(docs))]
74
+
75
+ if ids:
76
+ for doc, id in zip(docs, ids):
77
+ doc.metadata["id"] = id # add ids to documents metadata
78
+
79
+ # rate limiter
80
+ docs_txt = "".join(format_docs_plain(docs))
81
+ await self.agent.rate_limiter(
82
+ model_config=self.agent.config.embeddings_model, input=docs_txt
83
+ )
84
+
85
+ self.db.add_documents(documents=docs, ids=ids)
86
+ return ids
87
+
88
+
89
+def format_docs_plain(docs: list[Document]) -> list[str]:
90
+ result = []
91
+ for doc in docs:
92
+ text = ""
93
+ for k, v in doc.metadata.items():
94
+ text += f"{k}: {v}\n"
95
+ text += f"Content: {doc.page_content}"
96
+ result.append(text)
97
+ return result
98
+
99
+
100
+def cosine_normalizer(val: float) -> float:
101
+ res = (1 + val) / 2
102
+ res = max(
103
+ 0, min(1, res)
104
+ ) # float precision can cause values like 1.0000000596046448
105
+ return res
106
+
107
+
108
+def get_comparator(condition: str):
109
+ def comparator(data: dict[str, Any]):
110
+ try:
111
+ return eval(condition, {}, data)
112
+ except Exception as e:
113
+ # PrintStyle.error(f"Error evaluating condition: {e}")
114
+ return False
115
+
116
+ return comparator
python/tools/browser_agent.py
+23
-6
@@ -5,7 +5,7 @@ from agent import Agent
5
6
import models
7
from python.helpers.tool import Tool, Response
8
-from python.helpers import dirty_json, files, rfc_exchange, defer, strings
8
+from python.helpers import dirty_json, files, rfc_exchange, defer, strings, persist_chat
9
from python.helpers.print_style import PrintStyle
10
from python.helpers.browser_use import browser_use
11
from pydantic import BaseModel
@@ -57,9 +57,7 @@ class State:
57
thread_name="BrowserAgent" + self.agent.context.id
58
)
59
if self.agent.context.task:
60
- self.agent.context.task.add_child_task(
61
- self.task, terminate_thread=True
62
- )
60
+ self.agent.context.task.add_child_task(self.task, terminate_thread=True)
61
self.task.start_task(self._run_task, task)
62
return self.task
63
@@ -114,7 +112,7 @@ class State:
112
self.use_agent = browser_use.Agent(
113
task=task,
114
browser_context=self.context,
117
- llm=self.agent.get_utility_model(),
115
+ llm=model,
116
use_vision=self.agent.config.browser_model.vision,
117
system_prompt_class=CustomSystemPrompt,
118
controller=controller,
@@ -172,15 +170,28 @@ class BrowserAgent(Tool):
170
await self.prepare_state()
171
172
result = {}
173
+ agent = self.agent
174
ua = self.state.use_agent
175
page = await self.state.get_page()
176
+ ctx = self.state.context
177
178
if ua and page:
179
try:
180
181
async def _get_update():
182
+
183
+ await agent.wait_if_paused()
184
+
185
log = []
186
187
+ # dom_service = browser_use.DomService(page)
188
+ # dom_state = await browser_use.utils.time_execution_sync('get_clickable_elements')(
189
+ # dom_service.get_clickable_elements
190
+ # )()
191
+ # elements = dom_state.element_tree
192
+ # selector_map = dom_state.selector_map
193
+ # el_text = elements.clickable_elements_to_string()
194
+
195
for message in ua.message_manager.get_messages():
196
if message.type == "system":
197
continue
@@ -202,7 +213,13 @@ class BrowserAgent(Tool):
213
log.append("FW:" + part)
214
result["log"] = log
215
205
- path = files.get_abs_path("tmp/browser", f"{self.guid}.png")
216
+ path = files.get_abs_path(
217
+ persist_chat.get_chat_folder_path(agent.context.id),
218
+ "browser",
219
+ "screenshots",
220
+ f"{self.guid}.png",
221
+ )
222
+ files.make_dirs(path)
223
await page.screenshot(path=path, full_page=False, timeout=3000)
224
result["screenshot"] = f"img://{path}&t={str(time.time())}"
225
python/tools/memory_save.py
+1
-1
@@ -14,7 +14,7 @@ class MemorySave(Tool):
14
metadata = {"area": area, **kwargs}
15
16
db = await Memory.get(self.agent)
17
- id = db.insert_text(text, metadata)
17
+ id = await db.insert_text(text, metadata)
18
19
result = self.agent.read_prompt("fw.memory_saved.md", memory_id=id)
20
return Response(message=result, break_loop=False)
run_ui.py
+34
-12
@@ -46,13 +46,20 @@ def requires_auth(f):
46
@app.route("/", methods=["GET"])
47
@requires_auth
48
async def serve_index():
49
- gitinfo = git.get_git_info()
49
+ gitinfo = None
50
+ try:
51
+ gitinfo = git.get_git_info()
52
+ except Exception as e:
53
+ gitinfo = {
54
+ "version": "unknown",
55
+ "commit_time": "unknown",
56
+ }
57
return files.read_file(
58
"./webui/index.html",
59
version_no=gitinfo["version"],
60
version_time=gitinfo["commit_time"],
61
)
55
-
62
+
63
64
def run():
65
PrintStyle().print("Initializing framework...")
@@ -66,15 +73,22 @@ def run():
73
pass # Override to suppress request logging
74
75
# Get configuration from environment
69
- port = runtime.get_arg("port") or int(dotenv.get_dotenv_value("WEB_UI_PORT", 0)) or 5000
70
- host = runtime.get_arg("host") or dotenv.get_dotenv_value("WEB_UI_HOST") or "localhost"
71
- use_cloudflare = (runtime.get_arg("cloudflare_tunnel")
72
- or dotenv.get_dotenv_value("USE_CLOUDFLARE", "false").lower()) == "true"
73
-
76
+ port = (
77
+ runtime.get_arg("port")
78
+ or int(dotenv.get_dotenv_value("WEB_UI_PORT", 0))
79
+ or 5000
80
+ )
81
+ host = (
82
+ runtime.get_arg("host") or dotenv.get_dotenv_value("WEB_UI_HOST") or "localhost"
83
+ )
84
+ use_cloudflare = (
85
+ runtime.get_arg("cloudflare_tunnel")
86
+ or dotenv.get_dotenv_value("USE_CLOUDFLARE", "false").lower()
87
+ ) == "true"
88
89
tunnel = None
90
77
- try:
91
+ try:
92
# Initialize and start Cloudflare tunnel if enabled
93
if use_cloudflare and port:
94
try:
@@ -95,26 +109,34 @@ def run():
109
def register_api_handler(app, handler: type[ApiHandler]):
110
name = handler.__module__.split(".")[-1]
111
instance = handler(app, lock)
112
+
113
@requires_auth
114
async def handle_request():
115
return await instance.handle_request(request=request)
116
+
117
app.add_url_rule(
118
f"/{name}",
119
f"/{name}",
120
handle_request,
121
methods=["POST", "GET"],
122
)
107
-
123
+
124
# initialize and register API handlers
125
handlers = load_classes_from_folder("python/api", "*.py", ApiHandler)
126
for handler in handlers:
127
register_api_handler(app, handler)
112
-
128
+
129
try:
114
- server = make_server(host=host, port=port, app=app, request_handler=NoRequestLoggingWSGIRequestHandler, threaded=True)
130
+ server = make_server(
131
+ host=host,
132
+ port=port,
133
+ app=app,
134
+ request_handler=NoRequestLoggingWSGIRequestHandler,
135
+ threaded=True,
136
+ )
137
process.set_server(server)
138
server.log_startup()
117
- server.serve_forever()
139
+ server.serve_forever()
140
# Run Flask app
141
# app.run(
142
# request_handler=NoRequestLoggingWSGIRequestHandler, port=port, host=host
web_test.py
deleted
-84
@@ -1,84 +0,0 @@
1
-from browser_use import Agent, Browser, BrowserConfig, Controller, ActionResult
2
-from pydantic import BaseModel
3
-import asyncio
4
-
5
-import playwright
6
-import models
7
-from python.helpers import dotenv, files
8
-from playwright.async_api import async_playwright
9
-
10
-
11
-async def main():
12
-
13
- dotenv.load_dotenv()
14
- model = models.get_openai_chat("gpt-4o-mini")
15
-
16
- # Initialize controller first
17
- controller = Controller()
18
-
19
- # @controller.action("Ask user for information")
20
- # def ask_human(question: str, display_question: bool) -> str:
21
- # return input(f"\n{question}\nInput: ")
22
-
23
- class DoneResult(BaseModel):
24
- title: str
25
- response: str
26
- what_do_i_see: str
27
-
28
- # we overwrite done() in this example to demonstrate the validator
29
- @controller.registry.action("Done with task", param_model=DoneResult)
30
- async def done(params: DoneResult):
31
- result = ActionResult(is_done=True, extracted_content=params.model_dump_json())
32
- print(result)
33
- return result
34
-
35
- browser = Browser(
36
- config=BrowserConfig(
37
- headless=False,
38
- disable_security=True,
39
- )
40
- )
41
-
42
- # Await the coroutine to get the browser context
43
- context = await browser.new_context()
44
-
45
- async with context:
46
-
47
- # Add init script to the context - this will be applied to all new pages
48
- pw_context = context.session.context # type: ignore
49
- js_override = files.get_abs_path("lib/browser/init_override.js")
50
- await pw_context.add_init_script(path=js_override) # type: ignore
51
-
52
- agent = Agent(
53
- task="Go to weather.com",
54
- llm=model,
55
- browser=browser,
56
- browser_context=context,
57
- use_vision=True,
58
- controller=controller,
59
- )
60
-
61
- result = await agent.run()
62
- for out in result.model_outputs():
63
- print("-------------")
64
- print(out.current_state.memory)
65
- print(out.current_state.next_goal)
66
- print("-------------")
67
-
68
-
69
- agent = Agent(
70
- task="Search for berlin and tell me the temperature",
71
- llm=model,
72
- browser=browser,
73
- browser_context=context,
74
- use_vision=True,
75
- controller=controller,
76
- )
77
-
78
- result = await agent.run()
79
- # page = await agent.browser_context.get_current_page()
80
- print(result)
81
-
82
-
83
-asyncio.run(main())
84
-
webui/index.css
+23
@@ -1806,6 +1806,24 @@ input:checked + .slider:before {
1806
}
1807
}
1808
1809
+/* Link styling */
1810
+a {
1811
+ color: inherit;
1812
+ /* text-decoration: none; */
1813
+}
1814
+
1815
+a:visited {
1816
+ color: inherit;
1817
+}
1818
+
1819
+a:hover {
1820
+ color: inherit;
1821
+}
1822
+
1823
+a:active {
1824
+ color: inherit;
1825
+}
1826
+
1827
/* Light mode class */
1828
.light-mode {
1829
--color-background: var(--color-background-light);
@@ -2069,6 +2087,11 @@ input:checked + .slider:before {
2087
opacity: 0.6;
2088
}
2089
2090
+.path-link{
2091
+ margin-left: 0.1em;
2092
+ margin-right: 0.1em;
2093
+}
2094
+
2095
/* Alpine cloak to prevent FOUC */
2096
[x-cloak] {
2097
display: none !important;
webui/index.html
+4
-4
@@ -5,7 +5,7 @@
5
<meta charset="UTF-8">
6
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
7
<title>Agent Zero</title>
8
- <link rel="icon" type="image/svg+xml" href="public/darkSymbol.svg">
8
+ <link rel="icon" type="image/svg+xml" href="public/favicon.svg">
9
<link rel="stylesheet" href="index.css">
10
<link rel="stylesheet" href="css/toast.css">
11
<link rel="stylesheet" href="css/settings.css">
@@ -451,13 +451,13 @@
451
<template x-for="(section, sectionIndex) in settings.sections" :key="sectionIndex">
452
<div :id="'section' + (sectionIndex + 1)" class="section">
453
<div class="section-title" x-text="section.title"></div>
454
- <div class="section-description" x-text="section.description"></div>
454
+ <div class="section-description" x-html="section.description"></div>
455
456
<template x-for="(field, fieldIndex) in section.fields" :key="fieldIndex">
457
<div :class="{'field': true, 'field-full': field.type === 'textarea'}">
458
<div class="field-label">
459
<div class="field-title" x-text="field.title"></div>
460
- <div class="field-description" x-text="field.description"></div>
460
+ <div class="field-description" x-html="field.description"></div>
461
</div>
462
463
<div class="field-control">
@@ -767,7 +767,7 @@
767
class="dragdrop-overlay">
768
<img src="public/dragndrop.svg" alt="Drop files" class="dragdrop-icon">
769
<div class="dragdrop-text">Drop files to attach them to your message</div>
770
- <div class="dragdrop-subtext">Supported formats: Images (JPG, PNG, BMP) and Documents (PDF, TXT, CSV, HTML, JSON, MD)</div>
770
+ <div class="dragdrop-subtext"></div>
771
</div>
772
773
</body>
webui/index.js
+1
@@ -802,6 +802,7 @@ function toast(text, type = 'info', timeout = 5000) {
802
updateAndShowToast();
803
}
804
}
805
+window.toast = toast
806
807
function hideToast() {
808
const toast = document.getElementById('toast');
webui/js/file_browser.js
+271
-237
@@ -1,252 +1,286 @@
1
const fileBrowserModalProxy = {
2
- isOpen: false,
3
- isLoading: false,
4
-
5
- browser: {
6
- title: "File Browser",
7
- currentPath: "",
8
- entries: [],
9
- parentPath: "",
10
- sortBy: "name",
11
- sortDirection: "asc"
12
- },
13
-
14
- // Initialize navigation history
15
- history: [],
16
-
17
- async openModal() {
18
- const modalEl = document.getElementById('fileBrowserModal');
19
- const modalAD = Alpine.$data(modalEl);
20
-
21
- modalAD.isOpen = true;
22
- modalAD.isLoading = true;
23
- modalAD.history = []; // reset history when opening modal
24
-
25
- // Initialize currentPath to root if it's empty
26
- if (!modalAD.browser.currentPath) {
27
- modalAD.browser.currentPath = "$WORK_DIR";
28
- }
29
-
30
- await modalAD.fetchFiles(modalAD.browser.currentPath);
31
- },
32
-
33
- isArchive(filename) {
34
- const archiveExts = ['zip', 'tar', 'gz', 'rar', '7z'];
35
- const ext = filename.split('.').pop().toLowerCase();
36
- return archiveExts.includes(ext);
37
- },
38
-
39
- async fetchFiles(path = "") {
40
- this.isLoading = true;
41
- try {
42
- const response = await fetch(`/get_work_dir_files?path=${encodeURIComponent(path)}`);
43
-
44
- if (response.ok) {
45
- const data = await response.json();
46
- this.browser.entries = data.data.entries;
47
- this.browser.currentPath = data.data.current_path;
48
- this.browser.parentPath = data.data.parent_path;
49
- } else {
50
- console.error('Error fetching files:', await response.text());
51
- this.browser.entries = [];
52
- }
53
- } catch (error) {
54
- window.toastFetchError("Error fetching files", error)
55
- this.browser.entries = [];
56
- } finally {
57
- this.isLoading = false;
58
- }
59
- },
60
-
61
- async navigateToFolder(path) {
62
- // Push current path to history before navigating
63
- if (this.browser.currentPath !== path) {
64
- this.history.push(this.browser.currentPath);
65
- }
66
- await this.fetchFiles(path);
67
- },
68
-
69
- async navigateUp() {
70
- if (this.browser.parentPath !== "") {
71
- // Push current path to history before navigating up
72
- this.history.push(this.browser.currentPath);
73
- await this.fetchFiles(this.browser.parentPath);
74
- }
75
- },
76
-
77
- sortFiles(entries) {
78
- return [...entries].sort((a, b) => {
79
- // Folders always come first
80
- if (a.is_dir !== b.is_dir) {
81
- return a.is_dir ? -1 : 1;
82
- }
83
-
84
- const direction = this.browser.sortDirection === 'asc' ? 1 : -1;
85
- switch (this.browser.sortBy) {
86
- case 'name':
87
- return direction * a.name.localeCompare(b.name);
88
- case 'size':
89
- return direction * (a.size - b.size);
90
- case 'date':
91
- return direction * (new Date(a.modified) - new Date(b.modified));
92
- default:
93
- return 0;
94
- }
95
- });
96
- },
97
-
98
- toggleSort(column) {
99
- if (this.browser.sortBy === column) {
100
- this.browser.sortDirection = this.browser.sortDirection === 'asc' ? 'desc' : 'asc';
101
- } else {
102
- this.browser.sortBy = column;
103
- this.browser.sortDirection = 'asc';
104
- }
105
- },
2
+ isOpen: false,
3
+ isLoading: false,
4
+
5
+ browser: {
6
+ title: "File Browser",
7
+ currentPath: "",
8
+ entries: [],
9
+ parentPath: "",
10
+ sortBy: "name",
11
+ sortDirection: "asc",
12
+ },
13
+
14
+ // Initialize navigation history
15
+ history: [],
16
+
17
+ async openModal(path) {
18
+ const modalEl = document.getElementById("fileBrowserModal");
19
+ const modalAD = Alpine.$data(modalEl);
20
+
21
+ modalAD.isOpen = true;
22
+ modalAD.isLoading = true;
23
+ modalAD.history = []; // reset history when opening modal
24
+
25
+ // Initialize currentPath to root if it's empty
26
+ if (path) modalAD.browser.currentPath = path;
27
+ else if (!modalAD.browser.currentPath)
28
+ modalAD.browser.currentPath = "$WORK_DIR";
29
+
30
+ await modalAD.fetchFiles(modalAD.browser.currentPath);
31
+ },
32
+
33
+ isArchive(filename) {
34
+ const archiveExts = ["zip", "tar", "gz", "rar", "7z"];
35
+ const ext = filename.split(".").pop().toLowerCase();
36
+ return archiveExts.includes(ext);
37
+ },
38
+
39
+ async fetchFiles(path = "") {
40
+ this.isLoading = true;
41
+ try {
42
+ const response = await fetch(
43
+ `/get_work_dir_files?path=${encodeURIComponent(path)}`
44
+ );
45
+
46
+ if (response.ok) {
47
+ const data = await response.json();
48
+ this.browser.entries = data.data.entries;
49
+ this.browser.currentPath = data.data.current_path;
50
+ this.browser.parentPath = data.data.parent_path;
51
+ } else {
52
+ console.error("Error fetching files:", await response.text());
53
+ this.browser.entries = [];
54
+ }
55
+ } catch (error) {
56
+ window.toastFetchError("Error fetching files", error);
57
+ this.browser.entries = [];
58
+ } finally {
59
+ this.isLoading = false;
60
+ }
61
+ },
62
107
- async deleteFile(file) {
108
- if (!confirm(`Are you sure you want to delete ${file.name}?`)) {
109
- return;
110
- }
63
+ async navigateToFolder(path) {
64
+ // Push current path to history before navigating
65
+ if (this.browser.currentPath !== path) {
66
+ this.history.push(this.browser.currentPath);
67
+ }
68
+ await this.fetchFiles(path);
69
+ },
70
+
71
+ async navigateUp() {
72
+ if (this.browser.parentPath !== "") {
73
+ // Push current path to history before navigating up
74
+ this.history.push(this.browser.currentPath);
75
+ await this.fetchFiles(this.browser.parentPath);
76
+ }
77
+ },
78
+
79
+ sortFiles(entries) {
80
+ return [...entries].sort((a, b) => {
81
+ // Folders always come first
82
+ if (a.is_dir !== b.is_dir) {
83
+ return a.is_dir ? -1 : 1;
84
+ }
85
+
86
+ const direction = this.browser.sortDirection === "asc" ? 1 : -1;
87
+ switch (this.browser.sortBy) {
88
+ case "name":
89
+ return direction * a.name.localeCompare(b.name);
90
+ case "size":
91
+ return direction * (a.size - b.size);
92
+ case "date":
93
+ return direction * (new Date(a.modified) - new Date(b.modified));
94
+ default:
95
+ return 0;
96
+ }
97
+ });
98
+ },
99
+
100
+ toggleSort(column) {
101
+ if (this.browser.sortBy === column) {
102
+ this.browser.sortDirection =
103
+ this.browser.sortDirection === "asc" ? "desc" : "asc";
104
+ } else {
105
+ this.browser.sortBy = column;
106
+ this.browser.sortDirection = "asc";
107
+ }
108
+ },
109
112
- try {
113
- const response = await fetch('/delete_work_dir_file', {
114
- method: 'POST',
115
- headers: {
116
- 'Content-Type': 'application/json',
117
- },
118
- body: JSON.stringify({
119
- path: file.path,
120
- currentPath: this.browser.currentPath
121
- })
122
- });
123
-
124
- if (response.ok) {
125
- const data = await response.json();
126
- this.browser.entries = this.browser.entries.filter(entry => entry.path !== file.path);
127
- alert('File deleted successfully.');
128
- } else {
129
- alert(`Error deleting file: ${await response.text()}`);
130
- }
131
- } catch (error) {
132
- window.toastFetchError("Error deleting file", error)
133
- alert('Error deleting file');
134
- }
135
- },
110
+ async deleteFile(file) {
111
+ if (!confirm(`Are you sure you want to delete ${file.name}?`)) {
112
+ return;
113
+ }
114
137
- async handleFileUpload(event) {
138
- try {
139
- const files = event.target.files;
140
- if (!files.length) return;
141
-
142
- const formData = new FormData();
143
- formData.append('path', this.browser.currentPath);
144
-
145
- for (let i = 0; i < files.length; i++) {
146
- const ext = files[i].name.split('.').pop().toLowerCase();
147
- if (!['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) {
148
- if (files[i].size > 100 * 1024 * 1024) { // 100MB
149
- alert(`File ${files[i].name} exceeds the maximum allowed size of 100MB.`);
150
- continue;
151
- }
152
- }
153
- formData.append('files[]', files[i]);
154
- }
155
-
156
- // Proceed with upload after validation
157
- const response = await fetch('/upload_work_dir_files', {
158
- method: 'POST',
159
- body: formData
160
- });
161
-
162
- if (response.ok) {
163
- const data = await response.json();
164
- // Update the file list with new data
165
- this.browser.entries = data.data.entries.map(entry => ({
166
- ...entry,
167
- uploadStatus: data.failed.includes(entry.name) ? 'failed' : 'success'
168
- }));
169
- this.browser.currentPath = data.data.current_path;
170
- this.browser.parentPath = data.data.parent_path;
171
-
172
- // Show success message
173
- if (data.failed && data.failed.length > 0) {
174
- const failedFiles = data.failed.map(file => `${file.name}: ${file.error}`).join('\n');
175
- alert(`Some files failed to upload:\n${failedFiles}`);
176
- }
177
- } else {
178
-
179
- alert(data.message);
180
- }
181
-
182
- } catch (error) {
183
- window.toastFetchError("Error uploading files", error)
184
- alert('Error uploading files');
115
+ try {
116
+ const response = await fetch("/delete_work_dir_file", {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ },
121
+ body: JSON.stringify({
122
+ path: file.path,
123
+ currentPath: this.browser.currentPath,
124
+ }),
125
+ });
126
+
127
+ if (response.ok) {
128
+ const data = await response.json();
129
+ this.browser.entries = this.browser.entries.filter(
130
+ (entry) => entry.path !== file.path
131
+ );
132
+ alert("File deleted successfully.");
133
+ } else {
134
+ alert(`Error deleting file: ${await response.text()}`);
135
+ }
136
+ } catch (error) {
137
+ window.toastFetchError("Error deleting file", error);
138
+ alert("Error deleting file");
139
+ }
140
+ },
141
+
142
+ async handleFileUpload(event) {
143
+ try {
144
+ const files = event.target.files;
145
+ if (!files.length) return;
146
+
147
+ const formData = new FormData();
148
+ formData.append("path", this.browser.currentPath);
149
+
150
+ for (let i = 0; i < files.length; i++) {
151
+ const ext = files[i].name.split(".").pop().toLowerCase();
152
+ if (!["zip", "tar", "gz", "rar", "7z"].includes(ext)) {
153
+ if (files[i].size > 100 * 1024 * 1024) {
154
+ // 100MB
155
+ alert(
156
+ `File ${files[i].name} exceeds the maximum allowed size of 100MB.`
157
+ );
158
+ continue;
159
+ }
160
}
186
- },
187
-
188
- async downloadFile(file) {
189
-
190
- try {
191
-
192
- const downloadUrl = `/download_work_dir_file?path=${encodeURIComponent(file.path)}`;
193
-
194
- const response = await fetch(downloadUrl)
195
-
196
-
197
- if (!response.ok) {
198
- throw new Error('Network response was not ok');
199
- }
200
-
201
- const blob = await response.blob();
202
-
203
- const link = document.createElement('a');
204
- link.href = window.URL.createObjectURL(blob);
205
- link.download = file.name;
206
- document.body.appendChild(link);
207
- link.click();
208
- document.body.removeChild(link);
209
- window.URL.revokeObjectURL(link.href);
210
-
211
- } catch (error) {
212
- window.toastFetchError("Error downloading file", error)
213
- alert('Error downloading file');
161
+ formData.append("files[]", files[i]);
162
+ }
163
+
164
+ // Proceed with upload after validation
165
+ const response = await fetch("/upload_work_dir_files", {
166
+ method: "POST",
167
+ body: formData,
168
+ });
169
+
170
+ if (response.ok) {
171
+ const data = await response.json();
172
+ // Update the file list with new data
173
+ this.browser.entries = data.data.entries.map((entry) => ({
174
+ ...entry,
175
+ uploadStatus: data.failed.includes(entry.name) ? "failed" : "success",
176
+ }));
177
+ this.browser.currentPath = data.data.current_path;
178
+ this.browser.parentPath = data.data.parent_path;
179
+
180
+ // Show success message
181
+ if (data.failed && data.failed.length > 0) {
182
+ const failedFiles = data.failed
183
+ .map((file) => `${file.name}: ${file.error}`)
184
+ .join("\n");
185
+ alert(`Some files failed to upload:\n${failedFiles}`);
186
}
215
- },
216
-
217
- // Helper Functions
218
- formatFileSize(size) {
219
- if (size === 0) return '0 Bytes';
220
- const k = 1024;
221
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
222
- const i = Math.floor(Math.log(size) / Math.log(k));
223
- return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
224
- },
225
-
226
- formatDate(dateString) {
227
- const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
228
- return new Date(dateString).toLocaleDateString(undefined, options);
229
- },
230
-
231
- handleClose() {
232
- this.isOpen = false;
187
+ } else {
188
+ alert(data.message);
189
+ }
190
+ } catch (error) {
191
+ window.toastFetchError("Error uploading files", error);
192
+ alert("Error uploading files");
193
}
194
+ },
195
+
196
+ async downloadFile(file) {
197
+ try {
198
+ const downloadUrl = `/download_work_dir_file?path=${encodeURIComponent(
199
+ file.path
200
+ )}`;
201
+
202
+ const response = await fetch(downloadUrl);
203
+
204
+ if (!response.ok) {
205
+ throw new Error("Network response was not ok");
206
+ }
207
+
208
+ const blob = await response.blob();
209
+
210
+ const link = document.createElement("a");
211
+ link.href = window.URL.createObjectURL(blob);
212
+ link.download = file.name;
213
+ document.body.appendChild(link);
214
+ link.click();
215
+ document.body.removeChild(link);
216
+ window.URL.revokeObjectURL(link.href);
217
+ } catch (error) {
218
+ window.toastFetchError("Error downloading file", error);
219
+ alert("Error downloading file");
220
+ }
221
+ },
222
+
223
+ // Helper Functions
224
+ formatFileSize(size) {
225
+ if (size === 0) return "0 Bytes";
226
+ const k = 1024;
227
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
228
+ const i = Math.floor(Math.log(size) / Math.log(k));
229
+ return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
230
+ },
231
+
232
+ formatDate(dateString) {
233
+ const options = {
234
+ year: "numeric",
235
+ month: "short",
236
+ day: "numeric",
237
+ hour: "2-digit",
238
+ minute: "2-digit",
239
+ };
240
+ return new Date(dateString).toLocaleDateString(undefined, options);
241
+ },
242
+
243
+ handleClose() {
244
+ this.isOpen = false;
245
+ },
246
};
247
248
// Wait for Alpine to be ready
237
-document.addEventListener('alpine:init', () => {
238
- Alpine.data('fileBrowserModalProxy', () => ({
239
- init() {
240
- Object.assign(this, fileBrowserModalProxy);
241
- // Ensure immediate file fetch when modal opens
242
- this.$watch('isOpen', async (value) => {
243
- if (value) {
244
- await this.fetchFiles(this.browser.currentPath);
245
- }
246
- });
249
+document.addEventListener("alpine:init", () => {
250
+ Alpine.data("fileBrowserModalProxy", () => ({
251
+ init() {
252
+ Object.assign(this, fileBrowserModalProxy);
253
+ // Ensure immediate file fetch when modal opens
254
+ this.$watch("isOpen", async (value) => {
255
+ if (value) {
256
+ await this.fetchFiles(this.browser.currentPath);
257
}
248
- }));
258
+ });
259
+ },
260
+ }));
261
});
262
263
// Keep the global assignment for backward compatibility
264
window.fileBrowserModalProxy = fileBrowserModalProxy;
265
+
266
+openFileLink = async function (path) {
267
+ try {
268
+ const resp = await window.sendJsonData("/file_info", { path });
269
+ if (!resp.exists) {
270
+ window.toast("File does not exist.", "error");
271
+ return;
272
+ }
273
+
274
+ if (resp.is_dir) {
275
+ fileBrowserModalProxy.openModal(resp.abs_path);
276
+ } else {
277
+ fileBrowserModalProxy.downloadFile({
278
+ path: resp.abs_path,
279
+ name: resp.file_name,
280
+ });
281
+ }
282
+ } catch (e) {
283
+ window.toastFetchError("Error opening file", e);
284
+ }
285
+};
286
+window.openFileLink = openFileLink;
webui/js/messages.js
+46
-30
@@ -103,7 +103,7 @@ export function _drawMessage(
103
preElement.style.wordBreak = "break-word";
104
105
const spanElement = document.createElement("span");
106
- spanElement.innerHTML = escapeHTML(content);
106
+ spanElement.innerHTML = convertHTML(content);
107
108
// Add click handler for small screens
109
spanElement.addEventListener("click", () => {
@@ -117,9 +117,7 @@ export function _drawMessage(
117
// Render LaTeX math within the span
118
if (window.renderMathInElement && latex) {
119
renderMathInElement(spanElement, {
120
- delimiters: [
121
- { left: "$", right: "$", display: true }
122
- ],
120
+ delimiters: [{ left: "$", right: "$", display: true }],
121
throwOnError: false,
122
});
123
}
@@ -143,11 +141,10 @@ export function drawMessageDefault(
141
temp,
142
kvps = null
143
) {
146
- const messageContent = convertImageTags(content); // Convert image tags
144
_drawMessage(
145
messageContainer,
146
heading,
150
- messageContent,
147
+ content,
148
temp,
149
false,
150
kvps,
@@ -171,11 +168,10 @@ export function drawMessageAgent(
168
delete kvpsFlat["tool_args"];
169
}
170
174
- const messageContent = convertImageTags(content); // Convert image tags
171
_drawMessage(
172
messageContainer,
173
heading,
178
- messageContent,
174
+ content,
175
temp,
176
false,
177
kvpsFlat,
@@ -194,11 +190,10 @@ export function drawMessageResponse(
190
temp,
191
kvps = null
192
) {
197
- const messageContent = convertImageTags(content); // Convert image tags
193
_drawMessage(
194
messageContainer,
195
heading,
201
- messageContent,
196
+ content,
197
temp,
198
true,
199
null,
@@ -217,7 +212,6 @@ export function drawMessageDelegation(
212
temp,
213
kvps = null
214
) {
220
- const messageContent = convertImageTags(content); // Convert image tags
215
_drawMessage(
216
messageContainer,
217
heading,
@@ -251,10 +245,10 @@ export function drawMessageUser(
245
if (content && content.trim().length > 0) {
246
const textDiv = document.createElement("div");
247
textDiv.classList.add("message-text");
254
-
248
+
249
// Create a span for the content
250
const spanElement = document.createElement("span");
257
- spanElement.textContent = content;
251
+ spanElement.innerHTML = convertHTML(content);
252
textDiv.appendChild(spanElement);
253
254
// Add click handler
@@ -336,7 +330,6 @@ export function drawMessageTool(
330
temp,
331
kvps = null
332
) {
339
- const messageContent = convertImageTags(content); // Convert image tags
333
_drawMessage(
334
messageContainer,
335
heading,
@@ -358,8 +351,7 @@ export function drawMessageCodeExe(
351
temp,
352
kvps = null
353
) {
361
- const messageContent = convertImageTags(content); // Convert image tags
362
- _drawMessage(messageContainer, heading, messageContent, temp, true, null, [
354
+ _drawMessage(messageContainer, heading, content, temp, true, null, [
355
"message-ai",
356
"message-code-exe",
357
]);
@@ -374,11 +366,10 @@ export function drawMessageBrowser(
366
temp,
367
kvps = null
368
) {
377
- const messageContent = convertImageTags(content); // Convert image tags
369
_drawMessage(
370
messageContainer,
371
heading,
381
- messageContent,
372
+ content,
373
temp,
374
true,
375
kvps,
@@ -397,8 +388,7 @@ export function drawMessageAgentPlain(
388
temp,
389
kvps = null
390
) {
400
- const messageContent = convertImageTags(content); // Convert image tags
401
- _drawMessage(messageContainer, heading, messageContent, temp, false, kvps, [
391
+ _drawMessage(messageContainer, heading, content, temp, false, kvps, [
392
...classes,
393
]);
394
messageContainer.classList.add("center-container");
@@ -434,11 +424,10 @@ export function drawMessageUtil(
424
temp,
425
kvps = null
426
) {
437
- const messageContent = convertImageTags(content); // Convert image tags
427
_drawMessage(
428
messageContainer,
429
heading,
441
- messageContent,
430
+ content,
431
temp,
432
false,
433
kvps,
@@ -536,7 +525,7 @@ function drawKvps(container, kvps, latex) {
525
pre.classList.add("kvps-val");
526
// if (row.classList.contains("msg-thoughts")) {
527
const span = document.createElement("span");
539
- span.innerHTML = escapeHTML(value);
528
+ span.innerHTML = convertHTML(value);
529
pre.appendChild(span);
530
td.appendChild(pre);
531
addCopyButtonToElement(row);
@@ -548,9 +537,7 @@ function drawKvps(container, kvps, latex) {
537
538
if (window.renderMathInElement && latex) {
539
renderMathInElement(span, {
551
- delimiters: [
552
- { left: "$", right: "$", display: true }
553
- ],
540
+ delimiters: [{ left: "$", right: "$", display: true }],
541
throwOnError: false,
542
});
543
}
@@ -608,11 +595,16 @@ async function copyText(text, element) {
595
}
596
}
597
611
-function escapeHTML(str) {
612
- if (typeof str !== "string") {
613
- return str;
614
- }
598
+function convertHTML(str) {
599
+ if (typeof str !== "string") str = JSON.stringify(str, null, 2);
600
+
601
+ let result = escapeHTML(str);
602
+ result = convertPathsToLinks(result);
603
+ result = convertImageTags(result);
604
+ return result;
605
+}
606
607
+function escapeHTML(str) {
608
const escapeChars = {
609
"&": "&",
610
"<": "<",
@@ -622,3 +614,27 @@ function escapeHTML(str) {
614
};
615
return str.replace(/[&<>'"]/g, (char) => escapeChars[char]);
616
}
617
+
618
+function convertPathsToLinks(str) {
619
+ function generateLinks(match,...args) {
620
+ const parts = match.split("/");
621
+
622
+ if (!parts[0]) parts.shift();
623
+ let conc = "";
624
+ let html = "";
625
+ for (let part of parts) {
626
+ conc += "/" + part;
627
+ html += `/<a href="#" class="path-link" onclick="openFileLink('${conc}');">${part}</a>`;
628
+ }
629
+ return html;
630
+ }
631
+
632
+ const prefix = `(?:[ \`'"\\n]|'|")`; // Use a non-capturing group for OR logic
633
+ const folder = `[a-zA-Z0-9_\\/\\.]`; // Characters allowed in folder names
634
+ const file = `[a-zA-Z0-9_\\/]`; // Characters allowed in file names
635
+ const suffix = `(?<!\\.)`
636
+
637
+ const regex = new RegExp(`(?<=${prefix})\\/${folder}*${file}${suffix}`, 'g');
638
+
639
+ return str.replace(regex, generateLinks);
640
+}
webui/js/settings.js
+1
-1
@@ -15,7 +15,7 @@ const settingsModalProxy = {
15
16
17
const settings = {
18
- "title": "Settings page",
18
+ "title": "Settings",
19
"buttons": [
20
{
21
"id": "save",
webui/public/favicon.svg
new
+13
@@ -0,0 +1,13 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+<svg width="100%" height="100%" viewBox="0 0 960 960" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4
+ <g transform="matrix(1.13341,0,0,1.13341,-36.9503,-43.0342)">
5
+ <path d="M878.621,178.762C878.621,101.597 815.973,38.95 738.808,38.95L173.394,38.95C96.23,38.95 33.582,101.597 33.582,178.762L33.582,744.176C33.582,821.34 96.23,883.988 173.394,883.988L738.808,883.988C815.973,883.988 878.621,821.34 878.621,744.176L878.621,178.762Z" style="fill:rgb(1,4,26);"/>
6
+ </g>
7
+ <g transform="matrix(1.03321,0,0,1.03321,-15.9385,-15.938)">
8
+ <path d="M717.77,788.27C638.99,652.89 559.87,516.92 479.15,378.22C399.29,516.59 320.58,652.95 241.99,789.12L120,789.12C239.91,581.87 479.49,170.89 479.49,170.89C479.49,170.89 720.12,580.92 840,788.27L717.77,788.27Z" style="fill:white;fill-rule:nonzero;"/>
9
+ </g>
10
+ <g transform="matrix(1.03321,0,0,1.03321,-15.9385,-15.938)">
11
+ <path d="M633.08,788.85L323.54,788.85C344.15,753.01 364.09,718.33 383.88,683.93L574.1,683.93C593.38,718.23 612.57,752.36 633.08,788.85Z" style="fill:white;fill-rule:nonzero;"/>
12
+ </g>
13
+</svg>