API separation

frdel committed Nov 21, 2024 at 18:47 UTC c0947e30c757ecdfd08117b12ffc4d1d08543c9e
28 files changed +681 -768
docker/exe/fs/exe/node_eval.js
+16 -11
@@ -4,20 +4,25 @@ const vm = require('vm');
4 const path = require('path');
5 const Module = require('module');
6
7 -// Enhance `require` to search CWD first, then globally
7 + // Enhance `require` to search CWD and parent directories first, then globally
8 function customRequire(moduleName) {
9 - try {
10 - // Try resolving from CWD's node_modules
11 - const cwdPath = path.resolve(process.cwd(), 'node_modules', moduleName);
12 - return require(cwdPath);
13 - } catch (cwdErr) {
9 + let currentDir = process.cwd();
10 + const root = path.parse(currentDir).root;
11 +
12 + do {
13 try {
15 - // Try resolving as a global module
16 - return require(moduleName);
17 - } catch (globalErr) {
18 - console.error(`Cannot find module: ${moduleName}`);
19 - throw globalErr;
14 + const modulePath = path.join(currentDir, 'node_modules', moduleName);
15 + return require(modulePath);
16 + } catch (err) {
17 + currentDir = path.dirname(currentDir);
18 }
19 + } while (currentDir !== root);
20 +
21 + try {
22 + return require(moduleName);
23 + } catch (globalErr) {
24 + console.error(`Cannot find module: ${moduleName}`);
25 + throw globalErr;
26 }
27 }
28
python/api/chat_export.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import persist_chat
5 +
6 +class ExportChat(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + ctxid = input.get("ctxid", "")
9 + if not ctxid:
10 + raise Exception("No context id provided")
11 +
12 + context = self.get_context(ctxid)
13 + content = persist_chat.export_json_chat(context)
14 +
15 + return {
16 + "message": "Chats exported.",
17 + "ctxid": context.id,
18 + "content": content,
19 + }
\ No newline at end of file
python/api/chat_load.py new
+17
@@ -0,0 +1,17 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import persist_chat
5 +
6 +class LoadChats(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + chats = input.get("chats", [])
9 + if not chats:
10 + raise Exception("No chats provided")
11 +
12 + ctxids = persist_chat.load_json_chats(chats)
13 +
14 + return {
15 + "message": "Chats loaded.",
16 + "ctxids": ctxids,
17 + }
python/api/chat_remove.py new
+18
@@ -0,0 +1,18 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
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:
10 + ctxid = input.get("context", "")
11 +
12 + # context instance - get or create
13 + AgentContext.remove(ctxid)
14 + persist_chat.remove_chat(ctxid)
15 +
16 + return {
17 + "message": "Context removed.",
18 + }
python/api/chat_reset.py new
+17
@@ -0,0 +1,17 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import persist_chat
5 +
6 +class Reset(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + ctxid = input.get("context", "")
9 +
10 + # context instance - get or create
11 + context = self.get_context(ctxid)
12 + context.reset()
13 + persist_chat.save_tmp_chat(context)
14 +
15 + return {
16 + "message": "Agent restarted.",
17 + }
python/api/delete_work_dir_file.py new
+23
@@ -0,0 +1,23 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +
7 +
8 +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 + work_dir = files.get_abs_path("work_dir")
14 + browser = FileBrowser(work_dir)
15 +
16 + if browser.delete_file(file_path):
17 + # Get updated file list
18 + result = browser.get_files(current_path)
19 + return {
20 + "data": result
21 + }
22 + else:
23 + raise Exception("File not found or could not be deleted")
\ No newline at end of file
python/api/download_work_dir_file.py new
+27
@@ -0,0 +1,27 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response, send_file
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +import os
7 +
8 +
9 +
10 +
11 +class DownloadWorkDirFile(ApiHandler):
12 + async def process(self, input: dict, request: Request) -> dict | Response:
13 + file_path = request.args.get('path', '')
14 + if not file_path:
15 + raise ValueError("No file path provided")
16 +
17 + work_dir = files.get_abs_path("work_dir")
18 + browser = FileBrowser(work_dir)
19 +
20 + full_path = browser.get_file_path(file_path)
21 + if full_path:
22 + return send_file(
23 + full_path,
24 + as_attachment=True,
25 + download_name=os.path.basename(file_path)
26 + )
27 + raise Exception("File not found")
\ No newline at end of file
python/api/get_work_dir_files.py new
+15
@@ -0,0 +1,15 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +
7 +
8 +class GetWorkDirFiles(ApiHandler):
9 + async def process(self, input: dict, request: Request) -> dict | Response:
10 + current_path = request.args.get("path", "")
11 + work_dir = files.get_abs_path("work_dir")
12 + browser = FileBrowser(work_dir)
13 + result = browser.get_files(current_path)
14 +
15 + return {"data": result}
python/api/health.py new
+10
@@ -0,0 +1,10 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import git
5 +
6 +class HealthCheck(ApiHandler):
7 +
8 + async def process(self, input: dict, request: Request) -> dict | Response:
9 + gitinfo = git.get_git_info()
10 + return {"gitinfo": gitinfo}
python/api/import_knowledge.py new
+26
@@ -0,0 +1,26 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +import os
7 +from werkzeug.utils import secure_filename
8 +
9 +
10 +class ImportKnowledge(ApiHandler):
11 + async def process(self, input: dict, request: Request) -> dict | Response:
12 + if "files[]" not in request.files:
13 + raise Exception("No files part")
14 +
15 + file_list = request.files.getlist("files[]")
16 + KNOWLEDGE_FOLDER = files.get_abs_path("knowledge/custom/main")
17 +
18 + saved_filenames = []
19 +
20 + for file in file_list:
21 + if file:
22 + filename = secure_filename(file.filename) # type: ignore
23 + file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
24 + saved_filenames.append(filename)
25 +
26 + return {"message": "Knowledge Imported", "filenames": saved_filenames}
python/api/message.py new
+87
@@ -0,0 +1,87 @@
1 +from agent import AgentContext
2 +from python.helpers.api import ApiHandler
3 +from flask import Request, Response
4 +
5 +from python.helpers import files
6 +import os
7 +from werkzeug.utils import secure_filename
8 +from python.helpers.defer import DeferredTask
9 +from python.helpers.print_style import PrintStyle
10 +
11 +
12 +class Message(ApiHandler):
13 + async def process(self, input: dict, request: Request) -> dict | Response:
14 + task, context = await self.communicate(input=input, request=request)
15 + return await self.respond(task, context)
16 +
17 + async def respond(self, task: DeferredTask, context: AgentContext):
18 + result = await task.result() # type: ignore
19 + return {
20 + "message": result,
21 + "context": context.id,
22 + }
23 +
24 + async def communicate(self, input: dict, request: Request):
25 + # Handle both JSON and multipart/form-data
26 + if request.content_type.startswith("multipart/form-data"):
27 + text = request.form.get("text", "")
28 + ctxid = request.form.get("context", "")
29 + message_id = request.form.get("message_id", None)
30 + attachments = request.files.getlist("attachments")
31 + attachment_paths = []
32 +
33 + upload_folder = files.get_abs_path("work_dir/uploads")
34 +
35 + if attachments:
36 + os.makedirs(upload_folder, exist_ok=True)
37 + for attachment in attachments:
38 + if attachment.filename is None:
39 + continue
40 + filename = secure_filename(attachment.filename)
41 + save_path = files.get_abs_path(upload_folder, filename)
42 + attachment.save(save_path)
43 + attachment_paths.append(save_path)
44 + else:
45 + # Handle JSON request as before
46 + input_data = request.get_json()
47 + text = input_data.get("text", "")
48 + ctxid = input_data.get("context", "")
49 + message_id = input_data.get("message_id", None)
50 + attachment_paths = []
51 +
52 + # Now process the message
53 + message = text
54 +
55 + # Obtain agent context
56 + context = self.get_context(ctxid)
57 +
58 + # Store attachments in agent data
59 + context.agent0.set_data("attachments", attachment_paths)
60 +
61 + # Prepare attachment filenames for logging
62 + attachment_filenames = (
63 + [os.path.basename(path) for path in attachment_paths]
64 + if attachment_paths
65 + else []
66 + )
67 +
68 + # Print to console and log
69 + PrintStyle(
70 + background_color="#6C3483", font_color="white", bold=True, padding=True
71 + ).print(f"User message:")
72 + PrintStyle(font_color="white", padding=False).print(f"> {message}")
73 + if attachment_filenames:
74 + PrintStyle(font_color="white", padding=False).print("Attachments:")
75 + for filename in attachment_filenames:
76 + PrintStyle(font_color="white", padding=False).print(f"- {filename}")
77 +
78 + # Log the message with message_id and attachments
79 + context.log.log(
80 + type="user",
81 + heading="User message",
82 + content=message,
83 + kvps={"attachments": attachment_filenames},
84 + id=message_id,
85 + )
86 +
87 + return context.communicate(message), context
\ No newline at end of file
python/api/message_async.py new
+17
@@ -0,0 +1,17 @@
1 +from agent import AgentContext
2 +from python.helpers.api import ApiHandler
3 +from flask import Request, Response
4 +
5 +from python.helpers import files
6 +import os
7 +from werkzeug.utils import secure_filename
8 +from python.helpers.defer import DeferredTask
9 +from python.api.message import Message
10 +
11 +
12 +class MessageAsync(Message):
13 + async def respond(self, task: DeferredTask, context: AgentContext):
14 + return {
15 + "message": "Message received.",
16 + "context": context.id,
17 + }
python/api/pause.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +
5 +class Pause(ApiHandler):
6 + async def process(self, input: dict, request: Request) -> dict | Response:
7 + # input data
8 + paused = input.get("paused", False)
9 + ctxid = input.get("context", "")
10 +
11 + # context instance - get or create
12 + context = self.get_context(ctxid)
13 +
14 + context.paused = paused
15 +
16 + return {
17 + "message": "Agent paused." if paused else "Agent unpaused.",
18 + "pause": paused,
19 + }
python/api/poll.py new
+39
@@ -0,0 +1,39 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from agent import AgentContext
5 +
6 +class Poll(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + ctxid = input.get("context", None)
9 + from_no = input.get("log_from", 0)
10 +
11 + # context instance - get or create
12 + context = self.get_context(ctxid)
13 +
14 + logs = context.log.output(start=from_no)
15 +
16 + # loop AgentContext._contexts
17 + ctxs = []
18 + for ctx in AgentContext._contexts.values():
19 + ctxs.append(
20 + {
21 + "id": ctx.id,
22 + "no": ctx.no,
23 + "log_guid": ctx.log.guid,
24 + "log_version": len(ctx.log.updates),
25 + "log_length": len(ctx.log.logs),
26 + "paused": ctx.paused,
27 + }
28 + )
29 +
30 + # data from this server
31 + return {
32 + "context": context.id,
33 + "contexts": ctxs,
34 + "logs": logs,
35 + "log_guid": context.log.guid,
36 + "log_version": len(context.log.updates),
37 + "log_progress": context.log.progress,
38 + "paused": context.paused,
39 + }
\ No newline at end of file
python/api/rfc.py new
+9
@@ -0,0 +1,9 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import runtime
5 +
6 +class RFC(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + result = await runtime.handle_rfc(input) # type: ignore
9 + return result
python/api/settings_get.py new
+9
@@ -0,0 +1,9 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import settings
5 +
6 +class GetSettings(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + set = settings.convert_out(settings.get_settings())
9 + return {"settings": set}
python/api/settings_set.py new
+11
@@ -0,0 +1,11 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import settings
5 +
6 +
7 +class SetSettings(ApiHandler):
8 + async def process(self, input: dict, request: Request) -> dict | Response:
9 + set = settings.convert_in(input)
10 + set = settings.set_settings(set)
11 + return {"settings": set}
python/api/transcribe.py new
+10
@@ -0,0 +1,10 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers import runtime, whisper
5 +
6 +class Transcribe(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + audio = input.get("audio")
9 + result = await whisper.transcribe(audio) # type: ignore
10 + return result
python/api/upload.py new
+28
@@ -0,0 +1,28 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +from werkzeug.utils import secure_filename
7 +
8 +
9 +class UploadFile(ApiHandler):
10 + async def process(self, input: dict, request: Request) -> dict | Response:
11 + if "file" not in request.files:
12 + raise Exception("No file part")
13 +
14 + file_list = request.files.getlist("file") # Handle multiple files
15 + saved_filenames = []
16 +
17 + for file in file_list:
18 + if file and self.allowed_file(file.filename): # Check file type
19 + filename = secure_filename(file.filename) # type: ignore
20 + file.save(files.get_abs_path("work_dir/upload", filename))
21 + saved_filenames.append(filename)
22 +
23 + return {"filenames": saved_filenames} # Return saved filenames
24 +
25 +
26 + def allowed_file(self,filename):
27 + ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "txt", "pdf", "csv", "html", "json", "md"}
28 + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
\ No newline at end of file
python/api/upload_work_dir_files.py new
+34
@@ -0,0 +1,34 @@
1 +from python.helpers.api import ApiHandler
2 +from flask import Request, Response, send_file
3 +
4 +from python.helpers.file_browser import FileBrowser
5 +from python.helpers import files
6 +import os
7 +
8 +
9 +
10 +
11 +class UploadWorkDirFiles(ApiHandler):
12 + async def process(self, input: dict, request: Request) -> dict | Response:
13 + if "files[]" not in request.files:
14 + raise Exception("No files uploaded")
15 +
16 + current_path = request.form.get('path', '')
17 + uploaded_files = request.files.getlist("files[]")
18 +
19 + work_dir = files.get_abs_path("work_dir")
20 + browser = FileBrowser(work_dir)
21 +
22 + successful, failed = browser.save_files(uploaded_files, current_path)
23 +
24 + if not successful and failed:
25 + raise Exception("All uploads failed")
26 +
27 + result = browser.get_files(current_path)
28 +
29 + return {
30 + "message": "Files uploaded successfully" if not failed else "Some files failed to upload",
31 + "data": result,
32 + "successful": successful,
33 + "failed": failed
34 + }
\ No newline at end of file
python/helpers/api.py new
+57
@@ -0,0 +1,57 @@
1 +from abc import abstractmethod
2 +import json
3 +import threading
4 +from flask import Request, Response, jsonify, Flask
5 +from agent import AgentContext
6 +from initialize import initialize
7 +from python.helpers.print_style import PrintStyle
8 +from python.helpers.errors import format_error
9 +
10 +
11 +class ApiHandler:
12 + def __init__(self, app: Flask, thread_lock: threading.Lock):
13 + self.app = app
14 + self.thread_lock = thread_lock
15 +
16 + @abstractmethod
17 + async def process(self, input: dict, request: Request) -> dict | Response:
18 + pass
19 +
20 + async def handle_request(self, request: Request) -> Response:
21 + try:
22 + # input data from request based on type
23 + if request.is_json:
24 + input = request.get_json()
25 + else:
26 + input = {"data": request.get_data(as_text=True)}
27 +
28 + # process via handler
29 + output = await self.process(input, request)
30 +
31 + # return output based on type
32 + if isinstance(output, Response):
33 + return output
34 + else:
35 + response_json = json.dumps(output)
36 + return Response(response=response_json, status=200, mimetype="application/json")
37 +
38 + # return exceptions with 500
39 + except Exception as e:
40 + error = format_error(e)
41 + PrintStyle.error(error)
42 + return Response(response=error, status=500, mimetype="text/plain")
43 +
44 +
45 +
46 + # get context to run agent zero in
47 + def get_context(self, ctxid: str):
48 + with self.thread_lock:
49 + if not ctxid:
50 + first = AgentContext.first()
51 + if first:
52 + return first
53 + return AgentContext(config=initialize())
54 + got = AgentContext.get(ctxid)
55 + if got:
56 + return got
57 + return AgentContext(config=initialize(), id=ctxid)
\ No newline at end of file
python/helpers/extract_tools.py
+5 -2
@@ -51,7 +51,7 @@ def fix_json_string(json_string):
51
52 T = TypeVar('T') # Define a generic type variable
53
54 -def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]) -> list[Type[T]]:
54 +def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T], one_per_file: bool = True) -> list[Type[T]]:
55 classes = []
56 abs_folder = get_abs_path(folder)
57
@@ -70,8 +70,11 @@ def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]
70 class_list = inspect.getmembers(module, inspect.isclass)
71
72 # Filter for classes that are subclasses of the given base_class
73 - for cls in class_list:
73 + # iterate backwards to skip imported superclasses
74 + for cls in reversed(class_list):
75 if cls[1] is not base_class and issubclass(cls[1], base_class):
76 classes.append(cls[1])
77 + if one_per_file:
78 + break
79
80 return classes
\ No newline at end of file
run_ui.py
+29 -625
@@ -16,6 +16,8 @@ from python.helpers import persist_chat, settings, whisper, rfc, runtime, dotenv
16 import base64
17 from werkzeug.utils import secure_filename
18 from python.helpers.cloudflare_tunnel import CloudflareTunnel
19 +from python.helpers.extract_tools import load_classes_from_folder
20 +from python.helpers.api import ApiHandler
21 from python.helpers.file_browser import FileBrowser
22
23
@@ -29,21 +31,7 @@ lock = threading.Lock()
31 basic_auth = BasicAuth(app)
32
33
32 -# get context to run agent zero in
33 -def get_context(ctxid: str):
34 - with lock:
35 - if not ctxid:
36 - first = AgentContext.first()
37 - if first:
38 - return first
39 - return AgentContext(config=initialize())
40 - got = AgentContext.get(ctxid)
41 - if got:
42 - return got
43 - return AgentContext(config=initialize(), id=ctxid)
44 -
45 -
46 -# Now you can use @requires_auth function decorator to require login on certain pages
34 +# require authentication for handlers
35 def requires_auth(f):
36 @wraps(f)
37 async def decorated(*args, **kwargs):
@@ -63,621 +51,16 @@ def requires_auth(f):
51 return decorated
52
53
66 -UPLOAD_FOLDER = os.path.join(os.getcwd(), "work_dir", "uploads")
67 -
68 -
69 -@app.route("/upload", methods=["POST"])
70 -@requires_auth
71 -async def upload_file():
72 - if "file" not in request.files:
73 - return jsonify({"ok": False, "message": "No file part"}), 400
74 -
75 - files = request.files.getlist("file") # Handle multiple files
76 - saved_filenames = []
77 -
78 - for file in files:
79 - if file and allowed_file(file.filename): # Check file type
80 - filename = secure_filename(file.filename) # type: ignore
81 - file.save(os.path.join(UPLOAD_FOLDER, filename))
82 - saved_filenames.append(filename)
83 -
84 - return jsonify({"ok": True, "filenames": saved_filenames}) # Return saved filenames
85 -
86 -
87 -ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "txt", "pdf", "csv", "html", "json", "md"}
88 -
89 -
90 -def allowed_file(filename):
91 - return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
92 -
93 -
94 -@app.route("/import_knowledge", methods=["POST"])
95 -@requires_auth
96 -async def import_knowledge():
97 - if "files[]" not in request.files:
98 - return jsonify({"ok": False, "message": "No files part"}), 400
99 -
100 - files = request.files.getlist("files[]")
101 - KNOWLEDGE_FOLDER = os.path.join(os.getcwd(), "knowledge", "custom", "main")
102 -
103 - saved_filenames = []
104 -
105 - for file in files:
106 - if file:
107 - filename = secure_filename(file.filename) # type: ignore
108 - file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
109 - saved_filenames.append(filename)
110 -
111 - return jsonify(
112 - {"ok": True, "message": "Knowledge Imported", "filenames": saved_filenames}
113 - )
114 -
115 -
116 -@app.route("/getWorkDirFiles", methods=["GET"])
117 -@requires_auth
118 -async def get_work_dir_files():
119 - try:
120 - current_path = request.args.get('path', '')
121 - work_dir = files.get_abs_path("work_dir")
122 - browser = FileBrowser(work_dir)
123 - result = browser.get_files(current_path)
124 -
125 - response = {
126 - "ok": True,
127 - "data": result
128 - }
129 -
130 - except Exception as e:
131 - response = {
132 - "ok": False,
133 - "message": str(e)
134 - }
135 - PrintStyle.error(str(e))
136 -
137 - return jsonify(response)
138 -
139 -
140 -@app.route("/uploadWorkDirFiles", methods=["POST"])
141 -@requires_auth
142 -async def upload_work_dir_files():
143 - try:
144 - if "files[]" not in request.files:
145 - return jsonify({"ok": False, "message": "No files uploaded"}), 400
146 -
147 - current_path = request.form.get('path', '')
148 - uploaded_files = request.files.getlist("files[]")
149 -
150 - work_dir = files.get_abs_path("work_dir")
151 - browser = FileBrowser(work_dir)
152 -
153 - successful, failed = browser.save_files(uploaded_files, current_path)
154 -
155 - if not successful and failed:
156 - return jsonify({
157 - "ok": False,
158 - "message": "All uploads failed",
159 - "failed": failed
160 - }), 400
161 -
162 - result = browser.get_files(current_path)
163 -
164 - response = {
165 - "ok": True,
166 - "message": "Files uploaded successfully" if not failed else "Some files failed to upload",
167 - "data": result,
168 - "successful": successful,
169 - "failed": failed
170 - }
171 -
172 - except Exception as e:
173 - response = {
174 - "ok": False,
175 - "message": str(e)
176 - }
177 - PrintStyle.error(str(e))
178 -
179 - return jsonify(response)
180 -
181 -
182 -@app.route("/downloadWorkDirFile", methods=["GET"])
183 -@requires_auth
184 -async def download_work_dir_file():
185 - try:
186 - file_path = request.args.get('path', '')
187 - if not file_path:
188 - raise ValueError("No file path provided")
189 -
190 - work_dir = files.get_abs_path("work_dir")
191 - browser = FileBrowser(work_dir)
192 -
193 - full_path = browser.get_file_path(file_path)
194 - if full_path:
195 - return send_file(
196 - full_path,
197 - as_attachment=True,
198 - download_name=os.path.basename(file_path)
199 - )
200 -
201 - return jsonify({
202 - "ok": False,
203 - "message": "File not found"
204 - }), 404
205 -
206 - except Exception as e:
207 - return jsonify({
208 - "ok": False,
209 - "message": str(e)
210 - }), 500
211 -
212 -
213 -@app.route("/deleteWorkDirFile", methods=["POST"])
214 -@requires_auth
215 -async def delete_work_dir_file():
216 - try:
217 - data = request.get_json()
218 - file_path = data.get('path', '')
219 - current_path = data.get('currentPath', '')
220 -
221 - work_dir = files.get_abs_path("work_dir")
222 - browser = FileBrowser(work_dir)
223 -
224 - if browser.delete_file(file_path):
225 - # Get updated file list
226 - result = browser.get_files(current_path)
227 - response = {
228 - "ok": True,
229 - "data": result
230 - }
231 - else:
232 - response = {
233 - "ok": False,
234 - "message": "File not found or could not be deleted"
235 - }
236 -
237 - except Exception as e:
238 - response = {
239 - "ok": False,
240 - "message": str(e)
241 - }
242 - PrintStyle.error(str(e))
243 -
244 - return jsonify(response)
245 -
246 -
54 # handle default address, load index
55 @app.route("/", methods=["GET"])
56 @requires_auth
57 async def serve_index():
58 gitinfo = git.get_git_info()
252 - return files.read_file("./webui/index.html", version_no=gitinfo["version"], version_time=gitinfo["commit_time"])
253 -
254 -
255 -# simple health check, just return OK to see the server is running
256 -@app.route("/ok", methods=["GET", "POST"])
257 -async def health_check():
258 - gitinfo = git.get_git_info()
259 - return jsonify({"ok": True, "gitinfo": gitinfo})
260 -
261 -
262 -# send message to agent (async UI)
263 -@app.route("/msg", methods=["POST"])
264 -@requires_auth
265 -async def handle_message_async():
266 - return await handle_message(False)
267 -
268 -
269 -# send message to agent (synchronous API)
270 -@app.route("/msg_sync", methods=["POST"])
271 -@requires_auth
272 -async def handle_msg_sync():
273 - return await handle_message(True)
274 -
275 -
276 -async def handle_message(sync: bool):
277 - try:
278 - # Handle both JSON and multipart/form-data
279 - if request.content_type.startswith("multipart/form-data"):
280 - text = request.form.get("text", "")
281 - ctxid = request.form.get("context", "")
282 - message_id = request.form.get("message_id", None)
283 - attachments = request.files.getlist("attachments")
284 - attachment_paths = []
285 -
286 - upload_folder = files.get_abs_path("work_dir/uploads")
287 -
288 - if attachments:
289 - os.makedirs(upload_folder, exist_ok=True)
290 - for attachment in attachments:
291 - if attachment.filename is None:
292 - continue
293 - filename = secure_filename(attachment.filename)
294 - save_path = files.get_abs_path(upload_folder, filename)
295 - attachment.save(save_path)
296 - attachment_paths.append(save_path)
297 - else:
298 - # Handle JSON request as before
299 - input_data = request.get_json()
300 - text = input_data.get("text", "")
301 - ctxid = input_data.get("context", "")
302 - message_id = input_data.get("message_id", None)
303 - attachment_paths = []
304 -
305 - # Now process the message
306 - message = text
307 -
308 - # Obtain agent context
309 - context = get_context(ctxid)
310 -
311 - # Store attachments in agent data
312 - context.agent0.set_data("attachments", attachment_paths)
313 -
314 - # Prepare attachment filenames for logging
315 - attachment_filenames = (
316 - [os.path.basename(path) for path in attachment_paths]
317 - if attachment_paths
318 - else []
319 - )
320 -
321 - # Print to console and log
322 - PrintStyle(
323 - background_color="#6C3483", font_color="white", bold=True, padding=True
324 - ).print(f"User message:")
325 - PrintStyle(font_color="white", padding=False).print(f"> {message}")
326 - if attachment_filenames:
327 - PrintStyle(font_color="white", padding=False).print("Attachments:")
328 - for filename in attachment_filenames:
329 - PrintStyle(font_color="white", padding=False).print(f"- {filename}")
330 -
331 - # Log the message with message_id and attachments
332 - context.log.log(
333 - type="user",
334 - heading="User message",
335 - content=message,
336 - kvps={"attachments": attachment_filenames},
337 - id=message_id,
338 - )
339 -
340 - if sync:
341 - context.communicate(message)
342 - result = await context.process.result() # type: ignore
343 - response = {
344 - "ok": True,
345 - "message": result,
346 - "context": context.id,
347 - }
348 - else:
349 - context.communicate(message)
350 - response = {
351 - "ok": True,
352 - "message": "Message received.",
353 - "context": context.id,
354 - }
355 -
356 - except Exception as e:
357 - response = {
358 - "ok": False,
359 - "message": str(e),
360 - }
361 - PrintStyle.error(str(e))
362 -
363 - # respond with json
364 - return jsonify(response)
365 -
366 -
367 -# pausing/unpausing the agent
368 -@app.route("/pause", methods=["POST"])
369 -@requires_auth
370 -async def pause():
371 - try:
372 -
373 - # data sent to the server
374 - input = request.get_json()
375 - paused = input.get("paused", False)
376 - ctxid = input.get("context", "")
377 -
378 - # context instance - get or create
379 - context = get_context(ctxid)
380 -
381 - context.paused = paused
382 -
383 - response = {
384 - "ok": True,
385 - "message": "Agent paused." if paused else "Agent unpaused.",
386 - "pause": paused,
387 - }
388 -
389 - except Exception as e:
390 - response = {
391 - "ok": False,
392 - "message": str(e),
393 - }
394 - PrintStyle.error(str(e))
395 -
396 - # respond with json
397 - return jsonify(response)
398 -
399 -
400 -# load chats from json
401 -@app.route("/loadChats", methods=["POST"])
402 -@requires_auth
403 -async def load_chats():
404 - try:
405 - # data sent to the server
406 - input = request.get_json()
407 - chats = input.get("chats", [])
408 - if not chats:
409 - raise Exception("No chats provided")
410 -
411 - ctxids = persist_chat.load_json_chats(chats)
412 -
413 - response = {
414 - "ok": True,
415 - "message": "Chats loaded.",
416 - "ctxids": ctxids,
417 - }
418 -
419 - except Exception as e:
420 - response = {
421 - "ok": False,
422 - "message": str(e),
423 - }
424 - PrintStyle.error(str(e))
425 -
426 - # respond with json
427 - return jsonify(response)
428 -
429 -
430 -# save chats to json
431 -@app.route("/exportChat", methods=["POST"])
432 -@requires_auth
433 -async def export_chat():
434 - try:
435 - # data sent to the server
436 - input = request.get_json()
437 - ctxid = input.get("ctxid", "")
438 - if not ctxid:
439 - raise Exception("No context id provided")
440 -
441 - context = get_context(ctxid)
442 - content = persist_chat.export_json_chat(context)
443 -
444 - response = {
445 - "ok": True,
446 - "message": "Chats exported.",
447 - "ctxid": context.id,
448 - "content": content,
449 - }
450 -
451 - except Exception as e:
452 - response = {
453 - "ok": False,
454 - "message": str(e),
455 - }
456 - PrintStyle.error(str(e))
457 -
458 - # respond with json
459 - return jsonify(response)
460 -
461 -
462 -# restarting with new agent0
463 -@app.route("/reset", methods=["POST"])
464 -@requires_auth
465 -async def reset():
466 - try:
467 -
468 - # data sent to the server
469 - input = request.get_json()
470 - ctxid = input.get("context", "")
471 -
472 - # context instance - get or create
473 - context = get_context(ctxid)
474 - context.reset()
475 - persist_chat.save_tmp_chat(context)
476 -
477 - response = {
478 - "ok": True,
479 - "message": "Agent restarted.",
480 - }
481 -
482 - except Exception as e:
483 - response = {
484 - "ok": False,
485 - "message": str(e),
486 - }
487 - PrintStyle.error(str(e))
488 -
489 - # respond with json
490 - return jsonify(response)
491 -
492 -
493 -# killing context
494 -@app.route("/remove", methods=["POST"])
495 -@requires_auth
496 -async def remove():
497 - try:
498 -
499 - # data sent to the server
500 - input = request.get_json()
501 - ctxid = input.get("context", "")
502 -
503 - # context instance - get or create
504 - AgentContext.remove(ctxid)
505 - persist_chat.remove_chat(ctxid)
506 -
507 - response = {
508 - "ok": True,
509 - "message": "Context removed.",
510 - }
511 -
512 - except Exception as e:
513 - response = {
514 - "ok": False,
515 - "message": str(e),
516 - }
517 - PrintStyle.error(str(e))
518 -
519 - # respond with json
520 - return jsonify(response)
521 -
522 -
523 -# Web UI polling
524 -@app.route("/poll", methods=["POST"])
525 -@requires_auth
526 -async def poll():
527 - try:
528 -
529 - # data sent to the server
530 - input = request.get_json()
531 - ctxid = input.get("context", None)
532 - from_no = input.get("log_from", 0)
533 -
534 - # context instance - get or create
535 - context = get_context(ctxid)
536 -
537 - logs = context.log.output(start=from_no)
538 -
539 - # loop AgentContext._contexts
540 - ctxs = []
541 - for ctx in AgentContext._contexts.values():
542 - ctxs.append(
543 - {
544 - "id": ctx.id,
545 - "no": ctx.no,
546 - "log_guid": ctx.log.guid,
547 - "log_version": len(ctx.log.updates),
548 - "log_length": len(ctx.log.logs),
549 - "paused": ctx.paused,
550 - }
551 - )
552 -
553 - # data from this server
554 - response = {
555 - "ok": True,
556 - "context": context.id,
557 - "contexts": ctxs,
558 - "logs": logs,
559 - "log_guid": context.log.guid,
560 - "log_version": len(context.log.updates),
561 - "log_progress": context.log.progress,
562 - "paused": context.paused,
563 - }
564 -
565 - except Exception as e:
566 - response = {
567 - "ok": False,
568 - "message": str(e),
569 - }
570 - PrintStyle.error(str(e))
571 -
572 - # serialize json with json.dumps to preserve OrderedDict order
573 - response_json = json.dumps(response)
574 - return Response(response=response_json, status=200, mimetype="application/json")
575 - # return jsonify(response)
576 -
577 -
578 -# get current settings
579 -@app.route("/getSettings", methods=["POST"])
580 -@requires_auth
581 -async def get_settings():
582 - try:
583 -
584 - # data sent to the server
585 - input = request.get_json()
586 -
587 - set = settings.convert_out(settings.get_settings())
588 -
589 - response = {"ok": True, "settings": set}
590 -
591 - except Exception as e:
592 - response = {
593 - "ok": False,
594 - "message": str(e),
595 - }
596 - PrintStyle.error(str(e))
597 -
598 - # respond with json
599 - return jsonify(response)
600 -
601 -
602 -# set current settings
603 -@app.route("/setSettings", methods=["POST"])
604 -@requires_auth
605 -async def set_settings():
606 - try:
607 -
608 - # data sent to the server
609 - input = request.get_json()
610 -
611 - set = settings.convert_in(input)
612 - set = settings.set_settings(set)
613 -
614 - response = {"ok": True, "settings": set}
615 -
616 - except Exception as e:
617 - response = {
618 - "ok": False,
619 - "message": str(e),
620 - }
621 - PrintStyle.error(str(e))
622 -
623 - # respond with json
624 - return jsonify(response)
625 -
626 -
627 -# transcribe audio
628 -@app.route("/transcribe", methods=["POST"])
629 -@requires_auth
630 -async def transcribe():
631 - try:
632 -
633 - # data sent to the server
634 - input = request.get_json()
635 - audio = input.get("audio")
636 -
637 - # transcribe audio
638 - result = await whisper.transcribe(audio)
639 -
640 - response = {
641 - "ok": True,
642 - "text": result["text"],
643 - }
644 -
645 - except Exception as e:
646 - response = {
647 - "ok": False,
648 - "message": str(e),
649 - }
650 - PrintStyle.error(str(e))
651 -
652 - # respond with json
653 - return jsonify(response)
654 -
655 -
656 -# remote function call
657 -@app.route("/rfc", methods=["POST"])
658 -@requires_auth
659 -async def handle_rfc():
660 - try:
661 - # data sent to the server
662 - input = json.loads(request.get_json())
663 -
664 - # handle RFC
665 - result = await runtime.handle_rfc(input)
666 -
667 - response = {
668 - "ok": True,
669 - "result": result,
670 - }
671 -
672 - return jsonify(response)
673 - except Exception as e:
674 - response = {
675 - "ok": False,
676 - "message": str(e),
677 - }
678 - PrintStyle.error(str(e))
679 - return jsonify(response), 500
680 -
59 + return files.read_file(
60 + "./webui/index.html",
61 + version_no=gitinfo["version"],
62 + version_time=gitinfo["commit_time"],
63 + )
64
65 def run():
66 print("Initializing framework...")
@@ -710,6 +93,27 @@ def run():
93 # initialize contexts from persisted chats
94 persist_chat.load_tmp_chats()
95
96 + def register_api_handler(app, handler):
97 + name = handler.__module__.split(".")[-1]
98 + instance = handler(app, lock)
99 + @requires_auth
100 + async def handle_request():
101 + return await instance.handle_request(request=request)
102 + app.add_url_rule(
103 + f"/{name}",
104 + f"/{name}",
105 + handle_request,
106 + methods=["POST", "GET"],
107 + )
108 +
109 + # initialize and register API handlers
110 + handlers = load_classes_from_folder("python/api", "*.py", ApiHandler)
111 + for handler in handlers:
112 + register_api_handler(app, handler)
113 +
114 +
115 +
116 +
117 try:
118 # Run Flask app
119 app.run(
webui/file_browser.js
+71 -66
@@ -17,16 +17,16 @@ const fileBrowserModalProxy = {
17 async openModal() {
18 const modalEl = document.getElementById('fileBrowserModal');
19 const modalAD = Alpine.$data(modalEl);
20 -
20 +
21 modalAD.isOpen = true;
22 modalAD.isLoading = true;
23 modalAD.history = []; // reset history when opening modal
24 -
24 +
25 // Initialize currentPath to root if it's empty
26 if (!modalAD.browser.currentPath) {
27 modalAD.browser.currentPath = "";
28 }
29 -
29 +
30 await modalAD.fetchFiles(modalAD.browser.currentPath);
31 },
32
@@ -39,15 +39,15 @@ const fileBrowserModalProxy = {
39 async fetchFiles(path = "") {
40 this.isLoading = true;
41 try {
42 - const response = await fetch(`/getWorkDirFiles?path=${encodeURIComponent(path)}`);
43 - const data = await response.json();
44 -
45 - if (data.ok) {
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:', data.message);
50 + console.error('Error fetching files:', await response.text());
51 this.browser.entries = [];
52 }
53 } catch (error) {
@@ -108,7 +108,7 @@ const fileBrowserModalProxy = {
108 if (!confirm(`Are you sure you want to delete ${file.name}?`)) {
109 return;
110 }
111 -
111 +
112 try {
113 const response = await fetch('/deleteWorkDirFile', {
114 method: 'POST',
@@ -121,45 +121,46 @@ const fileBrowserModalProxy = {
121 })
122 });
123
124 - const data = await response.json();
125 - if (data.ok) {
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: ${data.message}`);
129 + alert(`Error deleting file: ${await response.text()}`);
130 }
131 } catch (error) {
132 console.error('Error deleting file:', error);
133 alert('Error deleting file');
134 }
135 },
136 -
137 - handleFileUpload(event) {
138 - const files = event.target.files;
139 - if (!files.length) return;
140 -
141 - const formData = new FormData();
142 - formData.append('path', this.browser.currentPath);
143 -
144 - for (let i = 0; i < files.length; i++) {
145 - const ext = files[i].name.split('.').pop().toLowerCase();
146 - if (!['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) {
147 - if (files[i].size > 100 * 1024 * 1024) { // 100MB
148 - alert(`File ${files[i].name} exceeds the maximum allowed size of 100MB.`);
149 - continue;
136 +
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 }
152 - formData.append('files[]', files[i]);
153 - }
154 -
155 - // Proceed with upload after validation
156 - fetch('/uploadWorkDirFiles', {
157 - method: 'POST',
158 - body: formData
159 - })
160 - .then(response => response.json())
161 - .then(data => {
162 - if (data.ok) {
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,48 +168,52 @@ const fileBrowserModalProxy = {
168 }));
169 this.browser.currentPath = data.data.current_path;
170 this.browser.parentPath = data.data.parent_path;
170 -
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 }
179 - })
180 - .catch(error => {
181 +
182 + } catch (error) {
183 console.error('Error uploading files:', error);
184 alert('Error uploading files');
183 - });
185 + }
186 },
187
186 - downloadFile(file) {
188 + async downloadFile(file) {
189 if (file.is_dir) return;
188 -
189 - const downloadUrl = `/downloadWorkDirFile?path=${encodeURIComponent(file.path)}`;
190 -
191 - fetch(downloadUrl)
192 -
193 - .then(response => {
194 - if (!response.ok) {
195 - throw new Error('Network response was not ok');
196 - }
197 - return response.blob();
198 - })
199 - .then(blob => {
200 - const link = document.createElement('a');
201 - link.href = window.URL.createObjectURL(blob);
202 - link.download = file.name;
203 - document.body.appendChild(link);
204 - link.click();
205 - document.body.removeChild(link);
206 - window.URL.revokeObjectURL(link.href);
207 - })
208 - .catch(error => {
209 - console.error('Error downloading file:', error);
210 - alert('Error downloading file');
211 - });
190 +
191 + try {
192 +
193 + const downloadUrl = `/download_work_dir_file?path=${encodeURIComponent(file.path)}`;
194 +
195 + const response = await fetch(downloadUrl)
196 +
197 +
198 + if (!response.ok) {
199 + throw new Error('Network response was not ok');
200 + }
201 +
202 + const blob = await response.blob();
203 +
204 + const link = document.createElement('a');
205 + link.href = window.URL.createObjectURL(blob);
206 + link.download = file.name;
207 + document.body.appendChild(link);
208 + link.click();
209 + document.body.removeChild(link);
210 + window.URL.revokeObjectURL(link.href);
211 +
212 + } catch (error) {
213 +
214 + console.error('Error downloading file:', error);
215 + alert('Error downloading file');
216 + }
217 },
218
219 // Helper Functions
webui/index.css
+1
@@ -356,6 +356,7 @@ h4 {
356 color: var(--color-text);
357 opacity: 0.7;
358 font-size: 0.7rem;
359 + user-select: all;
360 }
361
362 pre {
webui/index.js
+64 -61
@@ -114,7 +114,7 @@ export async function sendMessage() {
114 formData.append('attachments', attachments[i].file);
115 }
116
117 - response = await fetch('/msg', {
117 + response = await fetch('/message_async', {
118 method: 'POST',
119 body: formData
120 });
@@ -125,7 +125,7 @@ export async function sendMessage() {
125 context,
126 message_id: messageId
127 };
128 - response = await fetch('/msg', {
128 + response = await fetch('/message_async', {
129 method: 'POST',
130 headers: {
131 'Content-Type': 'application/json'
@@ -138,13 +138,15 @@ export async function sendMessage() {
138 const jsonResponse = await response.json();
139 if (!jsonResponse) {
140 toast("No response returned.", "error");
141 - } else if (!jsonResponse.ok) {
142 - if (jsonResponse.message) {
143 - toast(jsonResponse.message, "error");
144 - } else {
145 - toast("Undefined error.", "error");
146 - }
147 - } else {
141 + }
142 + // else if (!jsonResponse.ok) {
143 + // if (jsonResponse.message) {
144 + // toast(jsonResponse.message, "error");
145 + // } else {
146 + // toast("Undefined error.", "error");
147 + // }
148 + // }
149 + else {
150 setContext(jsonResponse.context);
151 }
152
@@ -257,10 +259,10 @@ window.loadKnowledge = async function () {
259 body: formData,
260 });
261
260 - const data = await response.json();
261 - if (!data.ok) {
262 - toast(data.message, "error");
262 + if (!response.ok) {
263 + toast(await response.text(), "error");
264 } else {
265 + const data = await response.json();
266 toast("Knowledge files imported: " + data.filenames.join(", "), "success");
267 }
268 };
@@ -274,7 +276,7 @@ function adjustTextareaHeight() {
276 chatInput.style.height = (chatInput.scrollHeight) + 'px';
277 }
278
277 -window.sendJsonData = async function (url, data) {
279 +export const sendJsonData = async function (url, data) {
280 const response = await fetch(url, {
281 method: 'POST',
282 headers: {
@@ -284,11 +286,13 @@ window.sendJsonData = async function (url, data) {
286 });
287
288 if (!response.ok) {
287 - throw new Error('Network response was not ok');
289 + const error = await response.text();
290 + throw new Error(error);
291 }
292 const jsonResponse = await response.json();
293 return jsonResponse;
294 }
295 +window.sendJsonData = sendJsonData
296
297 function generateGUID() {
298 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
@@ -308,40 +312,35 @@ async function poll() {
312 const response = await sendJsonData("/poll", { log_from: lastLogVersion, context });
313 //console.log(response)
314
311 - if (response.ok) {
312 -
313 - if (!context) setContext(response.context)
314 - if (response.context != context) return //skip late polls after context change
315 + if (!context) setContext(response.context)
316 + if (response.context != context) return //skip late polls after context change
317
316 - if (lastLogGuid != response.log_guid) {
317 - chatHistory.innerHTML = ""
318 - lastLogVersion = 0
319 - }
318 + if (lastLogGuid != response.log_guid) {
319 + chatHistory.innerHTML = ""
320 + lastLogVersion = 0
321 + }
322
321 - if (lastLogVersion != response.log_version) {
322 - updated = true
323 - for (const log of response.logs) {
324 - const messageId = log.id || log.no; // Use log.id if available
325 - setMessage(messageId, log.type, log.heading, log.content, log.temp, log.kvps);
326 - }
327 - afterMessagesUpdate(response.logs)
323 + if (lastLogVersion != response.log_version) {
324 + updated = true
325 + for (const log of response.logs) {
326 + const messageId = log.id || log.no; // Use log.id if available
327 + setMessage(messageId, log.type, log.heading, log.content, log.temp, log.kvps);
328 }
329 + afterMessagesUpdate(response.logs)
330 + }
331
330 - updateProgress(response.log_progress)
331 -
332 - //set ui model vars from backend
333 - const inputAD = Alpine.$data(inputSection);
334 - inputAD.paused = response.paused;
335 - const statusAD = Alpine.$data(statusSection);
336 - statusAD.connected = response.ok;
337 - const chatsAD = Alpine.$data(chatsSection);
338 - chatsAD.contexts = response.contexts;
339 -
340 - lastLogVersion = response.log_version;
341 - lastLogGuid = response.log_guid;
332 + updateProgress(response.log_progress)
333
334 + //set ui model vars from backend
335 + const inputAD = Alpine.$data(inputSection);
336 + inputAD.paused = response.paused;
337 + const statusAD = Alpine.$data(statusSection);
338 + statusAD.connected = true;
339 + const chatsAD = Alpine.$data(chatsSection);
340 + chatsAD.contexts = response.contexts;
341
344 - }
342 + lastLogVersion = response.log_version;
343 + lastLogGuid = response.log_guid;
344
345 } catch (error) {
346 console.error('Error:', error);
@@ -375,7 +374,7 @@ function speakMessages(logs) {
374 function updateProgress(progress) {
375 const defaultText = "Waiting for input"
376 if (!progress) progress = defaultText
378 -
377 +
378 if (progress == defaultText) {
379 removeClassFromElement(progressBar, "shiny-text")
380 } else {
@@ -406,7 +405,7 @@ window.pauseAgent = async function (paused) {
405 }
406
407 window.resetChat = async function () {
409 - const resp = await sendJsonData("/reset", { context });
408 + const resp = await sendJsonData("/chat_reset", { context });
409 updateAfterScroll()
410 }
411
@@ -434,7 +433,7 @@ window.killChat = async function (id) {
433 else setContext(generateGUID())
434 }
435
437 - if (found) sendJsonData("/remove", { context: id });
436 + if (found) sendJsonData("/chat_remove", { context: id });
437
438 updateAfterScroll()
439 }
@@ -534,17 +533,19 @@ function toggleCssProperty(selector, property, value) {
533 window.loadChats = async function () {
534 try {
535 const fileContents = await readJsonFiles();
537 - const response = await sendJsonData("/loadChats", { chats: fileContents });
536 + const response = await sendJsonData("/chat_load", { chats: fileContents });
537
538 if (!response) {
539 toast("No response returned.", "error")
541 - } else if (!response.ok) {
542 - if (response.message) {
543 - toast(response.message, "error")
544 - } else {
545 - toast("Undefined error.", "error")
546 - }
547 - } else {
540 + }
541 + // else if (!response.ok) {
542 + // if (response.message) {
543 + // toast(response.message, "error")
544 + // } else {
545 + // toast("Undefined error.", "error")
546 + // }
547 + // }
548 + else {
549 setContext(response.ctxids[0])
550 toast("Chats loaded.", "success")
551 }
@@ -556,17 +557,19 @@ window.loadChats = async function () {
557
558 window.saveChat = async function () {
559 try {
559 - const response = await sendJsonData("/exportChat", { ctxid: context });
560 + const response = await sendJsonData("/chat_export", { ctxid: context });
561
562 if (!response) {
563 toast("No response returned.", "error")
563 - } else if (!response.ok) {
564 - if (response.message) {
565 - toast(response.message, "error")
566 - } else {
567 - toast("Undefined error.", "error")
568 - }
569 - } else {
564 + }
565 + // else if (!response.ok) {
566 + // if (response.message) {
567 + // toast(response.message, "error")
568 + // } else {
569 + // toast("Undefined error.", "error")
570 + // }
571 + // }
572 + else {
573 downloadFile(response.ctxid + ".json", response.content)
574 toast("Chat file downloaded.", "success")
575 }
webui/settings.js
+2 -2
@@ -10,7 +10,7 @@ const settingsModalProxy = {
10 const modalAD = Alpine.$data(modalEl);
11
12 //get settings from backend
13 - const set = await sendJsonData("/getSettings", null);
13 + const set = await sendJsonData("/settings_get", null);
14
15 const settings = {
16 "title": "Settings page",
@@ -43,7 +43,7 @@ const settingsModalProxy = {
43
44 const modalEl = document.getElementById('settingsModal');
45 const modalAD = Alpine.$data(modalEl);
46 - resp = await window.sendJsonData("/setSettings", modalAD.settings);
46 + resp = await window.sendJsonData("/settings_set", modalAD.settings);
47
48 this.resolvePromise({
49 status: 'saved',
webui/speech.js
+1 -1
@@ -1,4 +1,4 @@
1 -import { pipeline, read_audio } from './transformers@3.0.2.js';
1 +// import { pipeline, read_audio } from './transformers@3.0.2.js';
2 import { updateChatInput, sendMessage } from './index.js';
3
4 const microphoneButton = document.getElementById('microphone-button');