1
-import argparse
1
import json
2
from functools import wraps
3
import os
7
from flask import Flask, request, jsonify, Response
8
from flask_basicauth import BasicAuth
9
from agent import AgentContext
11
-from initialize import initialize, set_global_kwargs
10
+from initialize import initialize
11
from python.helpers import files
12
from python.helpers.files import get_abs_path
13
from python.helpers.print_style import PrintStyle
14
from python.helpers.dotenv import load_dotenv
16
-from python.helpers import persist_chat, settings
17
-# from python.helpers.voice_transcription import VoiceTranscription
15
+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
23
app.config["JSON_SORT_KEYS"] = False # Disable key sorting in jsonify
24
25
lock = threading.Lock()
28
-parser = argparse.ArgumentParser()
29
-
30
-# Set up basic authentication, name and password from .env variables
31
-app.config["BASIC_AUTH_USERNAME"] = (
32
- os.environ.get("BASIC_AUTH_USERNAME") or "admin"
33
-) # default name
34
-app.config["BASIC_AUTH_PASSWORD"] = (
35
- os.environ.get("BASIC_AUTH_PASSWORD") or "admin"
36
-) # default pass
26
+
27
+# Set up basic authentication
28
basic_auth = BasicAuth(app)
29
30
46
def requires_auth(f):
47
@wraps(f)
48
async def decorated(*args, **kwargs):
58
- auth = request.authorization
59
- if not auth or not (
60
- auth.username == app.config["BASIC_AUTH_USERNAME"]
61
- and auth.password == app.config["BASIC_AUTH_PASSWORD"]
62
- ):
63
- return Response(
64
- "Could not verify your access level for that URL.\n"
65
- "You have to login with proper credentials",
66
- 401,
67
- {"WWW-Authenticate": 'Basic realm="Login Required"'},
68
- )
49
+ user = dotenv.get_dotenv_value("AUTH_LOGIN")
50
+ password = dotenv.get_dotenv_value("AUTH_PASSWORD")
51
+ if user and password:
52
+ auth = request.authorization
53
+ if not auth or not (
54
+ auth.username == user
55
+ and auth.password == password
56
+ ):
57
+ return Response(
58
+ "Could not verify your access level for that URL.\n"
59
+ "You have to login with proper credentials",
60
+ 401,
61
+ {"WWW-Authenticate": 'Basic realm="Login Required"'},
62
+ )
63
return await f(*args, **kwargs)
64
65
return decorated
66
67
74
-UPLOAD_FOLDER = os.path.join(os.getcwd(), 'work_dir', 'uploads')
68
+UPLOAD_FOLDER = os.path.join(os.getcwd(), "work_dir", "uploads")
69
+
70
76
-@app.route('/upload', methods=['POST'])
71
+@app.route("/upload", methods=["POST"])
72
+@requires_auth
73
async def upload_file():
78
- if 'file' not in request.files:
79
- return jsonify({'ok': False, 'message': 'No file part'}), 400
74
+ if "file" not in request.files:
75
+ return jsonify({"ok": False, "message": "No file part"}), 400
76
81
- files = request.files.getlist('file') # Handle multiple files
77
+ files = request.files.getlist("file") # Handle multiple files
78
saved_filenames = []
79
80
for file in files:
83
file.save(os.path.join(UPLOAD_FOLDER, filename))
84
saved_filenames.append(filename)
85
90
- return jsonify({'ok': True, 'filenames': saved_filenames}) # Return saved filenames
86
+ return jsonify({"ok": True, "filenames": saved_filenames}) # Return saved filenames
87
88
93
-ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'txt', 'pdf', 'csv', 'html', 'json', 'md'}
94
-
89
+ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "txt", "pdf", "csv", "html", "json", "md"}
90
def allowed_file(filename):
96
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
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():
101
- if 'files[]' not in request.files:
102
- return jsonify({'ok': False, 'message': 'No files part'}), 400
97
+ if "files[]" not in request.files:
98
+ return jsonify({"ok": False, "message": "No files part"}), 400
99
104
- files = request.files.getlist('files[]')
105
- KNOWLEDGE_FOLDER = os.path.join(os.getcwd(), 'knowledge', 'custom', 'main')
100
+ files = request.files.getlist("files[]")
101
+ KNOWLEDGE_FOLDER = os.path.join(os.getcwd(), "knowledge", "custom", "main")
102
103
saved_filenames = []
104
108
file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
109
saved_filenames.append(filename)
110
115
- return jsonify({'ok': True, 'message': "Knowledge Imported", 'filenames': saved_filenames})
111
+ return jsonify(
112
+ {"ok": True, "message": "Knowledge Imported", "filenames": saved_filenames}
113
+ )
114
115
116
@app.route("/work_dir", methods=["GET"]) # Correct route
117
+@requires_auth
118
async def browse_work_dir():
120
- work_dir = os.path.join(os.getcwd(), 'work_dir')
119
+ work_dir = os.path.join(os.getcwd(), "work_dir")
120
try:
122
- files = [f for f in os.listdir(work_dir) if os.path.isfile(os.path.join(work_dir, f))]
123
- return jsonify({'ok': True, 'files': files})
121
+ files = [
122
+ f for f in os.listdir(work_dir) if os.path.isfile(os.path.join(work_dir, f))
123
+ ]
124
+ return jsonify({"ok": True, "files": files})
125
except FileNotFoundError:
125
- return jsonify({'ok': False, 'message': 'work_dir not found'}), 404
126
+ return jsonify({"ok": False, "message": "work_dir not found"}), 404
127
except Exception as e:
127
- return jsonify({'ok': False, 'message': f'Error browsing work_dir: {str(e)}'}), 500
128
+ return (
129
+ jsonify({"ok": False, "message": f"Error browsing work_dir: {str(e)}"}),
130
+ 500,
131
+ )
132
+
133
134
# handle default address, show demo html page from ./test_form.html
135
@app.route("/", methods=["GET"])
136
+@requires_auth
137
async def test_form():
138
return Path(get_abs_path("./webui/index.html")).read_text()
139
143
async def health_check():
144
return "OK"
145
140
-
141
-# @app.route('/transcribe', methods=['POST'])
142
-# def transcribe_audio():
143
-# """
144
-# Transcribe audio data using Whisper.
145
-# Expected JSON payload:
146
-# {
147
-# 'audio_data': base64 encoded audio,
148
-# 'model_size': 'base', # Optional, defaults to 'base'
149
-# 'language': None, # Optional language code
150
-# 'is_final': False # Optional flag for final transcription
151
-# }
152
-# """
153
-# try:
154
-# # Parse request data
155
-# data = request.json
156
-# audio_data = data.get('audio_data')
157
-# model_size = data.get('model_size', 'base')
158
-# language = data.get('language')
159
-# is_final = data.get('is_final', False)
160
-
161
-# # Validate input
162
-# if not audio_data:
163
-# return jsonify({
164
-# "error": "No audio data provided",
165
-# "status": "error"
166
-# }), 400
167
-
168
-# # Validate model size
169
-# valid_model_sizes = ['tiny', 'base', 'small', 'medium', 'large']
170
-# if model_size not in valid_model_sizes:
171
-# return jsonify({
172
-# "error": f"Invalid model size. Choose from {valid_model_sizes}",
173
-# "status": "error"
174
-# }), 400
175
-
176
-# # Log the received audio data size
177
-# print(f"Received audio data size: {len(audio_data)} characters (base64)")
178
-
179
-# try:
180
-# # Transcribe using VoiceTranscription helper
181
-# text = VoiceTranscription.transcribe_bytes(
182
-# audio_data,
183
-# model_size=model_size,
184
-# language=language
185
-# )
186
-
187
-# # Return transcription result
188
-# return jsonify({
189
-# "text": text,
190
-# "is_final": is_final,
191
-# "model_size": model_size,
192
-# "status": "success"
193
-# })
194
-
195
-# except Exception as transcribe_error:
196
-# # Detailed error logging for transcription failures
197
-# print(f"Transcription error: {transcribe_error}")
198
-# return jsonify({
199
-# "error": "Transcription failed",
200
-# "details": str(transcribe_error),
201
-# "status": "error"
202
-# }), 500
203
-
204
-# except Exception as e:
205
-# # Catch-all error handler
206
-# print(f"Unexpected transcription error: {e}")
207
-# return jsonify({
208
-# "error": "Unexpected error during transcription",
209
-# "details": str(e),
210
-# "status": "error"
211
-# }), 500
212
-
213
-# # secret page, requires authentication
214
-# @app.route('/secret', methods=['GET'])
215
-# @requires_auth
216
-# async def secret_page():
217
-# return Path("./secret_page.html").read_text()
218
-
219
-
146
# send message to agent (async UI)
147
@app.route("/msg", methods=["POST"])
148
+@requires_auth
149
async def handle_message_async():
223
- return await handle_message(False)
150
+ return await handle_message(False)
151
+
152
153
# send message to agent (synchronous API)
154
@app.route("/msg_sync", methods=["POST"])
155
+@requires_auth
156
async def handle_msg_sync():
157
return await handle_message(True)
158
159
+
160
async def handle_message(sync: bool):
231
- try:
232
- # Handle both JSON and multipart/form-data
233
- if request.content_type.startswith('multipart/form-data'):
234
- text = request.form.get('text', '')
235
- ctxid = request.form.get('context', '')
236
- message_id = request.form.get('message_id', None)
237
- attachments = request.files.getlist('attachments')
238
- attachment_paths = []
239
-
240
- upload_folder = files.get_abs_path('work_dir/uploads')
241
-
242
- if attachments:
243
- os.makedirs(upload_folder, exist_ok=True)
244
- for attachment in attachments:
245
- filename = secure_filename(attachment.filename)
246
- save_path = files.get_abs_path(upload_folder, filename)
247
- attachment.save(save_path)
248
- attachment_paths.append(save_path)
249
- else:
250
- # Handle JSON request as before
251
- input_data = request.get_json()
252
- text = input_data.get('text', '')
253
- ctxid = input_data.get('context', '')
254
- message_id = input_data.get('message_id', None)
255
- attachment_paths = []
256
-
257
- # Now process the message
258
- message = text
259
-
260
- # Obtain agent context
261
- context = get_context(ctxid)
262
-
263
- # Store attachments in agent data
264
- context.agent0.set_data('attachments', attachment_paths)
265
-
266
- # Prepare attachment filenames for logging
267
- attachment_filenames = [os.path.basename(path) for path in attachment_paths] if attachment_paths else []
268
-
269
- # Print to console and log
270
- PrintStyle(
271
- background_color="#6C3483", font_color="white", bold=True, padding=True
272
- ).print(f"User message:")
273
- PrintStyle(font_color="white", padding=False).print(f"> {message}")
274
- if attachment_filenames:
275
- PrintStyle(font_color="white", padding=False).print("Attachments:")
276
- for filename in attachment_filenames:
277
- PrintStyle(font_color="white", padding=False).print(f"- {filename}")
278
-
279
- # Log the message with message_id and attachments
280
- context.log.log(type="user", heading="User message", content=message, kvps={'attachments': attachment_filenames}, id=message_id)
281
-
282
- if sync:
283
- context.communicate(message)
284
- result = await context.process.result() # type: ignore
285
- response = {
286
- "ok": True,
287
- "message": result,
288
- "context": context.id,
289
- }
290
- else:
291
- context.communicate(message)
292
- response = {
293
- "ok": True,
294
- "message": "Message received.",
295
- "context": context.id,
296
- }
297
-
298
- except Exception as e:
299
- response = {
300
- "ok": False,
301
- "message": str(e),
302
- }
303
- PrintStyle.error(str(e))
304
-
305
- # respond with json
306
- return jsonify(response)
161
+ try:
162
+ # Handle both JSON and multipart/form-data
163
+ if request.content_type.startswith("multipart/form-data"):
164
+ text = request.form.get("text", "")
165
+ ctxid = request.form.get("context", "")
166
+ message_id = request.form.get("message_id", None)
167
+ attachments = request.files.getlist("attachments")
168
+ attachment_paths = []
169
+
170
+ upload_folder = files.get_abs_path("work_dir/uploads")
171
+
172
+ if attachments:
173
+ os.makedirs(upload_folder, exist_ok=True)
174
+ for attachment in attachments:
175
+ filename = secure_filename(attachment.filename)
176
+ save_path = files.get_abs_path(upload_folder, filename)
177
+ attachment.save(save_path)
178
+ attachment_paths.append(save_path)
179
+ else:
180
+ # Handle JSON request as before
181
+ input_data = request.get_json()
182
+ text = input_data.get("text", "")
183
+ ctxid = input_data.get("context", "")
184
+ message_id = input_data.get("message_id", None)
185
+ attachment_paths = []
186
+
187
+ # Now process the message
188
+ message = text
189
+
190
+ # Obtain agent context
191
+ context = get_context(ctxid)
192
+
193
+ # Store attachments in agent data
194
+ context.agent0.set_data("attachments", attachment_paths)
195
+
196
+ # Prepare attachment filenames for logging
197
+ attachment_filenames = (
198
+ [os.path.basename(path) for path in attachment_paths]
199
+ if attachment_paths
200
+ else []
201
+ )
202
+
203
+ # Print to console and log
204
+ PrintStyle(
205
+ background_color="#6C3483", font_color="white", bold=True, padding=True
206
+ ).print(f"User message:")
207
+ PrintStyle(font_color="white", padding=False).print(f"> {message}")
208
+ if attachment_filenames:
209
+ PrintStyle(font_color="white", padding=False).print("Attachments:")
210
+ for filename in attachment_filenames:
211
+ PrintStyle(font_color="white", padding=False).print(f"- {filename}")
212
+
213
+ # Log the message with message_id and attachments
214
+ context.log.log(
215
+ type="user",
216
+ heading="User message",
217
+ content=message,
218
+ kvps={"attachments": attachment_filenames},
219
+ id=message_id,
220
+ )
221
+
222
+ if sync:
223
+ context.communicate(message)
224
+ result = await context.process.result() # type: ignore
225
+ response = {
226
+ "ok": True,
227
+ "message": result,
228
+ "context": context.id,
229
+ }
230
+ else:
231
+ context.communicate(message)
232
+ response = {
233
+ "ok": True,
234
+ "message": "Message received.",
235
+ "context": context.id,
236
+ }
237
+
238
+ except Exception as e:
239
+ response = {
240
+ "ok": False,
241
+ "message": str(e),
242
+ }
243
+ PrintStyle.error(str(e))
244
+
245
+ # respond with json
246
+ return jsonify(response)
247
248
249
# pausing/unpausing the agent
250
@app.route("/pause", methods=["POST"])
251
+@requires_auth
252
async def pause():
253
try:
254
281
282
# load chats from json
283
@app.route("/loadChats", methods=["POST"])
284
+@requires_auth
285
async def load_chats():
286
try:
287
# data sent to the server
311
312
# save chats to json
313
@app.route("/exportChat", methods=["POST"])
314
+@requires_auth
315
async def export_chat():
316
try:
317
# data sent to the server
343
344
# restarting with new agent0
345
@app.route("/reset", methods=["POST"])
346
+@requires_auth
347
async def reset():
348
try:
349
374
375
# killing context
376
@app.route("/remove", methods=["POST"])
377
+@requires_auth
378
async def remove():
379
try:
380
404
405
# Web UI polling
406
@app.route("/poll", methods=["POST"])
407
+@requires_auth
408
async def poll():
409
try:
410
459
460
# get current settings
461
@app.route("/getSettings", methods=["POST"])
462
+@requires_auth
463
async def get_settings():
464
try:
465
480
# respond with json
481
return jsonify(response)
482
483
+
484
# set current settings
485
@app.route("/setSettings", methods=["POST"])
486
+@requires_auth
487
async def set_settings():
488
try:
489
505
# respond with json
506
return jsonify(response)
507
559
-def run():
560
- print("Initializing framework...")
508
562
- # load env vars
563
- load_dotenv()
509
+# transcribe audio
510
+@app.route("/transcribe", methods=["POST"])
511
+@requires_auth
512
+async def transcribe():
513
+ try:
514
+
515
+ # data sent to the server
516
+ input = request.get_json()
517
+ audio = input.get("audio")
518
565
- # initialize contexts from persisted chats
566
- persist_chat.load_tmp_chats()
519
+ # transcribe audio
520
+ result = await whisper.transcribe(audio)
521
+
522
+ response = {
523
+ "ok": True,
524
+ "text": result["text"],
525
+ }
526
+
527
+ except Exception as e:
528
+ response = {
529
+ "ok": False,
530
+ "message": str(e),
531
+ }
532
+ PrintStyle.error(str(e))
533
+
534
+ # respond with json
535
+ return jsonify(response)
536
+
537
+
538
+# remote function call
539
+@app.route("/rfc", methods=["POST"])
540
+@requires_auth
541
+async def handle_rfc():
542
+ # data sent to the server
543
+ input = json.loads(request.get_json())
544
+
545
+ # handle RFC call
546
+ result = await rfc.handle_rfc(input)
547
+ return jsonify(result)
548
+
549
+
550
+def run():
551
+ print("Initializing framework...")
552
553
# Suppress only request logs but keep the startup messages
554
from werkzeug.serving import WSGIRequestHandler
557
def log_request(self, code="-", size="-"):
558
pass # Override to suppress request logging
559
575
- args, add_args = parser.parse_known_args()
576
- #add_args to dict
577
- glob_args = {}
578
- for arg in add_args:
579
- if "=" in arg:
580
- key, value = arg.split("=", 1)
581
- key = key.lstrip("-")
582
- glob_args[key] = value
583
- set_global_kwargs(**glob_args)
584
-
560
# Get configuration from environment
586
- port = args.port or int(os.environ.get("WEB_UI_PORT", 0)) or None
587
- host = args.host or os.environ.get("WEB_UI_HOST") or None
588
- use_cloudflare = os.environ.get("USE_CLOUDFLARE", "false").lower() == "true"
561
+ port = runtime.get_arg("port") or int(os.environ.get("WEB_UI_PORT", 0)) or None
562
+ host = runtime.get_arg("host") or os.environ.get("WEB_UI_HOST") or None
563
+ use_cloudflare = (
564
+ runtime.get_arg("cloudflare_tunnel")
565
+ or os.environ.get("USE_CLOUDFLARE", "false").lower() == "true"
566
+ )
567
568
# Initialize and start Cloudflare tunnel if enabled
569
tunnel = None
575
print(f"Failed to start Cloudflare tunnel: {e}")
576
print("Continuing without tunnel...")
577
578
+ # initialize contexts from persisted chats
579
+ persist_chat.load_tmp_chats()
580
+
581
try:
582
# Run Flask app
583
app.run(
603
- request_handler=NoRequestLoggingWSGIRequestHandler,
604
- port=port,
605
- host=host
584
+ request_handler=NoRequestLoggingWSGIRequestHandler, port=port, host=host
585
)
586
finally:
587
# Clean up tunnel if it was started
588
if tunnel:
589
tunnel.stop()
590
591
+
592
# run the internal server
593
if __name__ == "__main__":
614
-
615
- parser.add_argument("--port", type=int, default=0, help="Web UI port")
616
- parser.add_argument("--host", type=str, default=0, help="Web UI host")
617
-
594
+ runtime.initialize()
595
run()