mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-08 01:41:42 +00:00
Merge upstream/development into feat-agent-zero-skills-upgrade
Accept upstream's architectural refactors: - Settings UI sections moved from Python to frontend components - User data consolidated under /usr directory - Inline settings modal replaced by stacked modal system - settings.js removed (moved to component stores) Conflicts resolved by accepting upstream for all 5 files. Our skills/backup features will be re-implemented using the new architecture. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
95de41a99e
175 changed files with 14473 additions and 8675 deletions
|
|
@ -50,7 +50,7 @@ class ApiFilesGet(ApiHandler):
|
|||
if path.startswith("/a0/tmp/uploads/"):
|
||||
# Internal path - convert to external
|
||||
filename = path.replace("/a0/tmp/uploads/", "")
|
||||
external_path = files.get_abs_path("tmp/uploads", filename)
|
||||
external_path = files.get_abs_path("usr/uploads", filename)
|
||||
filename = os.path.basename(external_path)
|
||||
elif path.startswith("/a0/"):
|
||||
# Other internal Agent Zero paths
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from python.helpers.api import ApiHandler, Request, Response
|
|||
from python.helpers import files, projects
|
||||
from python.helpers.print_style import PrintStyle
|
||||
from python.helpers.projects import activate_project
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
from initialize import initialize_agent
|
||||
import threading
|
||||
|
||||
|
|
@ -48,8 +48,8 @@ class ApiMessage(ApiHandler):
|
|||
# Handle attachments (base64 encoded)
|
||||
attachment_paths = []
|
||||
if attachments:
|
||||
upload_folder_int = "/a0/tmp/uploads"
|
||||
upload_folder_ext = files.get_abs_path("tmp/uploads")
|
||||
upload_folder_int = "/a0/usr/uploads"
|
||||
upload_folder_ext = files.get_abs_path("usr/uploads")
|
||||
os.makedirs(upload_folder_ext, exist_ok=True)
|
||||
|
||||
for attachment in attachments:
|
||||
|
|
@ -57,9 +57,9 @@ class ApiMessage(ApiHandler):
|
|||
continue
|
||||
|
||||
try:
|
||||
filename = secure_filename(attachment["filename"])
|
||||
filename = safe_filename(attachment["filename"])
|
||||
if not filename:
|
||||
continue
|
||||
raise ValueError("Invalid filename")
|
||||
|
||||
# Decode base64 content
|
||||
file_content = base64.b64decode(attachment["base64"])
|
||||
|
|
@ -136,7 +136,7 @@ class ApiMessage(ApiHandler):
|
|||
# Add user message to chat history so it's visible in the UI
|
||||
context.log.log(
|
||||
type="user",
|
||||
heading="User message",
|
||||
heading="",
|
||||
content=message,
|
||||
kvps={"attachments": attachment_filenames},
|
||||
)
|
||||
|
|
|
|||
22
python/api/banners.py
Normal file
22
python/api/banners.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers.extension import call_extensions
|
||||
|
||||
|
||||
class GetBanners(ApiHandler):
|
||||
"""
|
||||
API endpoint for Welcome Screen banners.
|
||||
Add checks as extension scripts in python/extensions/banners/ or usr/extensions/banners/
|
||||
"""
|
||||
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
banners = input.get("banners", [])
|
||||
frontend_context = input.get("context", {})
|
||||
|
||||
# Banners array passed by reference - extensions append directly to it
|
||||
await call_extensions("banners", agent=None, banners=banners, frontend_context=frontend_context)
|
||||
|
||||
return {"banners": banners}
|
||||
|
||||
@classmethod
|
||||
def get_methods(cls) -> list[str]:
|
||||
return ["POST"]
|
||||
|
|
@ -17,13 +17,14 @@ class CreateChat(ApiHandler):
|
|||
new_context = self.use_context(new_ctxid)
|
||||
|
||||
# copy selected data from current to new context
|
||||
if current_context:
|
||||
current_data_1 = current_context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
|
||||
if current_data_1:
|
||||
new_context.set_data(projects.CONTEXT_DATA_KEY_PROJECT, current_data_1)
|
||||
current_data_2 = current_context.get_output_data(projects.CONTEXT_DATA_KEY_PROJECT)
|
||||
if current_data_2:
|
||||
new_context.set_output_data(projects.CONTEXT_DATA_KEY_PROJECT, current_data_2)
|
||||
# do not create new chats in the same project anymore, it can be annoying
|
||||
# if current_context:
|
||||
# current_data_1 = current_context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
|
||||
# if current_data_1:
|
||||
# new_context.set_data(projects.CONTEXT_DATA_KEY_PROJECT, current_data_1)
|
||||
# current_data_2 = current_context.get_output_data(projects.CONTEXT_DATA_KEY_PROJECT)
|
||||
# if current_data_2:
|
||||
# new_context.set_output_data(projects.CONTEXT_DATA_KEY_PROJECT, current_data_2)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import files, memory, notification, projects, notification, runtime
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
||||
class GetChatFilesPath(ApiHandler):
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ class GetCsrfToken(ApiHandler):
|
|||
)
|
||||
return {"ok": match, "origin": origin, "allowed_origins": allowed_origins}
|
||||
|
||||
|
||||
def get_origin_from_request(self, request: Request):
|
||||
# get from origin
|
||||
r = request.headers.get("Origin") or request.environ.get("HTTP_ORIGIN")
|
||||
|
|
@ -93,7 +92,9 @@ class GetCsrfToken(ApiHandler):
|
|||
# get the allowed origins from the environment
|
||||
allowed_origins = [
|
||||
origin.strip()
|
||||
for origin in (dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) or "").split(",")
|
||||
for origin in (dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) or "").split(
|
||||
","
|
||||
)
|
||||
if origin.strip()
|
||||
]
|
||||
|
||||
|
|
@ -114,12 +115,19 @@ class GetCsrfToken(ApiHandler):
|
|||
return allowed_origins
|
||||
|
||||
def get_default_allowed_origins(self) -> list[str]:
|
||||
return ["*://localhost:*", "*://127.0.0.1:*", "*://0.0.0.0:*"]
|
||||
return [
|
||||
"*://localhost",
|
||||
"*://localhost:*",
|
||||
"*://127.0.0.1",
|
||||
"*://127.0.0.1:*",
|
||||
"*://0.0.0.0",
|
||||
"*://0.0.0.0:*",
|
||||
]
|
||||
|
||||
def initialize_allowed_origins(self, request: Request):
|
||||
"""
|
||||
If A0 is hosted on a server, add the first visit origin to ALLOWED_ORIGINS.
|
||||
This simplifies deployment process as users can access their new instance without
|
||||
This simplifies deployment process as users can access their new instance without
|
||||
additional setup while keeping it secure.
|
||||
"""
|
||||
# dotenv value is already set, do nothing
|
||||
|
|
@ -144,5 +152,3 @@ class GetCsrfToken(ApiHandler):
|
|||
# if not, add it to the allowed origins
|
||||
allowed_origins.append(req_origin)
|
||||
dotenv.save_dotenv_value(ALLOWED_ORIGINS_KEY, ",".join(allowed_origins))
|
||||
|
||||
|
||||
|
|
@ -8,22 +8,25 @@ from python.api import get_work_dir_files
|
|||
|
||||
class DeleteWorkDirFile(ApiHandler):
|
||||
async def process(self, input: Input, request: Request) -> Output:
|
||||
file_path = input.get("path", "")
|
||||
if not file_path.startswith("/"):
|
||||
file_path = f"/{file_path}"
|
||||
try:
|
||||
file_path = input.get("path", "")
|
||||
if not file_path.startswith("/"):
|
||||
file_path = f"/{file_path}"
|
||||
|
||||
current_path = input.get("currentPath", "")
|
||||
current_path = input.get("currentPath", "")
|
||||
|
||||
# browser = FileBrowser()
|
||||
res = await runtime.call_development_function(delete_file, file_path)
|
||||
# browser = FileBrowser()
|
||||
res = await runtime.call_development_function(delete_file, file_path)
|
||||
|
||||
if res:
|
||||
# Get updated file list
|
||||
# result = browser.get_files(current_path)
|
||||
result = await runtime.call_development_function(get_work_dir_files.get_files, current_path)
|
||||
return {"data": result}
|
||||
else:
|
||||
raise Exception("File not found or could not be deleted")
|
||||
if res:
|
||||
# Get updated file list
|
||||
# result = browser.get_files(current_path)
|
||||
result = await runtime.call_development_function(get_work_dir_files.get_files, current_path)
|
||||
return {"data": result}
|
||||
else:
|
||||
return {"error": "File not found or could not be deleted"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
async def delete_file(file_path: str):
|
||||
|
|
|
|||
84
python/api/edit_work_dir_file.py
Normal file
84
python/api/edit_work_dir_file.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import mimetypes
|
||||
import os
|
||||
|
||||
from python.helpers.api import ApiHandler, Input, Output, Request
|
||||
from python.helpers.file_browser import FileBrowser
|
||||
from python.helpers import runtime, files
|
||||
|
||||
MAX_EDIT_FILE_SIZE = 1024 * 1024
|
||||
BINARY_SAMPLE_SIZE = 10 * 1024
|
||||
|
||||
|
||||
class EditWorkDirFile(ApiHandler):
|
||||
@classmethod
|
||||
def get_methods(cls):
|
||||
return ["GET", "POST"]
|
||||
|
||||
async def process(self, input: Input, request: Request) -> Output:
|
||||
try:
|
||||
if request.method == "GET":
|
||||
file_path = request.args.get("path", "")
|
||||
if not file_path:
|
||||
return {"error": "Path is required"}
|
||||
if not file_path.startswith("/"):
|
||||
file_path = f"/{file_path}"
|
||||
|
||||
data = await runtime.call_development_function(load_file, file_path)
|
||||
return {"data": data}
|
||||
|
||||
file_path = input.get("path", "")
|
||||
if not file_path:
|
||||
return {"error": "Path is required"}
|
||||
if not file_path.startswith("/"):
|
||||
file_path = f"/{file_path}"
|
||||
|
||||
content = input.get("content", "")
|
||||
if not isinstance(content, str):
|
||||
return {"error": "Content must be a string"}
|
||||
|
||||
content_size = len(content.encode("utf-8"))
|
||||
if content_size > MAX_EDIT_FILE_SIZE:
|
||||
return {"error": "File exceeds 1 MB and cannot be edited"}
|
||||
|
||||
res = await runtime.call_development_function(save_file, file_path, content)
|
||||
if not res:
|
||||
return {"error": "Failed to save file"}
|
||||
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
async def load_file(file_path: str) -> dict:
|
||||
browser = FileBrowser()
|
||||
full_path = browser.get_full_path(file_path)
|
||||
|
||||
if os.path.isdir(full_path):
|
||||
raise Exception("Path points to a directory")
|
||||
|
||||
size = os.path.getsize(full_path)
|
||||
if size > MAX_EDIT_FILE_SIZE:
|
||||
raise Exception("File exceeds 1 MB and cannot be edited")
|
||||
|
||||
# Binary detection: only sample the first ~10KB (per backend rules)
|
||||
if files.is_probably_binary_file(full_path, sample_size=BINARY_SAMPLE_SIZE):
|
||||
raise Exception("Binary file detected; editing is not supported")
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(full_path)
|
||||
try:
|
||||
with open(full_path, "r", encoding="utf-8", errors="strict") as file:
|
||||
content = file.read()
|
||||
except UnicodeDecodeError:
|
||||
raise Exception("Unable to decode file as UTF-8; editing is not supported")
|
||||
|
||||
return {
|
||||
"path": file_path,
|
||||
"name": os.path.basename(full_path),
|
||||
"mime_type": mime_type or "text/plain",
|
||||
"content": content,
|
||||
}
|
||||
|
||||
|
||||
def save_file(file_path: str, content: str) -> bool:
|
||||
browser = FileBrowser()
|
||||
return browser.save_text_file(file_path, content)
|
||||
|
|
@ -23,20 +23,21 @@ class ImageGet(ApiHandler):
|
|||
if not path:
|
||||
raise ValueError("No path provided")
|
||||
|
||||
# no real need to check, we have the extension filter in place
|
||||
# check if path is within base directory
|
||||
if runtime.is_development():
|
||||
in_base = files.is_in_base_dir(files.fix_dev_path(path))
|
||||
else:
|
||||
in_base = files.is_in_base_dir(path)
|
||||
if not in_base:
|
||||
raise ValueError("Path is outside of allowed directory")
|
||||
# if runtime.is_development():
|
||||
# in_base = files.is_in_base_dir(files.fix_dev_path(path))
|
||||
# else:
|
||||
# in_base = files.is_in_base_dir(path)
|
||||
# if not in_base and not files.is_in_dir(path, "/root"):
|
||||
# raise ValueError("Path is outside of allowed directory")
|
||||
|
||||
# get file extension and info
|
||||
file_ext = os.path.splitext(path)[1].lower()
|
||||
filename = os.path.basename(path)
|
||||
|
||||
# list of allowed image extensions
|
||||
image_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]
|
||||
image_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico", ".svgz"]
|
||||
|
||||
# # If metadata is requested, return file information
|
||||
# if metadata:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import files, memory
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
|
||||
|
||||
class ImportKnowledge(ApiHandler):
|
||||
|
|
@ -32,7 +32,9 @@ class ImportKnowledge(ApiHandler):
|
|||
|
||||
for file in file_list:
|
||||
if file and file.filename:
|
||||
filename = secure_filename(file.filename) # type: ignore
|
||||
filename = safe_filename(file.filename)
|
||||
if not filename:
|
||||
continue
|
||||
file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
|
||||
saved_filenames.append(filename)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import files, memory, notification, projects, notification
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
||||
class ReindexKnowledge(ApiHandler):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from agent import AgentContext, UserMessage
|
||||
from python.helpers.api import ApiHandler, Request, Response
|
||||
|
||||
from python.helpers import files, extension
|
||||
from python.helpers import files, extension, message_queue as mq
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
from python.helpers.defer import DeferredTask
|
||||
from python.helpers.print_style import PrintStyle
|
||||
|
||||
|
||||
class Message(ApiHandler):
|
||||
|
|
@ -29,15 +28,17 @@ class Message(ApiHandler):
|
|||
attachments = request.files.getlist("attachments")
|
||||
attachment_paths = []
|
||||
|
||||
upload_folder_int = "/a0/tmp/uploads"
|
||||
upload_folder_ext = files.get_abs_path("tmp/uploads") # for development environment
|
||||
upload_folder_int = "/a0/usr/uploads"
|
||||
upload_folder_ext = files.get_abs_path("usr/uploads") # for development environment
|
||||
|
||||
if attachments:
|
||||
os.makedirs(upload_folder_ext, exist_ok=True)
|
||||
for attachment in attachments:
|
||||
if attachment.filename is None:
|
||||
continue
|
||||
filename = secure_filename(attachment.filename)
|
||||
filename = safe_filename(attachment.filename)
|
||||
if not filename:
|
||||
continue
|
||||
save_path = files.get_abs_path(upload_folder_ext, filename)
|
||||
attachment.save(save_path)
|
||||
attachment_paths.append(os.path.join(upload_folder_int, filename))
|
||||
|
|
@ -64,30 +65,7 @@ class Message(ApiHandler):
|
|||
# Store attachments in agent data
|
||||
# context.agent0.set_data("attachments", attachment_paths)
|
||||
|
||||
# Prepare attachment filenames for logging
|
||||
attachment_filenames = (
|
||||
[os.path.basename(path) for path in attachment_paths]
|
||||
if attachment_paths
|
||||
else []
|
||||
)
|
||||
|
||||
# Print to console and log
|
||||
PrintStyle(
|
||||
background_color="#6C3483", font_color="white", bold=True, padding=True
|
||||
).print(f"User message:")
|
||||
PrintStyle(font_color="white", padding=False).print(f"> {message}")
|
||||
if attachment_filenames:
|
||||
PrintStyle(font_color="white", padding=False).print("Attachments:")
|
||||
for filename in attachment_filenames:
|
||||
PrintStyle(font_color="white", padding=False).print(f"- {filename}")
|
||||
|
||||
# Log the message with message_id and attachments
|
||||
context.log.log(
|
||||
type="user",
|
||||
heading="User message",
|
||||
content=message,
|
||||
kvps={"attachments": attachment_filenames},
|
||||
id=message_id,
|
||||
)
|
||||
# Log to console and UI using helper function
|
||||
mq.log_user_message(context, message, attachment_paths, message_id)
|
||||
|
||||
return context.communicate(UserMessage(message, attachment_paths)), context
|
||||
|
|
|
|||
21
python/api/message_queue_add.py
Normal file
21
python/api/message_queue_add.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import message_queue as mq
|
||||
from agent import AgentContext
|
||||
|
||||
|
||||
class MessageQueueAdd(ApiHandler):
|
||||
"""Add a message to the queue."""
|
||||
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
context = AgentContext.get(input.get("context", ""))
|
||||
if not context:
|
||||
return Response("Context not found", status=404)
|
||||
|
||||
text = input.get("text", "").strip()
|
||||
attachments = input.get("attachments", []) # filenames from /upload API
|
||||
|
||||
if not text and not attachments:
|
||||
return Response("Empty message", status=400)
|
||||
|
||||
item = mq.add(context, text, attachments)
|
||||
return {"ok": True, "item_id": item["id"], "queue_length": len(mq.get_queue(context))}
|
||||
16
python/api/message_queue_remove.py
Normal file
16
python/api/message_queue_remove.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import message_queue as mq
|
||||
from agent import AgentContext
|
||||
|
||||
|
||||
class MessageQueueRemove(ApiHandler):
|
||||
"""Remove message(s) from queue."""
|
||||
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
context = AgentContext.get(input.get("context", ""))
|
||||
if not context:
|
||||
return Response("Context not found", status=404)
|
||||
|
||||
item_id = input.get("item_id") # None means clear all
|
||||
remaining = mq.remove(context, item_id)
|
||||
return {"ok": True, "remaining": remaining}
|
||||
30
python/api/message_queue_send.py
Normal file
30
python/api/message_queue_send.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import message_queue as mq
|
||||
from agent import AgentContext
|
||||
|
||||
|
||||
class MessageQueueSend(ApiHandler):
|
||||
"""Send queued message(s) immediately."""
|
||||
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
context = AgentContext.get(input.get("context", ""))
|
||||
if not context:
|
||||
return Response("Context not found", status=404)
|
||||
|
||||
if not mq.has_queue(context):
|
||||
return {"ok": True, "message": "Queue empty"}
|
||||
|
||||
item_id = input.get("item_id")
|
||||
send_all = input.get("send_all", False)
|
||||
|
||||
if send_all:
|
||||
count = mq.send_all_aggregated(context)
|
||||
return {"ok": True, "sent_count": count}
|
||||
|
||||
# Send single item
|
||||
item = mq.pop_item(context, item_id) if item_id else mq.pop_first(context)
|
||||
if not item:
|
||||
return Response("Item not found", status=404)
|
||||
|
||||
mq.send_message(context, item)
|
||||
return {"ok": True, "sent_item_id": item["id"]}
|
||||
|
|
@ -122,4 +122,6 @@ class Poll(ApiHandler):
|
|||
"notifications": notifications,
|
||||
"notifications_guid": notification_manager.guid,
|
||||
"notifications_version": len(notification_manager.updates),
|
||||
"message_queue": context.output_data.get("message_queue", []) if context else [],
|
||||
"running": context.is_running() if context else False,
|
||||
}
|
||||
|
|
|
|||
54
python/api/rename_work_dir_file.py
Normal file
54
python/api/rename_work_dir_file.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from python.helpers.api import ApiHandler, Input, Output, Request
|
||||
from python.helpers.file_browser import FileBrowser
|
||||
from python.helpers import runtime
|
||||
from python.api import get_work_dir_files
|
||||
|
||||
|
||||
class RenameWorkDirFile(ApiHandler):
|
||||
async def process(self, input: Input, request: Request) -> Output:
|
||||
try:
|
||||
action = input.get("action", "rename")
|
||||
new_name = (input.get("newName", "") or "").strip()
|
||||
if not new_name:
|
||||
return {"error": "New name is required"}
|
||||
|
||||
current_path = input.get("currentPath", "")
|
||||
|
||||
if action == "create-folder":
|
||||
parent_path = input.get("parentPath", current_path)
|
||||
if not parent_path:
|
||||
return {"error": "Parent path is required"}
|
||||
res = await runtime.call_development_function(
|
||||
create_folder, parent_path, new_name
|
||||
)
|
||||
else:
|
||||
file_path = input.get("path", "")
|
||||
if not file_path:
|
||||
return {"error": "Path is required"}
|
||||
if not file_path.startswith("/"):
|
||||
file_path = f"/{file_path}"
|
||||
res = await runtime.call_development_function(
|
||||
rename_item, file_path, new_name
|
||||
)
|
||||
|
||||
if res:
|
||||
result = await runtime.call_development_function(
|
||||
get_work_dir_files.get_files, current_path
|
||||
)
|
||||
return {"data": result}
|
||||
|
||||
error_msg = "Failed to create folder" if action == "create-folder" else "Rename failed"
|
||||
return {"error": error_msg}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
async def rename_item(file_path: str, new_name: str) -> bool:
|
||||
browser = FileBrowser()
|
||||
return browser.rename_item(file_path, new_name)
|
||||
|
||||
|
||||
async def create_folder(parent_path: str, folder_name: str) -> bool:
|
||||
browser = FileBrowser()
|
||||
return browser.create_folder(parent_path, folder_name)
|
||||
|
|
@ -4,8 +4,9 @@ from python.helpers import settings
|
|||
|
||||
class GetSettings(ApiHandler):
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
set = settings.convert_out(settings.get_settings())
|
||||
return {"settings": set}
|
||||
backend = settings.get_settings()
|
||||
out = settings.convert_out(backend)
|
||||
return dict(out)
|
||||
|
||||
@classmethod
|
||||
def get_methods(cls) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from typing import Any
|
|||
|
||||
class SetSettings(ApiHandler):
|
||||
async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
|
||||
set = settings.convert_in(input)
|
||||
set = settings.set_settings(set)
|
||||
return {"settings": set}
|
||||
frontend = input.get("settings", input)
|
||||
backend = settings.convert_in(settings.Settings(**frontend))
|
||||
backend = settings.set_settings(backend)
|
||||
out = settings.convert_out(backend)
|
||||
return dict(out)
|
||||
|
|
|
|||
|
|
@ -18,16 +18,19 @@ async def process(input: dict) -> dict | Response:
|
|||
port = runtime.get_web_ui_port()
|
||||
provider = input.get("provider", "serveo") # Default to serveo
|
||||
tunnel_url = tunnel_manager.start_tunnel(port, provider)
|
||||
if tunnel_url is None:
|
||||
# Add a little delay and check again - tunnel might be starting
|
||||
import time
|
||||
time.sleep(2)
|
||||
tunnel_url = tunnel_manager.get_tunnel_url()
|
||||
error = tunnel_manager.get_last_error()
|
||||
if error:
|
||||
return {
|
||||
"success": False,
|
||||
"tunnel_url": None,
|
||||
"message": error,
|
||||
"notifications": tunnel_manager.get_notifications()
|
||||
}
|
||||
|
||||
return {
|
||||
"success": tunnel_url is not None,
|
||||
"tunnel_url": tunnel_url,
|
||||
"message": "Tunnel creation in progress" if tunnel_url is None else "Tunnel created successfully"
|
||||
"notifications": tunnel_manager.get_notifications()
|
||||
}
|
||||
|
||||
elif action == "stop":
|
||||
|
|
@ -41,9 +44,17 @@ async def process(input: dict) -> dict | Response:
|
|||
"is_running": tunnel_manager.is_running
|
||||
}
|
||||
|
||||
elif action == "notifications":
|
||||
return {
|
||||
"success": True,
|
||||
"notifications": tunnel_manager.get_notifications(),
|
||||
"tunnel_url": tunnel_manager.get_tunnel_url(),
|
||||
"is_running": tunnel_manager.is_running
|
||||
}
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid action. Use 'create', 'stop', or 'get'."
|
||||
"error": "Invalid action. Use 'create', 'stop', 'get', or 'notifications'."
|
||||
}
|
||||
|
||||
def stop():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from python.helpers.api import ApiHandler, Request, Response
|
||||
from python.helpers import files
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
|
||||
|
||||
class UploadFile(ApiHandler):
|
||||
|
|
@ -13,8 +13,12 @@ class UploadFile(ApiHandler):
|
|||
|
||||
for file in file_list:
|
||||
if file and self.allowed_file(file.filename): # Check file type
|
||||
filename = secure_filename(file.filename) # type: ignore
|
||||
file.save(files.get_abs_path("tmp/upload", filename))
|
||||
if not file.filename:
|
||||
continue
|
||||
filename = safe_filename(file.filename)
|
||||
if not filename:
|
||||
continue
|
||||
file.save(files.get_abs_path("usr/uploads", filename))
|
||||
saved_filenames.append(filename)
|
||||
|
||||
return {"filenames": saved_filenames} # Return saved filenames
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class InitialMessage(Extension):
|
|||
# Add to log (green bubble) for immediate UI display
|
||||
self.agent.context.log.log(
|
||||
type="response",
|
||||
heading=f"{self.agent.agent_name}: Welcome",
|
||||
content=initial_message_text,
|
||||
finished=True,
|
||||
update_progress="none",
|
||||
|
|
|
|||
63
python/extensions/banners/_10_unsecured_connection.py
Normal file
63
python/extensions/banners/_10_unsecured_connection.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from python.helpers.extension import Extension
|
||||
from python.helpers import dotenv
|
||||
import re
|
||||
|
||||
|
||||
class UnsecuredConnectionCheck(Extension):
|
||||
"""Check: non-local without credentials, or credentials over non-HTTPS."""
|
||||
|
||||
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
|
||||
hostname = frontend_context.get("hostname", "")
|
||||
protocol = frontend_context.get("protocol", "")
|
||||
|
||||
auth_login = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN, "")
|
||||
auth_password = dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD, "")
|
||||
has_credentials = bool(auth_login and auth_login.strip() and auth_password and auth_password.strip())
|
||||
|
||||
is_local = self._is_localhost(hostname)
|
||||
is_https = protocol == "https:"
|
||||
|
||||
if not is_local and not has_credentials:
|
||||
banners.append({
|
||||
"id": "unsecured-connection",
|
||||
"type": "warning",
|
||||
"priority": 80,
|
||||
"title": "Unsecured Connection",
|
||||
"html": """You are accessing Agent Zero from a non-local address without authentication.
|
||||
<a href="#" onclick="document.getElementById('settings').click(); return false;">
|
||||
Configure credentials</a> in Settings → External Services → Authentication.""",
|
||||
"dismissible": True,
|
||||
"source": "backend"
|
||||
})
|
||||
|
||||
if has_credentials and not is_local and not is_https:
|
||||
banners.append({
|
||||
"id": "credentials-unencrypted",
|
||||
"type": "warning",
|
||||
"priority": 90,
|
||||
"title": "Credentials May Be Sent Unencrypted",
|
||||
"html": """Your connection is not using HTTPS. Login credentials may be transmitted in plain text.
|
||||
Consider using HTTPS or a secure tunnel.""",
|
||||
"dismissible": True,
|
||||
"source": "backend"
|
||||
})
|
||||
|
||||
def _is_localhost(self, hostname: str) -> bool:
|
||||
local_patterns = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
|
||||
|
||||
if hostname in local_patterns:
|
||||
return True
|
||||
|
||||
# RFC1918 private ranges
|
||||
if re.match(r"^192\.168\.\d{1,3}\.\d{1,3}$", hostname):
|
||||
return True
|
||||
if re.match(r"^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$", hostname):
|
||||
return True
|
||||
if re.match(r"^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$", hostname):
|
||||
return True
|
||||
|
||||
# .local domains
|
||||
if hostname.endswith(".local"):
|
||||
return True
|
||||
|
||||
return False
|
||||
64
python/extensions/banners/_20_missing_api_key.py
Normal file
64
python/extensions/banners/_20_missing_api_key.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from python.helpers.extension import Extension
|
||||
from python.helpers import settings as settings_helper
|
||||
import models
|
||||
|
||||
|
||||
class MissingApiKeyCheck(Extension):
|
||||
"""Check if API keys are configured for selected model providers."""
|
||||
|
||||
LOCAL_PROVIDERS = ["ollama", "lm_studio"]
|
||||
LOCAL_EMBEDDING = ["huggingface"]
|
||||
MODEL_TYPE_NAMES = {
|
||||
"chat": "Chat Model",
|
||||
"utility": "Utility Model",
|
||||
"browser": "Web Browser Model",
|
||||
"embedding": "Embedding Model",
|
||||
}
|
||||
|
||||
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
|
||||
current_settings = settings_helper.get_settings()
|
||||
model_providers = {
|
||||
"chat": current_settings.get("chat_model_provider", ""),
|
||||
"utility": current_settings.get("util_model_provider", ""),
|
||||
"browser": current_settings.get("browser_model_provider", ""),
|
||||
"embedding": current_settings.get("embed_model_provider", ""),
|
||||
}
|
||||
|
||||
missing_providers = []
|
||||
|
||||
for model_type, provider in model_providers.items():
|
||||
if not provider:
|
||||
continue
|
||||
|
||||
provider_lower = provider.lower()
|
||||
if provider_lower in self.LOCAL_PROVIDERS:
|
||||
continue
|
||||
if model_type == "embedding" and provider_lower in self.LOCAL_EMBEDDING:
|
||||
continue
|
||||
|
||||
api_key = models.get_api_key(provider_lower)
|
||||
if not (api_key and api_key.strip() and api_key != "None"):
|
||||
missing_providers.append({
|
||||
"model_type": self.MODEL_TYPE_NAMES.get(model_type, model_type),
|
||||
"provider": provider,
|
||||
})
|
||||
|
||||
if not missing_providers:
|
||||
return
|
||||
|
||||
model_list = ", ".join(
|
||||
f"{p['model_type']} ({p['provider']})" for p in missing_providers
|
||||
)
|
||||
|
||||
banners.append({
|
||||
"id": "missing-api-key",
|
||||
"type": "error",
|
||||
"priority": 100,
|
||||
"title": "Missing API Key",
|
||||
"html": f"""No API key configured for: {model_list}.
|
||||
Agent Zero will not be able to function properly.
|
||||
<a href="#" onclick="document.getElementById('settings').click(); return false;">
|
||||
Add your API key</a> in Settings → External Services → API Keys.""",
|
||||
"dismissible": False,
|
||||
"source": "backend"
|
||||
})
|
||||
|
|
@ -19,8 +19,10 @@ class LogForStream(Extension):
|
|||
)
|
||||
)
|
||||
|
||||
def build_heading(agent, text: str):
|
||||
return f"icon://network_intelligence {agent.agent_name}: {text}"
|
||||
def build_heading(agent, text: str, icon: str = "network_intelligence"):
|
||||
# Include agent identifier for all agents (A0:, A1:, A2:, etc.)
|
||||
agent_prefix = f"{agent.agent_name}: "
|
||||
return f"{agent_prefix}{text}"
|
||||
|
||||
def build_default_heading(agent):
|
||||
return build_heading(agent, "Generating...")
|
||||
return build_heading(agent, "Calling LLM...")
|
||||
|
|
@ -89,7 +89,7 @@ class RecallMemories(Extension):
|
|||
except Exception as e:
|
||||
err = errors.format_error(e)
|
||||
self.agent.context.log.log(
|
||||
type="error", heading="Recall memories extension error:", content=err
|
||||
type="warning", heading="Recall memories extension error:", content=err
|
||||
)
|
||||
query = ""
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ class RecallMemories(Extension):
|
|||
except Exception as e:
|
||||
err = errors.format_error(e)
|
||||
self.agent.context.log.log(
|
||||
type="error", heading="Failed to filter relevant memories", content=err
|
||||
type="warning", heading="Failed to filter relevant memories", content=err
|
||||
)
|
||||
filter_inds = []
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ class MemorizeMemories(Extension):
|
|||
# memory_log = self.agent.context.log.log(
|
||||
# type="util",
|
||||
# heading=f"Processing memory fragment: {txt[:50]}...",
|
||||
# temp=False,
|
||||
# update_progress="none" # Don't affect status bar
|
||||
# )
|
||||
|
||||
|
|
@ -133,7 +132,6 @@ class MemorizeMemories(Extension):
|
|||
memory_log.update(
|
||||
result="Fragment processed successfully",
|
||||
heading=f"Memory fragment completed: {txt[:50]}...",
|
||||
temp=False, # Show completion message
|
||||
update_progress="none" # Show briefly then disappear
|
||||
)
|
||||
else:
|
||||
|
|
@ -141,7 +139,6 @@ class MemorizeMemories(Extension):
|
|||
memory_log.update(
|
||||
result="Fragment processing failed",
|
||||
heading=f"Memory fragment failed: {txt[:50]}...",
|
||||
temp=False, # Show completion message
|
||||
update_progress="none" # Show briefly then disappear
|
||||
)
|
||||
total_processed += 1
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ class MemorizeSolutions(Extension):
|
|||
# solution_log = self.agent.context.log.log(
|
||||
# type="util",
|
||||
# heading=f"Processing solution: {txt[:50]}...",
|
||||
# temp=False,
|
||||
# update_progress="none" # Don't affect status bar
|
||||
# )
|
||||
|
||||
|
|
@ -140,7 +139,6 @@ class MemorizeSolutions(Extension):
|
|||
solution_log.update(
|
||||
result="Solution processed successfully",
|
||||
heading=f"Solution completed: {txt[:50]}...",
|
||||
temp=False, # Show completion message
|
||||
update_progress="none" # Show briefly then disappear
|
||||
)
|
||||
else:
|
||||
|
|
@ -148,7 +146,6 @@ class MemorizeSolutions(Extension):
|
|||
solution_log.update(
|
||||
result="Solution processing failed",
|
||||
heading=f"Solution failed: {txt[:50]}...",
|
||||
temp=False, # Show completion message
|
||||
update_progress="none" # Show briefly then disappear
|
||||
)
|
||||
total_processed += 1
|
||||
|
|
|
|||
34
python/extensions/process_chain_end/_50_process_queue.py
Normal file
34
python/extensions/process_chain_end/_50_process_queue.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import asyncio
|
||||
from python.helpers.extension import Extension
|
||||
from python.helpers import message_queue as mq
|
||||
from agent import AgentContext, Agent, LoopData
|
||||
|
||||
|
||||
class ProcessQueue(Extension):
|
||||
"""Process queued messages after monologue ends."""
|
||||
|
||||
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
|
||||
# Only process for agent0 (main agent)
|
||||
if self.agent.number != 0:
|
||||
return
|
||||
|
||||
context = self.agent.context
|
||||
|
||||
# Check if there are queued messages
|
||||
if mq.has_queue(context):
|
||||
# Schedule delayed task to send next queued message
|
||||
# This allows current monologue to fully complete first
|
||||
asyncio.create_task(self._delayed_send(context))
|
||||
|
||||
async def _delayed_send(self, context: AgentContext):
|
||||
"""Wait for task to complete, then send next queued message."""
|
||||
|
||||
# Wait for current task to finish, but no more than 1 minute to prevent hanging tasks
|
||||
total_wait = 0
|
||||
while context.is_running() and total_wait < 60:
|
||||
await asyncio.sleep(0.1)
|
||||
total_wait += 0.1
|
||||
|
||||
# Send next queued message if task is not running
|
||||
if not context.is_running():
|
||||
mq.send_next(context)
|
||||
|
|
@ -12,8 +12,10 @@ class LogFromStream(Extension):
|
|||
async def execute(self, loop_data: LoopData = LoopData(), text: str = "", **kwargs):
|
||||
|
||||
# thought length indicator
|
||||
pipes = "|" * math.ceil(math.sqrt(len(text)))
|
||||
heading = build_heading(self.agent, f"Reasoning.. {pipes}")
|
||||
length = f"({len(text)})" if text else ""
|
||||
pipes = "|" * math.ceil(math.sqrt(len(text))/2)
|
||||
heading = build_heading(self.agent, f"Reasoning... {pipes}")
|
||||
step = f"Reasoning... {length}"
|
||||
|
||||
# create log message and store it in loop data temporary params
|
||||
if "log_item_generating" not in loop_data.params_temporary:
|
||||
|
|
@ -21,9 +23,10 @@ class LogFromStream(Extension):
|
|||
self.agent.context.log.log(
|
||||
type="agent",
|
||||
heading=heading,
|
||||
step=step
|
||||
)
|
||||
)
|
||||
|
||||
# update log message
|
||||
log_item = loop_data.params_temporary["log_item_generating"]
|
||||
log_item.update(heading=heading, reasoning=text)
|
||||
log_item.update(heading=heading, reasoning=text, step=step)
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ class LogFromStream(Extension):
|
|||
if "headline" in parsed:
|
||||
heading = build_heading(self.agent, parsed['headline'])
|
||||
elif "tool_name" in parsed:
|
||||
heading = build_heading(self.agent, f"Using tool {parsed['tool_name']}") # if the llm skipped headline
|
||||
heading = build_heading(self.agent, f"Using {parsed['tool_name']}") # if the llm skipped headline
|
||||
elif "thoughts" in parsed:
|
||||
# thought length indicator
|
||||
thoughts = "\n".join(parsed["thoughts"])
|
||||
pipes = "|" * math.ceil(math.sqrt(len(thoughts)))
|
||||
heading = build_heading(self.agent, f"Thinking... {pipes}")
|
||||
length = "|" * math.ceil(math.sqrt(len(text))/2)
|
||||
heading = build_heading(self.agent, f"Thinking... {length}")
|
||||
else:
|
||||
heading = build_heading(self.agent, "Receiving...")
|
||||
|
||||
# create log message and store it in loop data temporary params
|
||||
if "log_item_generating" not in loop_data.params_temporary:
|
||||
|
|
@ -45,7 +46,25 @@ class LogFromStream(Extension):
|
|||
kvps = {}
|
||||
if log_item.kvps is not None and "reasoning" in log_item.kvps:
|
||||
kvps["reasoning"] = log_item.kvps["reasoning"]
|
||||
|
||||
# step description for UI - using tool XY, writing Python code, etc.
|
||||
if parsed is not None and "tool_name" in parsed and parsed["tool_name"]:
|
||||
kvps["step"] = f"Using {parsed['tool_name']}..." # using tool XY
|
||||
if parsed["tool_name"]=="code_execution_tool":
|
||||
if "tool_args" in parsed and "runtime" in parsed["tool_args"]:
|
||||
length = ""
|
||||
if "code" in parsed["tool_args"]:
|
||||
length = f"({len(parsed['tool_args']['code'])})"
|
||||
kvps["step"] = f"Writing code... {length}"
|
||||
if parsed["tool_args"]["runtime"] == "python":
|
||||
kvps["step"] = f"Writing Python code... {length}"
|
||||
elif parsed["tool_args"]["runtime"] == "nodejs":
|
||||
kvps["step"] = f"Writing Node.js code... {length}"
|
||||
elif parsed["tool_args"]["runtime"] == "terminal":
|
||||
kvps["step"] = f"Writing terminal command... {length}"
|
||||
kvps.update(parsed)
|
||||
|
||||
|
||||
|
||||
# update the log item
|
||||
log_item.update(heading=heading, content=text, kvps=kvps)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from python.helpers import persist_chat, tokens
|
||||
from python.helpers.extension import Extension
|
||||
from agent import LoopData
|
||||
import asyncio
|
||||
from python.helpers.log import LogItem
|
||||
from python.helpers import log
|
||||
import math
|
||||
from python.extensions.before_main_llm_call._10_log_for_stream import build_heading, build_default_heading
|
||||
|
||||
|
||||
class LogFromStream(Extension):
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
loop_data: LoopData = LoopData(),
|
||||
text: str = "",
|
||||
parsed: dict = {},
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
# get log item from loop data temporary params
|
||||
log_item = loop_data.params_temporary["log_item_generating"]
|
||||
if log_item is None:
|
||||
return
|
||||
|
||||
# remove step parameter when done
|
||||
if log_item.kvps is not None and "step" in log_item.kvps:
|
||||
del log_item.kvps["step"]
|
||||
|
||||
# update the log item
|
||||
log_item.update(kvps=log_item.kvps)
|
||||
|
|
@ -3,7 +3,8 @@ import io
|
|||
import base64
|
||||
from PIL import Image
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from python.helpers.print_style import PrintStyle
|
||||
|
||||
|
|
@ -41,10 +42,10 @@ class AttachmentManager:
|
|||
except AttributeError:
|
||||
return False
|
||||
|
||||
def save_file(self, file, filename: str) -> Tuple[str, Dict]:
|
||||
def save_file(self, file: FileStorage, name: str) -> Tuple[str, Dict]:
|
||||
"""Save file and return path and metadata"""
|
||||
try:
|
||||
filename = secure_filename(filename)
|
||||
filename = safe_filename(name)
|
||||
if not filename:
|
||||
raise ValueError("Invalid filename")
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ class AttachmentManager:
|
|||
return file_path, metadata
|
||||
|
||||
except Exception as e:
|
||||
PrintStyle.error(f"Error saving file {filename}: {e}")
|
||||
PrintStyle.error(f"Error saving file {name}: {e}")
|
||||
return None, {} # type: ignore
|
||||
|
||||
def generate_image_preview(self, image_path: str, max_size: int = 800) -> Optional[str]:
|
||||
|
|
|
|||
|
|
@ -60,28 +60,12 @@ class BackupService:
|
|||
# Ensure paths don't have double slashes
|
||||
agent_root = self.agent_zero_root.rstrip('/')
|
||||
|
||||
return f"""# Agent Zero Knowledge (excluding defaults)
|
||||
{agent_root}/knowledge/**
|
||||
!{agent_root}/knowledge/default/**
|
||||
|
||||
# Agent Zero Skills (excluding builtins)
|
||||
{agent_root}/skills/**
|
||||
!{agent_root}/skills/builtin/**
|
||||
|
||||
# Memory (excluding embeddings cache)
|
||||
{agent_root}/memory/**
|
||||
!{agent_root}/memory/**/embeddings/**
|
||||
|
||||
# Configuration and Settings (CRITICAL)
|
||||
{agent_root}/.env
|
||||
{agent_root}/tmp/settings.json
|
||||
{agent_root}/tmp/secrets.env
|
||||
{agent_root}/tmp/chats/**
|
||||
{agent_root}/tmp/scheduler/**
|
||||
{agent_root}/tmp/uploads/**
|
||||
|
||||
# User data
|
||||
return f"""# User data
|
||||
# All persistent user data is now centralized in /usr for easier backup and restore
|
||||
{agent_root}/usr/**
|
||||
|
||||
# Explicitly include .env
|
||||
{agent_root}/usr/.env
|
||||
"""
|
||||
|
||||
def _get_agent_zero_version(self) -> str:
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class DockerContainerManager:
|
|||
if existing_container:
|
||||
if existing_container.status != 'running':
|
||||
PrintStyle.standard(f"Starting existing container: {self.name} for safe code execution...")
|
||||
if self.logger: self.logger.log(type="info", content=f"Starting existing container: {self.name} for safe code execution...", temp=True)
|
||||
if self.logger: self.logger.log(type="info", content=f"Starting existing container: {self.name} for safe code execution...")
|
||||
|
||||
existing_container.start()
|
||||
self.container = existing_container
|
||||
|
|
@ -84,7 +84,7 @@ class DockerContainerManager:
|
|||
# PrintStyle.standard(f"Container with name '{self.name}' is already running with ID: {existing_container.id}")
|
||||
else:
|
||||
PrintStyle.standard(f"Initializing docker container {self.name} for safe code execution...")
|
||||
if self.logger: self.logger.log(type="info", content=f"Initializing docker container {self.name} for safe code execution...", temp=True)
|
||||
if self.logger: self.logger.log(type="info", content=f"Initializing docker container {self.name} for safe code execution...")
|
||||
|
||||
self.container = self.client.containers.run(
|
||||
self.image,
|
||||
|
|
|
|||
|
|
@ -359,6 +359,7 @@ class DocumentQueryHelper:
|
|||
self.agent = agent
|
||||
self.store = DocumentQueryStore.get(agent)
|
||||
self.progress_callback = progress_callback or (lambda x: None)
|
||||
self.store_lock = asyncio.Lock()
|
||||
|
||||
async def document_qa(
|
||||
self, document_uris: List[str], questions: Sequence[str]
|
||||
|
|
@ -527,9 +528,10 @@ class DocumentQueryHelper:
|
|||
if add_to_db:
|
||||
self.progress_callback(f"Indexing document")
|
||||
await self.agent.handle_intervention()
|
||||
success, ids = await self.store.add_document(
|
||||
document_content, document_uri_norm
|
||||
)
|
||||
async with self.store_lock:
|
||||
success, ids = await self.store.add_document(
|
||||
document_content, document_uri_norm
|
||||
)
|
||||
if not success:
|
||||
self.progress_callback(f"Failed to index document")
|
||||
raise ValueError(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def load_dotenv():
|
|||
|
||||
|
||||
def get_dotenv_file_path():
|
||||
return get_abs_path(".env")
|
||||
return get_abs_path("usr/.env")
|
||||
|
||||
def get_dotenv_value(key: str, default: Any = None):
|
||||
# load_dotenv()
|
||||
|
|
|
|||
|
|
@ -537,7 +537,7 @@ async def read_messages(
|
|||
port: int = 993,
|
||||
username: str = "",
|
||||
password: str = "",
|
||||
download_folder: str = "tmp/email",
|
||||
download_folder: str = "usr/email",
|
||||
options: Optional[Dict[str, Any]] = None,
|
||||
filter: Optional[Dict[str, Any]] = None,
|
||||
) -> List[Message]:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ def error_text(e: Exception):
|
|||
return str(e)
|
||||
|
||||
|
||||
def format_error(e: Exception, start_entries=6, end_entries=4):
|
||||
def format_error(e: Exception, start_entries=20, end_entries=15):
|
||||
# format traceback from the provided exception instead of the most recent one
|
||||
traceback_text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||
# Split the traceback into lines
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ class AgentZeroWorker(Worker): # type: ignore[misc]
|
|||
heading="Remote user message",
|
||||
content=agent_message.message,
|
||||
kvps={"from": "A2A"},
|
||||
temp=False,
|
||||
)
|
||||
|
||||
# Process message through Agent Zero (includes response)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import shutil
|
|||
import base64
|
||||
import subprocess
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from werkzeug.utils import secure_filename
|
||||
from python.helpers.security import safe_filename
|
||||
from datetime import datetime
|
||||
|
||||
from python.helpers import files
|
||||
|
|
@ -19,6 +19,7 @@ class FileBrowser:
|
|||
}
|
||||
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
MAX_TEXT_FILE_SIZE = 1 * 1024 * 1024 # 1MB
|
||||
|
||||
def __init__(self):
|
||||
# if runtime.is_development():
|
||||
|
|
@ -69,7 +70,9 @@ class FileBrowser:
|
|||
for file in files:
|
||||
try:
|
||||
if file and self._is_allowed_file(file.filename, file):
|
||||
filename = secure_filename(file.filename)
|
||||
filename = safe_filename(file.filename)
|
||||
if not filename:
|
||||
raise ValueError("Invalid filename")
|
||||
file_path = target_dir / filename
|
||||
|
||||
file.save(str(file_path))
|
||||
|
|
@ -107,6 +110,78 @@ class FileBrowser:
|
|||
PrintStyle.error(f"Error deleting {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def rename_item(self, file_path: str, new_name: str) -> bool:
|
||||
try:
|
||||
if not new_name or new_name in {".", ".."}:
|
||||
raise ValueError("Invalid new name")
|
||||
if "/" in new_name or "\\" in new_name:
|
||||
raise ValueError("New name cannot include path separators")
|
||||
|
||||
full_path = (self.base_dir / file_path).resolve()
|
||||
if not str(full_path).startswith(str(self.base_dir)):
|
||||
raise ValueError("Invalid path")
|
||||
if not full_path.exists():
|
||||
raise FileNotFoundError("File or folder not found")
|
||||
|
||||
new_path = full_path.with_name(new_name)
|
||||
if not str(new_path).startswith(str(self.base_dir)):
|
||||
raise ValueError("Invalid target path")
|
||||
if full_path == new_path:
|
||||
return True
|
||||
if new_path.exists():
|
||||
raise FileExistsError("Target already exists")
|
||||
|
||||
os.rename(full_path, new_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
PrintStyle.error(f"Error renaming {file_path}: {e}")
|
||||
raise
|
||||
|
||||
def create_folder(self, parent_path: str, folder_name: str) -> bool:
|
||||
try:
|
||||
if not folder_name or folder_name in {".", ".."}:
|
||||
raise ValueError("Invalid folder name")
|
||||
if "/" in folder_name or "\\" in folder_name:
|
||||
raise ValueError("Folder name cannot include path separators")
|
||||
|
||||
parent_full = (self.base_dir / parent_path).resolve()
|
||||
if not str(parent_full).startswith(str(self.base_dir)):
|
||||
raise ValueError("Invalid parent path")
|
||||
|
||||
target_dir = (parent_full / folder_name).resolve()
|
||||
if not str(target_dir).startswith(str(self.base_dir)):
|
||||
raise ValueError("Invalid target path")
|
||||
if target_dir.exists():
|
||||
raise FileExistsError("Folder already exists")
|
||||
|
||||
os.makedirs(target_dir, exist_ok=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
PrintStyle.error(f"Error creating folder {folder_name}: {e}")
|
||||
raise
|
||||
|
||||
def save_text_file(self, file_path: str, content: str) -> bool:
|
||||
try:
|
||||
if not isinstance(content, str):
|
||||
raise ValueError("Content must be a string")
|
||||
content_size = len(content.encode("utf-8"))
|
||||
if content_size > self.MAX_TEXT_FILE_SIZE:
|
||||
raise ValueError("File exceeds 1 MB and cannot be edited")
|
||||
|
||||
full_path = (self.base_dir / file_path).resolve()
|
||||
if not str(full_path).startswith(str(self.base_dir)):
|
||||
raise ValueError("Invalid path")
|
||||
if full_path.exists() and full_path.is_dir():
|
||||
raise ValueError("Target is a directory")
|
||||
|
||||
os.makedirs(full_path.parent, exist_ok=True)
|
||||
with open(full_path, "w", encoding="utf-8") as file:
|
||||
file.write(content)
|
||||
return True
|
||||
except Exception as e:
|
||||
PrintStyle.error(f"Error saving file {file_path}: {e}")
|
||||
raise
|
||||
|
||||
def _is_allowed_file(self, filename: str, file) -> bool:
|
||||
# allow any file to be uploaded in file browser
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,42 @@ def read_file_base64(relative_path):
|
|||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
|
||||
def is_probably_binary_bytes(data: bytes, threshold: float = 0.3) -> bool:
|
||||
"""
|
||||
Binary detection.
|
||||
|
||||
- Fast path: NUL bytes => binary
|
||||
- Otherwise: treat high ratio of suspicious ASCII control bytes as binary.
|
||||
(We intentionally do NOT treat bytes >= 0x80 as binary to avoid false
|
||||
positives for UTF-8 text.)
|
||||
"""
|
||||
if not data:
|
||||
return False
|
||||
if b"\x00" in data:
|
||||
return True
|
||||
|
||||
# Count suspicious control bytes
|
||||
allowed = {8, 9, 10, 12, 13} # \b \t \n \f \r
|
||||
suspicious = sum(
|
||||
1
|
||||
for b in data
|
||||
if ((b < 32 and b not in allowed) or b == 127)
|
||||
)
|
||||
return (suspicious / len(data)) > threshold
|
||||
|
||||
|
||||
def is_probably_binary_file(
|
||||
file_path: str, sample_size: int = 10 * 1024, threshold: float = 0.3
|
||||
) -> bool:
|
||||
"""Binary detection by reading only the first ~sample_size bytes of a file."""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
sample = f.read(sample_size)
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
raise OSError(f"Unable to read file for binary detection: {file_path}")
|
||||
return is_probably_binary_bytes(sample, threshold=threshold)
|
||||
|
||||
|
||||
def replace_placeholders_text(_content: str, **kwargs):
|
||||
# Replace placeholders with values from kwargs
|
||||
for key, value in kwargs.items():
|
||||
|
|
@ -410,6 +446,10 @@ def move_dir(old_path: str, new_path: str):
|
|||
abs_new = get_abs_path(new_path)
|
||||
if not os.path.isdir(abs_old):
|
||||
return # nothing to rename
|
||||
|
||||
# ensure parent directory exists
|
||||
os.makedirs(os.path.dirname(abs_new), exist_ok=True)
|
||||
|
||||
try:
|
||||
os.rename(abs_old, abs_new)
|
||||
except Exception:
|
||||
|
|
@ -505,12 +545,14 @@ def dirname(path: str):
|
|||
|
||||
|
||||
def is_in_base_dir(path: str):
|
||||
# check if the given path is within the base directory
|
||||
base_dir = get_base_dir()
|
||||
# normalize paths to handle relative paths and symlinks
|
||||
return is_in_dir(path,get_base_dir())
|
||||
|
||||
|
||||
def is_in_dir(path:str,dir:str):
|
||||
# check if the given path is within the directory
|
||||
abs_path = os.path.abspath(path)
|
||||
# check if the absolute path starts with the base directory
|
||||
return os.path.commonpath([abs_path, base_dir]) == base_dir
|
||||
abs_dir = os.path.abspath(dir)
|
||||
return os.path.commonpath([abs_path, abs_dir]) == abs_dir
|
||||
|
||||
|
||||
def get_subdirectories(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from dataclasses import dataclass, field
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Literal, Optional, Dict, TypeVar, TYPE_CHECKING
|
||||
|
||||
T = TypeVar("T")
|
||||
|
|
@ -20,12 +21,14 @@ Type = Literal[
|
|||
"agent",
|
||||
"browser",
|
||||
"code_exe",
|
||||
"subagent",
|
||||
"error",
|
||||
"hint",
|
||||
"info",
|
||||
"progress",
|
||||
"response",
|
||||
"tool",
|
||||
"mcp",
|
||||
"input",
|
||||
"user",
|
||||
"util",
|
||||
|
|
@ -126,14 +129,16 @@ class LogItem:
|
|||
type: Type
|
||||
heading: str = ""
|
||||
content: str = ""
|
||||
temp: bool = False
|
||||
update_progress: Optional[ProgressUpdate] = "persistent"
|
||||
kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
|
||||
id: Optional[str] = None # Add id field
|
||||
guid: str = ""
|
||||
timestamp: float = 0.0
|
||||
agentno: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
self.guid = self.log.guid
|
||||
self.timestamp = self.timestamp or time.time()
|
||||
|
||||
def update(
|
||||
self,
|
||||
|
|
@ -141,7 +146,6 @@ class LogItem:
|
|||
heading: str | None = None,
|
||||
content: str | None = None,
|
||||
kvps: dict | None = None,
|
||||
temp: bool | None = None,
|
||||
update_progress: ProgressUpdate | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
|
|
@ -152,7 +156,6 @@ class LogItem:
|
|||
heading=heading,
|
||||
content=content,
|
||||
kvps=kvps,
|
||||
temp=temp,
|
||||
update_progress=update_progress,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -179,8 +182,9 @@ class LogItem:
|
|||
"type": self.type,
|
||||
"heading": self.heading,
|
||||
"content": self.content,
|
||||
"temp": self.temp,
|
||||
"kvps": self.kvps,
|
||||
"timestamp": self.timestamp,
|
||||
"agentno": self.agentno,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -199,18 +203,27 @@ class Log:
|
|||
heading: str | None = None,
|
||||
content: str | None = None,
|
||||
kvps: dict | None = None,
|
||||
temp: bool | None = None,
|
||||
update_progress: ProgressUpdate | None = None,
|
||||
id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> LogItem:
|
||||
|
||||
# add a minimal item to the log
|
||||
# Determine agent number from streaming agent
|
||||
agentno = 0
|
||||
if self.context and self.context.streaming_agent:
|
||||
agentno = self.context.streaming_agent.number
|
||||
|
||||
item = LogItem(
|
||||
log=self,
|
||||
no=len(self.logs),
|
||||
type=type,
|
||||
agentno=agentno,
|
||||
)
|
||||
# Set duration on previous item and mark it as updated
|
||||
if self.logs:
|
||||
prev = self.logs[-1]
|
||||
self.updates += [prev.no]
|
||||
self.logs.append(item)
|
||||
|
||||
# and update it (to have just one implementation)
|
||||
|
|
@ -220,7 +233,6 @@ class Log:
|
|||
heading=heading,
|
||||
content=content,
|
||||
kvps=kvps,
|
||||
temp=temp,
|
||||
update_progress=update_progress,
|
||||
id=id,
|
||||
**kwargs,
|
||||
|
|
@ -234,7 +246,6 @@ class Log:
|
|||
heading: str | None = None,
|
||||
content: str | None = None,
|
||||
kvps: dict | None = None,
|
||||
temp: bool | None = None,
|
||||
update_progress: ProgressUpdate | None = None,
|
||||
id: Optional[str] = None,
|
||||
**kwargs,
|
||||
|
|
@ -247,9 +258,6 @@ class Log:
|
|||
if type is not None:
|
||||
item.type = type
|
||||
|
||||
if temp is not None:
|
||||
item.temp = temp
|
||||
|
||||
if update_progress is not None:
|
||||
item.update_progress = update_progress
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from datetime import timedelta
|
|||
import json
|
||||
from python.helpers import errors
|
||||
from python.helpers import settings
|
||||
from python.helpers.log import LogItem
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -90,7 +91,6 @@ def initialize_mcp(mcp_servers_config: str):
|
|||
AgentContext.log_to_all(
|
||||
type="warning",
|
||||
content=f"Failed to update MCP settings: {e}",
|
||||
temp=False,
|
||||
)
|
||||
|
||||
PrintStyle(
|
||||
|
|
@ -101,6 +101,14 @@ def initialize_mcp(mcp_servers_config: str):
|
|||
class MCPTool(Tool):
|
||||
"""MCP Tool wrapper"""
|
||||
|
||||
def get_log_object(self) -> LogItem:
|
||||
return self.agent.context.log.log(
|
||||
type="mcp",
|
||||
heading=f"icon://extension {self.agent.agent_name}: Using MCP tool '{self.name}'",
|
||||
content="",
|
||||
kvps={"tool_name": self.name, **self.args},
|
||||
)
|
||||
|
||||
async def execute(self, **kwargs: Any):
|
||||
error = ""
|
||||
try:
|
||||
|
|
@ -1071,9 +1079,9 @@ class MCPClientRemote(MCPClientBase):
|
|||
server: MCPServerRemote = cast(MCPServerRemote, self.server)
|
||||
set = settings.get_settings()
|
||||
|
||||
# Use lower timeouts for faster failure detection
|
||||
init_timeout = min(server.init_timeout or set["mcp_client_init_timeout"], 5)
|
||||
tool_timeout = min(server.tool_timeout or set["mcp_client_tool_timeout"], 10)
|
||||
# Resolve timeout: check server config first, then settings, defaulting to 5s/10s
|
||||
init_timeout = server.init_timeout or set["mcp_client_init_timeout"] or 5
|
||||
tool_timeout = server.tool_timeout or set["mcp_client_tool_timeout"] or 10
|
||||
|
||||
client_factory = CustomHTTPClientFactory(verify=server.verify)
|
||||
# Check if this is a streaming HTTP type
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class Memory:
|
|||
log_item.stream(progress="\nInitializing VectorDB")
|
||||
|
||||
em_dir = files.get_abs_path(
|
||||
"memory/embeddings"
|
||||
"tmp/memory/embeddings"
|
||||
) # just caching, no need to parameterize
|
||||
db_dir = abs_db_dir(memory_subdir)
|
||||
|
||||
|
|
@ -335,6 +335,16 @@ class Memory:
|
|||
filename_pattern="**/SKILL.md",
|
||||
)
|
||||
|
||||
# load custom instruments descriptions
|
||||
index = knowledge_import.load_knowledge(
|
||||
log_item,
|
||||
files.get_abs_path("usr/instruments"),
|
||||
index,
|
||||
{"area": Memory.Area.INSTRUMENTS.value},
|
||||
filename_pattern="**/*.md",
|
||||
recursive=True,
|
||||
)
|
||||
|
||||
return index
|
||||
|
||||
def get_document_by_id(self, id: str) -> Document | None:
|
||||
|
|
@ -485,7 +495,9 @@ class Memory:
|
|||
def get_custom_knowledge_subdir_abs(agent: Agent) -> str:
|
||||
for dir in agent.config.knowledge_subdirs:
|
||||
if dir != "default":
|
||||
return files.get_abs_path("knowledge", dir)
|
||||
if dir == "custom":
|
||||
return files.get_abs_path("usr/knowledge")
|
||||
return files.get_abs_path("usr/knowledge", dir)
|
||||
raise Exception("No custom knowledge subdir set")
|
||||
|
||||
|
||||
|
|
@ -501,7 +513,7 @@ def abs_db_dir(memory_subdir: str) -> str:
|
|||
|
||||
return files.get_abs_path(get_project_meta_folder(memory_subdir[9:]), "memory")
|
||||
# standard subdirs
|
||||
return files.get_abs_path("memory", memory_subdir)
|
||||
return files.get_abs_path("usr/memory", memory_subdir)
|
||||
|
||||
|
||||
def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str:
|
||||
|
|
@ -513,7 +525,11 @@ def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str:
|
|||
get_project_meta_folder(knowledge_subdir[9:]), "knowledge", *sub_dirs
|
||||
)
|
||||
# standard subdirs
|
||||
return files.get_abs_path("knowledge", knowledge_subdir, *sub_dirs)
|
||||
if knowledge_subdir == "default":
|
||||
return files.get_abs_path("knowledge", *sub_dirs)
|
||||
if knowledge_subdir == "custom":
|
||||
return files.get_abs_path("usr/knowledge", *sub_dirs)
|
||||
return files.get_abs_path("usr/knowledge", knowledge_subdir, *sub_dirs)
|
||||
|
||||
|
||||
def get_memory_subdir_abs(agent: Agent) -> str:
|
||||
|
|
@ -548,7 +564,7 @@ def get_existing_memory_subdirs() -> list[str]:
|
|||
)
|
||||
|
||||
# Get subdirectories from memory folder
|
||||
subdirs = files.get_subdirectories("memory", exclude="embeddings")
|
||||
subdirs = files.get_subdirectories("usr/memory")
|
||||
|
||||
project_subdirs = files.get_subdirectories(get_projects_parent_folder())
|
||||
for project_subdir in project_subdirs:
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ class MemoryConsolidator:
|
|||
if log_item:
|
||||
log_item.update(
|
||||
progress="No similar memories found, inserting new memory",
|
||||
temp=True
|
||||
)
|
||||
try:
|
||||
db = await Memory.get(self.agent)
|
||||
|
|
@ -153,7 +152,6 @@ class MemoryConsolidator:
|
|||
if log_item:
|
||||
log_item.update(
|
||||
progress=f"Found {len(similar_memories)} similar memories, analyzing...",
|
||||
temp=True,
|
||||
similar_memories_count=len(similar_memories)
|
||||
)
|
||||
|
||||
|
|
@ -174,7 +172,6 @@ class MemoryConsolidator:
|
|||
if log_item:
|
||||
log_item.update(
|
||||
progress=f"Filtered out {deleted_count} deleted memories, {len(valid_similar_memories)} remain for analysis",
|
||||
temp=True,
|
||||
race_condition_detected=True,
|
||||
deleted_similar_memories_count=deleted_count
|
||||
)
|
||||
|
|
@ -185,7 +182,6 @@ class MemoryConsolidator:
|
|||
if log_item:
|
||||
log_item.update(
|
||||
progress="No valid similar memories remain, inserting new memory",
|
||||
temp=True
|
||||
)
|
||||
try:
|
||||
db = await Memory.get(self.agent)
|
||||
|
|
@ -220,7 +216,6 @@ class MemoryConsolidator:
|
|||
if log_item:
|
||||
log_item.update(
|
||||
progress="LLM analysis suggests skipping consolidation",
|
||||
temp=True
|
||||
)
|
||||
try:
|
||||
db = await Memory.get(self.agent)
|
||||
|
|
|
|||
182
python/helpers/message_queue.py
Normal file
182
python/helpers/message_queue.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import os
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent import AgentContext
|
||||
|
||||
from python.helpers.print_style import PrintStyle
|
||||
|
||||
QUEUE_KEY = "message_queue"
|
||||
QUEUE_SEQ_KEY = "message_queue_seq"
|
||||
UPLOAD_FOLDER = "/a0/tmp/uploads"
|
||||
|
||||
|
||||
def get_queue(context: "AgentContext") -> list:
|
||||
"""Get current queue from context.data."""
|
||||
return context.get_data(QUEUE_KEY) or []
|
||||
|
||||
|
||||
def _get_next_seq(context: "AgentContext") -> int:
|
||||
"""Get next sequence number."""
|
||||
seq = context.get_data(QUEUE_SEQ_KEY) or 0
|
||||
seq += 1
|
||||
context.set_data(QUEUE_SEQ_KEY, seq)
|
||||
return seq
|
||||
|
||||
|
||||
def _sync_output(context: "AgentContext"):
|
||||
"""Sync queue to output_data for frontend polling."""
|
||||
queue = get_queue(context)
|
||||
# Truncate text for frontend display
|
||||
truncated = []
|
||||
for item in queue:
|
||||
truncated.append({
|
||||
"id": item["id"],
|
||||
"seq": item.get("seq", 0),
|
||||
"text": item["text"][:100] + "..." if len(item["text"]) > 100 else item["text"],
|
||||
"attachments": [a.split("/")[-1] for a in item.get("attachments", [])],
|
||||
"attachment_count": len(item.get("attachments", [])),
|
||||
})
|
||||
context.set_output_data(QUEUE_KEY, truncated)
|
||||
|
||||
|
||||
def add(context: "AgentContext", text: str, attachments: list[str] | None = None) -> dict:
|
||||
"""Add message to queue. Attachments should be filenames, will be converted to full paths."""
|
||||
queue = get_queue(context)
|
||||
|
||||
# Convert filenames to full paths
|
||||
full_paths = []
|
||||
for att in (attachments or []):
|
||||
if att.startswith("/"):
|
||||
full_paths.append(att)
|
||||
else:
|
||||
full_paths.append(f"{UPLOAD_FOLDER}/{att}")
|
||||
|
||||
item = {
|
||||
"id": str(uuid.uuid4())[:8],
|
||||
"seq": _get_next_seq(context),
|
||||
"text": text,
|
||||
"attachments": full_paths,
|
||||
}
|
||||
queue.append(item)
|
||||
context.set_data(QUEUE_KEY, queue)
|
||||
_sync_output(context)
|
||||
return item
|
||||
|
||||
|
||||
def remove(context: "AgentContext", item_id: str | None = None) -> int:
|
||||
"""Remove item(s). If item_id is None, clears all. Returns remaining count."""
|
||||
if not item_id:
|
||||
context.set_data(QUEUE_KEY, [])
|
||||
context.set_output_data(QUEUE_KEY, [])
|
||||
return 0
|
||||
queue = [i for i in get_queue(context) if i["id"] != item_id]
|
||||
context.set_data(QUEUE_KEY, queue)
|
||||
_sync_output(context)
|
||||
return len(queue)
|
||||
|
||||
|
||||
def pop_first(context: "AgentContext") -> dict | None:
|
||||
"""Remove and return first item."""
|
||||
queue = get_queue(context)
|
||||
if not queue:
|
||||
return None
|
||||
item = queue.pop(0)
|
||||
context.set_data(QUEUE_KEY, queue)
|
||||
_sync_output(context)
|
||||
return item
|
||||
|
||||
|
||||
def pop_item(context: "AgentContext", item_id: str) -> dict | None:
|
||||
"""Remove and return specific item."""
|
||||
queue = get_queue(context)
|
||||
for i, item in enumerate(queue):
|
||||
if item["id"] == item_id:
|
||||
queue.pop(i)
|
||||
context.set_data(QUEUE_KEY, queue)
|
||||
_sync_output(context)
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def has_queue(context: "AgentContext") -> bool:
|
||||
"""Check if queue has items."""
|
||||
return len(get_queue(context)) > 0
|
||||
|
||||
|
||||
def log_user_message(
|
||||
context: "AgentContext",
|
||||
message: str,
|
||||
attachment_paths: list[str],
|
||||
message_id: str | None = None,
|
||||
source: str = "",
|
||||
):
|
||||
"""Log user message to console and UI. Used by message API and queue processing."""
|
||||
# Prepare attachment filenames for logging
|
||||
attachment_filenames = (
|
||||
[os.path.basename(path) for path in attachment_paths]
|
||||
if attachment_paths
|
||||
else []
|
||||
)
|
||||
|
||||
# Print to console
|
||||
label = f"User message{source}:"
|
||||
PrintStyle(
|
||||
background_color="#6C3483", font_color="white", bold=True, padding=True
|
||||
).print(label)
|
||||
PrintStyle(font_color="white", padding=False).print(f"> {message}")
|
||||
if attachment_filenames:
|
||||
PrintStyle(font_color="white", padding=False).print("Attachments:")
|
||||
for filename in attachment_filenames:
|
||||
PrintStyle(font_color="white", padding=False).print(f"- {filename}")
|
||||
|
||||
# Log to UI
|
||||
context.log.log(
|
||||
type="user",
|
||||
heading="",
|
||||
content=message,
|
||||
kvps={"attachments": attachment_filenames},
|
||||
id=message_id,
|
||||
)
|
||||
|
||||
|
||||
def send_message(context: "AgentContext", item: dict, source: str = " (from queue)"):
|
||||
"""Send a single queued message (log + communicate)."""
|
||||
from agent import UserMessage # Import here to avoid circular import
|
||||
|
||||
message = item.get("text", "")
|
||||
attachments = item.get("attachments", [])
|
||||
log_user_message(context, message, attachments, source=source)
|
||||
context.communicate(UserMessage(message, attachments))
|
||||
|
||||
|
||||
def send_next(context: "AgentContext") -> bool:
|
||||
"""Send next queued message. Returns True if sent, False if queue empty."""
|
||||
if not has_queue(context):
|
||||
return False
|
||||
item = pop_first(context)
|
||||
if item:
|
||||
send_message(context, item)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def send_all_aggregated(context: "AgentContext") -> int:
|
||||
"""Aggregate and send all queued messages as one. Returns count of items sent."""
|
||||
from agent import UserMessage # Import here to avoid circular import
|
||||
|
||||
if not has_queue(context):
|
||||
return 0
|
||||
|
||||
items = []
|
||||
while has_queue(context):
|
||||
items.append(pop_first(context))
|
||||
|
||||
# Combine texts with separator
|
||||
text = "\n\n---\n\n".join(i["text"] for i in items if i["text"])
|
||||
attachments = [a for i in items for a in i.get("attachments", [])]
|
||||
|
||||
log_user_message(context, text, attachments, source=" (queued batch)")
|
||||
context.communicate(UserMessage(text, attachments))
|
||||
return len(items)
|
||||
112
python/helpers/migration.py
Normal file
112
python/helpers/migration.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import os
|
||||
from python.helpers import files
|
||||
from python.helpers.print_style import PrintStyle
|
||||
|
||||
def migrate_user_data() -> None:
|
||||
"""
|
||||
Migrate user data from /tmp and other locations to /usr.
|
||||
"""
|
||||
|
||||
PrintStyle().print("Checking for data migration...")
|
||||
|
||||
# --- Migrate Directories -------------------------------------------------------
|
||||
# Move directories from tmp/ or other source locations to usr/
|
||||
|
||||
_move_dir("tmp/chats", "usr/chats")
|
||||
_move_dir("tmp/scheduler", "usr/scheduler", overwrite=True)
|
||||
_move_dir("tmp/uploads", "usr/uploads")
|
||||
_move_dir("tmp/upload", "usr/upload")
|
||||
_move_dir("tmp/downloads", "usr/downloads")
|
||||
_move_dir("tmp/email", "usr/email")
|
||||
_move_dir("knowledge/custom", "usr/knowledge", overwrite=True)
|
||||
_move_dir("instruments/custom", "usr/instruments", overwrite=True)
|
||||
|
||||
# --- Migrate Files -------------------------------------------------------------
|
||||
# Move specific configuration files to usr/
|
||||
|
||||
_move_file("tmp/settings.json", "usr/settings.json")
|
||||
_move_file("tmp/secrets.env", "usr/secrets.env")
|
||||
_move_file(".env", "usr/.env", overwrite=True)
|
||||
|
||||
# --- Special Migration Cases ---------------------------------------------------
|
||||
|
||||
# Migrate Memory
|
||||
_migrate_memory()
|
||||
|
||||
# Flatten default directories (knowledge/default -> knowledge/, etc.)
|
||||
# We use _merge_dir_contents because we want to move the *contents* of default/
|
||||
# into the parent directory, not move the default directory itself.
|
||||
_merge_dir_contents("knowledge/default", "knowledge")
|
||||
_merge_dir_contents("instruments/default", "instruments")
|
||||
|
||||
# --- Cleanup -------------------------------------------------------------------
|
||||
|
||||
# Remove obsolete directories after migration
|
||||
_cleanup_obsolete()
|
||||
|
||||
PrintStyle().print("Migration check complete.")
|
||||
|
||||
# --- Helper Functions ----------------------------------------------------------
|
||||
|
||||
def _move_dir(src: str, dst: str, overwrite: bool = False) -> None:
|
||||
"""
|
||||
Move a directory from src to dst if src exists and dst does not.
|
||||
"""
|
||||
if files.exists(src) and (not files.exists(dst) or overwrite):
|
||||
PrintStyle().print(f"Migrating {src} to {dst}...")
|
||||
if overwrite and files.exists(dst):
|
||||
files.delete_dir(dst)
|
||||
files.move_dir(src, dst)
|
||||
|
||||
def _move_file(src: str, dst: str, overwrite: bool = False) -> None:
|
||||
"""
|
||||
Move a file from src to dst if src exists and dst does not.
|
||||
"""
|
||||
if files.exists(src) and (not files.exists(dst) or overwrite):
|
||||
PrintStyle().print(f"Migrating {src} to {dst}...")
|
||||
files.move_file(src, dst)
|
||||
|
||||
def _migrate_memory(base_path: str = "memory") -> None:
|
||||
"""
|
||||
Migrate memory subdirectories.
|
||||
"""
|
||||
subdirs = files.get_subdirectories(base_path)
|
||||
for subdir in subdirs:
|
||||
if subdir == "embeddings":
|
||||
# Special case: Embeddings
|
||||
_move_dir("memory/embeddings", "tmp/memory/embeddings")
|
||||
else:
|
||||
# Move other memory items to usr/memory
|
||||
dst = f"usr/memory/{subdir}"
|
||||
_move_dir(f"memory/{subdir}", dst)
|
||||
|
||||
def _merge_dir_contents(src_parent: str, dst_parent: str) -> None:
|
||||
"""
|
||||
Moves all subdirectories from src_parent to dst_parent.
|
||||
Useful for flattening structures like 'knowledge/default/*' -> 'knowledge/*'.
|
||||
"""
|
||||
if not files.exists(src_parent):
|
||||
return
|
||||
|
||||
# Iterate over subdirectories in the source parent
|
||||
subdirs = files.get_subdirectories(src_parent)
|
||||
for subdir in subdirs:
|
||||
src = f"{src_parent}/{subdir}"
|
||||
dst = f"{dst_parent}/{subdir}"
|
||||
|
||||
# Move the subdirectory if it doesn't exist in destination
|
||||
_move_dir(src, dst)
|
||||
|
||||
def _cleanup_obsolete() -> None:
|
||||
"""
|
||||
Remove directories that are no longer needed.
|
||||
"""
|
||||
to_remove = [
|
||||
"knowledge/default",
|
||||
"instruments/default",
|
||||
"memory"
|
||||
]
|
||||
for path in to_remove:
|
||||
if files.exists(path):
|
||||
PrintStyle().print(f"Removing {path}...")
|
||||
files.delete_dir(path)
|
||||
|
|
@ -9,7 +9,7 @@ from initialize import initialize_agent
|
|||
|
||||
from python.helpers.log import Log, LogItem
|
||||
|
||||
CHATS_FOLDER = "tmp/chats"
|
||||
CHATS_FOLDER = "usr/chats"
|
||||
LOG_SIZE = 1000
|
||||
CHAT_FILE_NAME = "chat.json"
|
||||
|
||||
|
|
@ -262,17 +262,17 @@ def _deserialize_log(data: dict[str, Any]) -> "Log":
|
|||
# Deserialize the list of LogItem objects
|
||||
i = 0
|
||||
for item_data in data.get("logs", []):
|
||||
log.logs.append(
|
||||
LogItem(
|
||||
log=log, # restore the log reference
|
||||
no=i, # item_data["no"],
|
||||
type=item_data["type"],
|
||||
heading=item_data.get("heading", ""),
|
||||
content=item_data.get("content", ""),
|
||||
kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
|
||||
temp=item_data.get("temp", False),
|
||||
)
|
||||
)
|
||||
log.logs.append(LogItem(
|
||||
log=log, # restore the log reference
|
||||
no=i, # item_data["no"],
|
||||
type=item_data["type"],
|
||||
heading=item_data.get("heading", ""),
|
||||
content=item_data.get("content", ""),
|
||||
kvps=OrderedDict(item_data["kvps"]) if item_data["kvps"] else None,
|
||||
timestamp=item_data.get("timestamp", 0.0),
|
||||
agentno=item_data.get("agentno", 0),
|
||||
id=item_data.get("id"),
|
||||
))
|
||||
log.updates.append(i)
|
||||
i += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import yaml
|
||||
from python.helpers import files
|
||||
from typing import List, Dict, Optional, TypedDict
|
||||
from typing import List, Dict, Optional, TypedDict, Literal
|
||||
|
||||
ModelType = Literal["chat", "embedding"]
|
||||
|
||||
# Type alias for UI option items
|
||||
class FieldOption(TypedDict):
|
||||
|
|
@ -68,16 +69,15 @@ class ProviderManager:
|
|||
opts.append({"value": pid, "label": name})
|
||||
self._options[p_type] = opts
|
||||
|
||||
def get_providers(self, provider_type: str) -> List[FieldOption]:
|
||||
def get_providers(self, provider_type: ModelType) -> List[FieldOption]:
|
||||
"""Returns a list of providers for a given type (e.g., 'chat', 'embedding')."""
|
||||
return self._options.get(provider_type, []) if self._options else []
|
||||
|
||||
|
||||
def get_raw_providers(self, provider_type: str) -> List[Dict[str, str]]:
|
||||
def get_raw_providers(self, provider_type: ModelType) -> List[Dict[str, str]]:
|
||||
"""Return raw provider dictionaries for advanced use-cases."""
|
||||
return self._raw.get(provider_type, []) if self._raw else []
|
||||
|
||||
def get_provider_config(self, provider_type: str, provider_id: str) -> Optional[Dict[str, str]]:
|
||||
def get_provider_config(self, provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]:
|
||||
"""Return the metadata dict for a single provider id (case-insensitive)."""
|
||||
provider_id_low = provider_id.lower()
|
||||
for p in self.get_raw_providers(provider_type):
|
||||
|
|
@ -86,16 +86,16 @@ class ProviderManager:
|
|||
return None
|
||||
|
||||
|
||||
def get_providers(provider_type: str) -> List[FieldOption]:
|
||||
def get_providers(provider_type: ModelType) -> List[FieldOption]:
|
||||
"""Convenience function to get providers of a specific type."""
|
||||
return ProviderManager.get_instance().get_providers(provider_type)
|
||||
|
||||
|
||||
def get_raw_providers(provider_type: str) -> List[Dict[str, str]]:
|
||||
def get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]:
|
||||
"""Return full metadata for providers of a given type."""
|
||||
return ProviderManager.get_instance().get_raw_providers(provider_type)
|
||||
|
||||
|
||||
def get_provider_config(provider_type: str, provider_id: str) -> Optional[Dict[str, str]]:
|
||||
def get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]:
|
||||
"""Return metadata for a single provider (None if not found)."""
|
||||
return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
|
||||
return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
|
||||
|
|
@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||
|
||||
# New alias-based placeholder format §§secret(KEY)
|
||||
ALIAS_PATTERN = r"§§secret\(([A-Za-z_][A-Za-z0-9_]*)\)"
|
||||
DEFAULT_SECRETS_FILE = "tmp/secrets.env"
|
||||
DEFAULT_SECRETS_FILE = "usr/secrets.env"
|
||||
|
||||
|
||||
def alias_for_key(key: str, placeholder: str = "§§secret({key})") -> str:
|
||||
|
|
|
|||
49
python/helpers/security.py
Normal file
49
python/helpers/security.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import re
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from typing import Final, Optional
|
||||
|
||||
# Forbidden characters:
|
||||
# Linux/Unix: / and NULL byte
|
||||
# Windows: < > : " / \ | ? * and ASCII control characters (0-31)
|
||||
# Shell-sensitive: ~ to prevent accidental home directory access
|
||||
FORBIDDEN_CHARS_RE: Final = re.compile(r'[<>:"|?*~/\\\x00-\x1f\x7f]')
|
||||
|
||||
# Windows reserved filenames
|
||||
WINDOWS_RESERVED: Final = frozenset({
|
||||
"CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
||||
})
|
||||
|
||||
FILENAME_MAX_LENGTH: Final = 255
|
||||
|
||||
def safe_filename(filename: str) -> Optional[str]:
|
||||
# Normalize Unicode (NFC)
|
||||
filename = unicodedata.normalize("NFC", str(filename))
|
||||
# Replace forbidden chars
|
||||
filename = FORBIDDEN_CHARS_RE.sub("_", filename)
|
||||
# Remove leading/trailing spaces and trailing dots
|
||||
filename = filename.lstrip(" ").rstrip(". ")
|
||||
|
||||
path = Path(filename)
|
||||
suffixes = ''.join(path.suffixes)
|
||||
stem = path.name[:-len(suffixes)] if suffixes else path.name
|
||||
|
||||
# Check Windows reserved names
|
||||
if stem.upper() in WINDOWS_RESERVED:
|
||||
filename = f"{stem}-{suffixes}"
|
||||
|
||||
# Truncate if too long
|
||||
if len(filename) > FILENAME_MAX_LENGTH:
|
||||
max_stem_len = FILENAME_MAX_LENGTH - len(suffixes)
|
||||
if max_stem_len > 0:
|
||||
# Truncate filename
|
||||
stem = stem[:max_stem_len]
|
||||
filename = stem + suffixes
|
||||
else:
|
||||
# Extension is too long, truncate everything
|
||||
filename = filename[:FILENAME_MAX_LENGTH]
|
||||
if not filename:
|
||||
return None
|
||||
return filename
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -82,7 +82,6 @@ class SSHInteractiveSession:
|
|||
self.logger.log(
|
||||
type="info",
|
||||
content=f"SSH Connection attempt {errors}...",
|
||||
temp=True,
|
||||
)
|
||||
time.sleep(5)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ from python.helpers.print_style import PrintStyle
|
|||
from python.helpers.defer import DeferredTask
|
||||
from python.helpers.files import get_abs_path, make_dirs, read_file, write_file
|
||||
from python.helpers.localization import Localization
|
||||
from python.helpers import projects
|
||||
from python.helpers import projects, guids
|
||||
import pytz
|
||||
from typing import Annotated
|
||||
|
||||
SCHEDULER_FOLDER = "tmp/scheduler"
|
||||
SCHEDULER_FOLDER = "usr/scheduler"
|
||||
|
||||
# ----------------------
|
||||
# Task Models
|
||||
|
|
@ -118,7 +118,7 @@ class TaskPlan(BaseModel):
|
|||
|
||||
|
||||
class BaseTask(BaseModel):
|
||||
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
uuid: str = Field(default_factory=lambda: guids.generate_id())
|
||||
context_id: Optional[str] = Field(default=None)
|
||||
state: TaskState = Field(default=TaskState.IDLE)
|
||||
name: str = Field()
|
||||
|
|
@ -874,7 +874,7 @@ class TaskScheduler:
|
|||
# Log the message with message_id and attachments
|
||||
context.log.log(
|
||||
type="user",
|
||||
heading="User message",
|
||||
heading="",
|
||||
content=task_prompt,
|
||||
kvps={"attachments": attachment_filenames},
|
||||
id=str(uuid.uuid4()),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
from flaredantic import FlareTunnel, FlareConfig, ServeoConfig, ServeoTunnel
|
||||
from flaredantic import (
|
||||
FlareTunnel, FlareConfig,
|
||||
ServeoConfig, ServeoTunnel,
|
||||
MicrosoftTunnel, MicrosoftConfig,
|
||||
notifier, NotifyData, NotifyEvent
|
||||
)
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
from python.helpers.print_style import PrintStyle
|
||||
|
||||
# Singleton to manage the tunnel instance
|
||||
class TunnelManager:
|
||||
|
|
@ -19,6 +26,35 @@ class TunnelManager:
|
|||
self.tunnel_url = None
|
||||
self.is_running = False
|
||||
self.provider = None
|
||||
self.notifications = deque(maxlen=50)
|
||||
self._subscribed = False
|
||||
|
||||
def _on_notify(self, data: NotifyData):
|
||||
"""Handle notifications from flaredantic"""
|
||||
self.notifications.append({
|
||||
"event": data.event.value,
|
||||
"message": data.message,
|
||||
"data": data.data
|
||||
})
|
||||
|
||||
def _ensure_subscribed(self):
|
||||
"""Subscribe to flaredantic notifications if not already"""
|
||||
if not self._subscribed:
|
||||
notifier.subscribe(self._on_notify)
|
||||
self._subscribed = True
|
||||
|
||||
def get_notifications(self):
|
||||
"""Get and clear pending notifications"""
|
||||
notifications = list(self.notifications)
|
||||
self.notifications.clear()
|
||||
return notifications
|
||||
|
||||
def get_last_error(self):
|
||||
"""Check for recent error in notifications without clearing"""
|
||||
for n in reversed(list(self.notifications)):
|
||||
if n['event'] == NotifyEvent.ERROR.value:
|
||||
return n['message']
|
||||
return None
|
||||
|
||||
def start_tunnel(self, port=80, provider="serveo"):
|
||||
"""Start a new tunnel or return the existing one's URL"""
|
||||
|
|
@ -26,6 +62,8 @@ class TunnelManager:
|
|||
return self.tunnel_url
|
||||
|
||||
self.provider = provider
|
||||
self._ensure_subscribed()
|
||||
self.notifications.clear()
|
||||
|
||||
try:
|
||||
# Start tunnel in a separate thread to avoid blocking
|
||||
|
|
@ -34,6 +72,9 @@ class TunnelManager:
|
|||
if self.provider == "cloudflared":
|
||||
config = FlareConfig(port=port, verbose=True)
|
||||
self.tunnel = FlareTunnel(config)
|
||||
elif self.provider == "microsoft":
|
||||
config = MicrosoftConfig(port=port, verbose=True) # type: ignore
|
||||
self.tunnel = MicrosoftTunnel(config)
|
||||
else: # Default to serveo
|
||||
config = ServeoConfig(port=port) # type: ignore
|
||||
self.tunnel = ServeoTunnel(config)
|
||||
|
|
@ -42,23 +83,34 @@ class TunnelManager:
|
|||
self.tunnel_url = self.tunnel.tunnel_url
|
||||
self.is_running = True
|
||||
except Exception as e:
|
||||
print(f"Error in tunnel thread: {str(e)}")
|
||||
error_msg = str(e)
|
||||
PrintStyle.error(f"Error in tunnel thread: {error_msg}")
|
||||
self.notifications.append({
|
||||
"event": NotifyEvent.ERROR.value,
|
||||
"message": error_msg,
|
||||
"data": None
|
||||
})
|
||||
|
||||
tunnel_thread = threading.Thread(target=run_tunnel)
|
||||
tunnel_thread.daemon = True
|
||||
tunnel_thread.start()
|
||||
|
||||
# Wait for tunnel to start (max 15 seconds instead of 5)
|
||||
for _ in range(150): # Increased from 50 to 150 iterations
|
||||
# Wait for tunnel to start (no timeout - user may need time for login)
|
||||
import time
|
||||
while True:
|
||||
if self.tunnel_url:
|
||||
break
|
||||
import time
|
||||
|
||||
# Check if we have errors
|
||||
if any(n['event'] == NotifyEvent.ERROR.value for n in self.notifications):
|
||||
break
|
||||
# Check if thread died without producing URL
|
||||
if not tunnel_thread.is_alive():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
return self.tunnel_url
|
||||
except Exception as e:
|
||||
print(f"Error starting tunnel: {str(e)}")
|
||||
PrintStyle.error(f"Error starting tunnel: {str(e)}")
|
||||
return None
|
||||
|
||||
def stop_tunnel(self):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from typing import Any, List, Sequence
|
||||
import uuid
|
||||
from langchain_community.vectorstores import FAISS
|
||||
|
||||
# faiss needs to be patched for python 3.12 on arm #TODO remove once not needed
|
||||
|
|
@ -17,6 +16,7 @@ from langchain.embeddings import CacheBackedEmbeddings
|
|||
from simpleeval import simple_eval
|
||||
|
||||
from agent import Agent
|
||||
from python.helpers import guids
|
||||
|
||||
|
||||
class MyFaiss(FAISS):
|
||||
|
|
@ -99,7 +99,7 @@ class VectorDB:
|
|||
return result
|
||||
|
||||
async def insert_documents(self, docs: list[Document]):
|
||||
ids = [str(uuid.uuid4()) for _ in range(len(docs))]
|
||||
ids = [guids.generate_id() for _ in range(len(docs))]
|
||||
|
||||
if ids:
|
||||
for doc, id in zip(docs, ids):
|
||||
|
|
@ -141,7 +141,7 @@ def cosine_normalizer(val: float) -> float:
|
|||
def get_comparator(condition: str):
|
||||
def comparator(data: dict[str, Any]):
|
||||
try:
|
||||
result = simple_eval(condition, {}, data)
|
||||
result = simple_eval(condition, names=data)
|
||||
return result
|
||||
except Exception as e:
|
||||
# PrintStyle.error(f"Error evaluating condition: {e}")
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class State:
|
|||
disable_security=True,
|
||||
chromium_sandbox=False,
|
||||
accept_downloads=True,
|
||||
downloads_path=files.get_abs_path("tmp/downloads"),
|
||||
downloads_path=files.get_abs_path("usr/downloads"),
|
||||
allowed_domains=["*", "http://*", "https://*"],
|
||||
executable_path=pw_binary,
|
||||
keep_alive=True,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class Delegation(Tool):
|
|||
|
||||
def get_log_object(self):
|
||||
return self.agent.context.log.log(
|
||||
type="tool",
|
||||
type="subagent",
|
||||
heading=f"icon://communication {self.agent.agent_name}: Calling Subordinate Agent",
|
||||
content="",
|
||||
kvps=self.args,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue