merge prep
frdel committed
Jun 30, 2025 at 16:16 UTC
2fcc8ec8853cba688fe1a980c5e7201432e0dff3
13 files changed
+296
-74
models.py
+1
@@ -33,6 +33,7 @@ from langchain_core.messages import (
33
from langchain.embeddings.base import Embeddings
34
35
load_dotenv()
36
+os.environ['LITELLM_LOG'] = "ERROR" # only errors
37
38
39
class ModelType(Enum):
prompts/default/browser_agent.system.md
+3
-18
@@ -4,14 +4,9 @@ Follow instructions as closely as possible
4
When told go to website, open the website. If no other instructions: stop there
5
Do not interact with the website unless told to
6
Always accept all cookies if prompted on the website, NEVER go to browser cookie settings
7
-In page_summary respond with one paragraph of main content plus an overview of page elements
7
If asked specific questions about a website, be as precise and close to the actual page content as possible
8
If you are waiting for instructions: you should end the task and mark as done
9
11
-## Response Format
12
-Your responses must always be formatted as a JSON object
13
-The response JSON must contain at least the following fields: "title", "response", "page_summary"
14
-
10
## Task Completion
11
When you have completed the assigned task OR are waiting for further instructions:
12
1. Use the "Complete task" action to mark the task as complete
@@ -20,18 +15,8 @@ When you have completed the assigned task OR are waiting for further instruction
15
16
## Important Notes
17
- Always call "Complete task" when your objective is achieved
18
+- In page_summary respond with one paragraph of main content plus an overview of page elements
19
+- Response field is used to answer to user's task or ask additional questions
20
- If you navigate to a website and no further actions are requested, call "Complete task" immediately
21
- If you complete any requested interaction (clicking, typing, etc.), call "Complete task"
25
-- Never leave a task running indefinitely - always conclude with "Complete task"
26
-
27
-## Response fields
28
- * title (type: str) - The ttitle of the current web page
29
- * response (type: str) - Your response to your superior's last request
30
- * page_summary (type: str) - Summary of the current web page as requested by superior
31
-
32
-## Example response
33
-{
34
- "title": "Google Search",
35
- "response": "I have succesfully navigated to the response page.",
36
- "page_summary": "The page contains a menu bar with ... and a search input field. Under the search field there are two buttons with ... and ..."
37
-}
22
+- Never leave a task running indefinitely - always conclude with "Complete task"
\ No newline at end of file
python/api/image_get.py
+31
-25
@@ -1,34 +1,40 @@
1
import os
2
import re
3
+from typing import override
4
from python.helpers.api import ApiHandler
5
from python.helpers import files
6
from flask import Request, Response, send_file
7
8
9
class ImageGet(ApiHandler):
10
+
11
+ @classmethod
12
+ def get_methods(cls) -> list[str]:
13
+ return ["GET"]
14
+
15
async def process(self, input: dict, request: Request) -> dict | Response:
10
- # input data
11
- path = input.get("path", request.args.get("path", ""))
12
- if not path:
13
- raise ValueError("No path provided")
14
-
15
- # check if path is within base directory
16
- if not files.is_in_base_dir(path):
17
- raise ValueError("Path is outside of allowed directory")
18
-
19
- # check if file has an image extension
20
- # list of allowed image extensions
21
- allowed_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]
22
- # get file extension
23
- file_ext = os.path.splitext(path)[1].lower()
24
- if file_ext not in allowed_extensions:
25
- raise ValueError(f"File type not allowed. Allowed types: {', '.join(allowed_extensions)}")
26
-
27
- # check if file exists
28
- if not os.path.exists(path):
29
- raise ValueError("File not found")
30
-
31
- # send file
32
- return send_file(path)
33
-
34
-
\ No newline at end of file
16
+ # input data
17
+ path = input.get("path", request.args.get("path", ""))
18
+ if not path:
19
+ raise ValueError("No path provided")
20
+
21
+ # check if path is within base directory
22
+ if not files.is_in_base_dir(path):
23
+ raise ValueError("Path is outside of allowed directory")
24
+
25
+ # check if file has an image extension
26
+ # list of allowed image extensions
27
+ allowed_extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]
28
+ # get file extension
29
+ file_ext = os.path.splitext(path)[1].lower()
30
+ if file_ext not in allowed_extensions:
31
+ raise ValueError(
32
+ f"File type not allowed. Allowed types: {', '.join(allowed_extensions)}"
33
+ )
34
+
35
+ # check if file exists
36
+ if not os.path.exists(path):
37
+ raise ValueError("File not found")
38
+
39
+ # send file
40
+ return send_file(path)
python/tools/code_execution_tool.py
+1
-1
@@ -219,7 +219,7 @@ class CodeExecution(Tool):
219
while True:
220
await asyncio.sleep(sleep_time)
221
full_output, partial_output = await self.state.shells[session].read_output(
222
- timeout=3, reset_full_output=reset_full_output
222
+ timeout=1, reset_full_output=reset_full_output
223
)
224
reset_full_output = False # only reset once
225
run_ui.py
+3
-1
@@ -131,7 +131,9 @@ def csrf_protect(f):
131
async def decorated(*args, **kwargs):
132
token = session.get("csrf_token")
133
header = request.headers.get("X-CSRF-Token")
134
- if not token or not header or token != header:
134
+ cookie = request.cookies.get("csrf_token")
135
+ sent = header or cookie
136
+ if not token or not sent or token != sent:
137
return Response("CSRF token missing or invalid", 403)
138
return await f(*args, **kwargs)
139
webui/components/messages/resize/message-resize-store.js
new
+85
@@ -0,0 +1,85 @@
1
+import { createStore } from "/js/AlpineStore.js";
2
+import { toggleCssProperty } from "/js/css.js";
3
+
4
+const model = {
5
+ settings: {},
6
+
7
+ async init() {
8
+ this.settings =
9
+ JSON.parse(localStorage.getItem("messageResizeSettings") || "null") ||
10
+ this._getDefaultSettings();
11
+ this._applyAllSettings();
12
+ },
13
+
14
+ _getDefaultSettings() {
15
+ return {
16
+ "message-agent": { minimized: true, maximized: false },
17
+ "messsage-code-exec": { minimized: false, maximized: false },
18
+ };
19
+ },
20
+
21
+ _getSetting(className) {
22
+ return this.settings[className] || { minimized: false, maximized: false };
23
+ },
24
+
25
+ _getDefaultSetting() {
26
+ return { minimized: false, maximized: false };
27
+ },
28
+
29
+ _setSetting(className, setting) {
30
+ this.settings[className] = setting;
31
+ localStorage.setItem(
32
+ "messageResizeSettings",
33
+ JSON.stringify(this.settings)
34
+ );
35
+ },
36
+
37
+ _applyAllSettings() {
38
+ for (const [className, setting] of Object.entries(this.settings)) {
39
+ this._applySetting(className, setting);
40
+ }
41
+ },
42
+
43
+ async minimizeMessageClass(className, event) {
44
+ const set = this._getSetting(className);
45
+ set.minimized = !set.minimized;
46
+ this._setSetting(className, set);
47
+ this._applySetting(className, set);
48
+ this._applyScroll(event);
49
+ },
50
+
51
+ async maximizeMessageClass(className, event) {
52
+ const set = this._getSetting(className);
53
+ if (set.minimized) return this.minimizeMessageClass(className, event); // if minimized, unminimize first
54
+ set.maximized = !set.maximized;
55
+ this._setSetting(className, set);
56
+ this._applySetting(className, set);
57
+ this._applyScroll(event);
58
+ },
59
+
60
+ _applyScroll(event) {
61
+ event.target.scrollIntoView({ behavior: "smooth" });
62
+ },
63
+
64
+ _applySetting(className, setting) {
65
+ toggleCssProperty(
66
+ `.${className} .message-body`,
67
+ "max-height",
68
+ setting.maximized ? undefined : "30em"
69
+ );
70
+ toggleCssProperty(
71
+ `.${className} .message-body`,
72
+ "overflow-y",
73
+ setting.maximized ? undefined : "auto"
74
+ );
75
+ toggleCssProperty(
76
+ `.${className} .message-body`,
77
+ "display",
78
+ setting.minimized ? "none" : "block"
79
+ );
80
+ },
81
+};
82
+
83
+const store = createStore("messageResize", model);
84
+
85
+export { store };
webui/index.css
+32
@@ -689,6 +689,18 @@ pre {
689
background-color: #4b3a69;
690
}
691
692
+.message-code-exe .message-body {
693
+ min-height: 5em;
694
+ background-color: var(--color-panel);
695
+ border-radius: 0.5em;
696
+ margin: 0.3em;
697
+ padding: 0.3em;
698
+}
699
+
700
+.light-mode .message-code-exe .message-body {
701
+ border: 1px solid var(--color-border);
702
+}
703
+
704
.message-browser {
705
background-color: #4b3a69;
706
}
@@ -725,6 +737,11 @@ pre {
737
width: 100%;
738
}
739
740
+.kvps-val pre {
741
+ white-space: pre-wrap; /* keep \n, collapse no spaces, allow wrapping */
742
+ word-break: break-word; /* optional – forces really long “words” to break */
743
+}
744
+
745
.msg-kvps th,
746
.msg-kvps td {
747
align-content: center;
@@ -744,6 +761,9 @@ pre {
761
762
.msg-heading {
763
margin: 0;
764
+ padding-right: 5.5em; /* space for buttons */
765
+ position: relative;
766
+ display: block;
767
}
768
769
/* Message Actions */
@@ -1735,6 +1755,9 @@ input:checked + .slider:before {
1755
1756
.kvps-val {
1757
margin: 0 0 0.4rem 0;
1758
+ white-space: pre-wrap;
1759
+ word-break: break-word;
1760
+ overflow-wrap: anywhere;
1761
}
1762
}
1763
@@ -3081,3 +3104,12 @@ nav ul li a img {
3104
border: 1px solid rgba(142, 142, 142, 0.1);
3105
border-radius: 0.3em;
3106
}
3107
+
3108
+.msg-min-max-btns {
3109
+ position: absolute;
3110
+ top: -0.2em;
3111
+ right: -0.5em;
3112
+ display: flex;
3113
+ gap: 0.3em;
3114
+ z-index: 1;
3115
+}
webui/index.html
+4
@@ -118,6 +118,10 @@
118
<script defer src="vendor/katex/katex.auto-render.min.js"
119
crossorigin="anonymous"></script>
120
121
+ <!-- Google Icons -->
122
+ <link rel="stylesheet" href="vendor/google/google-icons.css" />
123
+
124
+
125
<!-- Non-module scripts after Alpine.js -->
126
<script type="text/javascript" src="js/settings.js"></script>
127
<script type="text/javascript" src="js/file_browser.js"></script>
webui/js/api.js
+2
-1
@@ -79,5 +79,6 @@ async function getCsrfToken() {
79
credentials: "same-origin",
80
}).then((r) => r.json());
81
csrfToken = response.token;
82
+ document.cookie = "csrf_token=" + csrfToken + "; SameSite=Strict; Path=/";
83
return csrfToken;
83
-}
84
+}
\ No newline at end of file
webui/js/css.js
new
+50
@@ -0,0 +1,50 @@
1
+// Create and keep a reference to a dynamic stylesheet for runtime CSS changes
2
+let dynamicStyleSheet;
3
+{
4
+ const style = document.createElement("style");
5
+ style.appendChild(document.createTextNode(""));
6
+ document.head.appendChild(style);
7
+ dynamicStyleSheet = style.sheet;
8
+}
9
+
10
+export function toggleCssProperty(selector, property, value) {
11
+ // Get the stylesheet that contains the class
12
+ const styleSheets = document.styleSheets;
13
+
14
+ // Iterate through all stylesheets to find the class
15
+ for (let i = 0; i < styleSheets.length; i++) {
16
+ const styleSheet = styleSheets[i];
17
+ let rules;
18
+ try {
19
+ rules = styleSheet.cssRules || styleSheet.rules;
20
+ } catch (e) {
21
+ // Skip stylesheets we cannot access due to CORS/security restrictions
22
+ continue;
23
+ }
24
+ if (!rules) continue;
25
+
26
+ for (let j = 0; j < rules.length; j++) {
27
+ const rule = rules[j];
28
+ if (rule.selectorText == selector) {
29
+ _applyCssToRule(rule, property, value);
30
+ return;
31
+ }
32
+ }
33
+ }
34
+ // If not found, add it to the dynamic stylesheet
35
+ const ruleIndex = dynamicStyleSheet.insertRule(
36
+ `${selector} {}`,
37
+ dynamicStyleSheet.cssRules.length
38
+ );
39
+ const rule = dynamicStyleSheet.cssRules[ruleIndex];
40
+ _applyCssToRule(rule, property, value);
41
+}
42
+
43
+// Helper to apply/remove a CSS property on a rule
44
+function _applyCssToRule(rule, property, value) {
45
+ if (value === undefined) {
46
+ rule.style.removeProperty(property);
47
+ } else {
48
+ rule.style.setProperty(property, value);
49
+ }
50
+ }
\ No newline at end of file
webui/js/messages.js
+64
-28
@@ -1,6 +1,7 @@
1
// copy button
2
import { openImageModal } from "./image_modal.js";
3
import { marked } from "../vendor/marked/marked.esm.js";
4
+import { store as messageResizeStore } from "/components/messages/resize/message-resize-store.js";
5
6
function createCopyButton() {
7
const button = document.createElement("button");
@@ -81,23 +82,39 @@ export function _drawMessage(
82
content,
83
temp,
84
followUp,
85
+ mainClass = "",
86
kvps = null,
87
messageClasses = [],
88
contentClasses = [],
89
latex = false,
88
- markdown = false
90
+ markdown = false,
91
+ resizeBtns = true
92
) {
93
const messageDiv = document.createElement("div");
91
- messageDiv.classList.add("message", ...messageClasses);
94
+ messageDiv.classList.add("message", mainClass, ...messageClasses);
95
96
if (heading) {
97
const headingElement = document.createElement("h4");
98
headingElement.classList.add("msg-heading");
99
headingElement.textContent = heading;
100
messageDiv.appendChild(headingElement);
101
+
102
+ if (resizeBtns) {
103
+ const minMaxBtn = document.createElement("div");
104
+ minMaxBtn.classList.add("msg-min-max-btns");
105
+ minMaxBtn.innerHTML = `
106
+ <a href="#" class="msg-min-max-btn" @click.prevent="$store.messageResize.minimizeMessageClass('${mainClass}', $event)"><span class="material-symbols-outlined">minimize</span></a>
107
+ <a href="#" class="msg-min-max-btn" @click.prevent="$store.messageResize.maximizeMessageClass('${mainClass}', $event)"><span class="material-symbols-outlined">expand_all</span></a>
108
+ `;
109
+ headingElement.appendChild(minMaxBtn);
110
+ }
111
}
112
100
- drawKvps(messageDiv, kvps, false);
113
+ const bodyDiv = document.createElement("div");
114
+ bodyDiv.classList.add("message-body");
115
+ messageDiv.appendChild(bodyDiv);
116
+
117
+ drawKvps(bodyDiv, kvps, false);
118
119
if (content && content.trim().length > 0) {
120
if (markdown) {
@@ -124,7 +141,7 @@ export function _drawMessage(
141
142
contentDiv.appendChild(spanElement);
143
addCopyButtonToElement(contentDiv);
127
- messageDiv.appendChild(contentDiv);
144
+ bodyDiv.appendChild(contentDiv);
145
} else {
146
const preElement = document.createElement("pre");
147
preElement.classList.add("msg-content", ...contentClasses);
@@ -141,7 +158,7 @@ export function _drawMessage(
158
159
preElement.appendChild(spanElement);
160
addCopyButtonToElement(preElement);
144
- messageDiv.appendChild(preElement);
161
+ bodyDiv.appendChild(preElement);
162
}
163
}
164
@@ -151,6 +168,11 @@ export function _drawMessage(
168
messageContainer.classList.add("message-followup");
169
}
170
171
+ // autoscroll the body if needed
172
+ setTimeout(() => {
173
+ bodyDiv.scrollTop = bodyDiv.scrollHeight;
174
+ }, 0);
175
+
176
return messageDiv;
177
}
178
@@ -169,8 +191,9 @@ export function drawMessageDefault(
191
content,
192
temp,
193
false,
194
+ "message-default",
195
kvps,
173
- ["message-ai", "message-default"],
196
+ ["message-ai"],
197
["msg-json"],
198
false,
199
false
@@ -198,8 +221,9 @@ export function drawMessageAgent(
221
content,
222
temp,
223
false,
224
+ "message-agent",
225
kvpsFlat,
202
- ["message-ai", "message-agent"],
226
+ ["message-ai"],
227
["msg-json"],
228
false,
229
false
@@ -221,8 +245,9 @@ export function drawMessageResponse(
245
content,
246
temp,
247
true,
248
+ "message-agent-response",
249
null,
225
- ["message-ai", "message-agent-response"],
250
+ ["message-ai"],
251
[],
252
true,
253
true
@@ -244,8 +269,9 @@ export function drawMessageDelegation(
269
content,
270
temp,
271
true,
272
+ "message-agent-delegation",
273
kvps,
248
- ["message-ai", "message-agent", "message-agent-delegation"],
274
+ ["message-ai", "message-agent"],
275
[],
276
true,
277
false
@@ -364,8 +390,9 @@ export function drawMessageTool(
390
content,
391
temp,
392
true,
393
+ "message-tool",
394
kvps,
368
- ["message-ai", "message-tool"],
395
+ ["message-ai"],
396
["msg-output"],
397
false,
398
false
@@ -387,8 +414,9 @@ export function drawMessageCodeExe(
414
content,
415
temp,
416
true,
417
+ "message-code-exe",
418
null,
391
- ["message-ai", "message-code-exe"],
419
+ ["message-ai"],
420
[],
421
false,
422
false
@@ -410,8 +438,9 @@ export function drawMessageBrowser(
438
content,
439
temp,
440
true,
441
+ "message-browser",
442
kvps,
414
- ["message-ai", "message-browser"],
443
+ ["message-ai"],
444
["msg-json"],
445
false,
446
false
@@ -419,7 +448,7 @@ export function drawMessageBrowser(
448
}
449
450
export function drawMessageAgentPlain(
422
- classes,
451
+ mainClass,
452
messageContainer,
453
id,
454
type,
@@ -434,8 +463,9 @@ export function drawMessageAgentPlain(
463
content,
464
temp,
465
false,
466
+ mainClass,
467
kvps,
438
- [...classes],
468
+ [],
469
[],
470
false,
471
false
@@ -453,7 +483,7 @@ export function drawMessageInfo(
483
kvps = null
484
) {
485
return drawMessageAgentPlain(
456
- ["message-info"],
486
+ "message-info",
487
messageContainer,
488
id,
489
type,
@@ -479,8 +509,9 @@ export function drawMessageUtil(
509
content,
510
temp,
511
false,
512
+ "message-util",
513
kvps,
483
- ["message-util"],
514
+ [],
515
["msg-json"],
516
false,
517
false
@@ -498,7 +529,7 @@ export function drawMessageWarning(
529
kvps = null
530
) {
531
return drawMessageAgentPlain(
501
- ["message-warning"],
532
+ "message-warning",
533
messageContainer,
534
id,
535
type,
@@ -519,7 +550,7 @@ export function drawMessageError(
550
kvps = null
551
) {
552
return drawMessageAgentPlain(
522
- ["message-error"],
553
+ "message-error",
554
messageContainer,
555
id,
556
type,
@@ -537,7 +568,8 @@ function drawKvps(container, kvps, latex) {
568
for (let [key, value] of Object.entries(kvps)) {
569
const row = table.insertRow();
570
row.classList.add("kvps-row");
540
- if (key === "thoughts" || key === "reasoning") // TODO: find a better way to determine special class assignment
571
+ if (key === "thoughts" || key === "reasoning")
572
+ // TODO: find a better way to determine special class assignment
573
row.classList.add("msg-thoughts");
574
575
const th = row.insertCell();
@@ -557,6 +589,11 @@ function drawKvps(container, kvps, latex) {
589
addValue(value);
590
}
591
592
+ // autoscroll the KVP value if needed
593
+ setTimeout(() => {
594
+ tdiv.scrollTop = tdiv.scrollHeight;
595
+ }, 0);
596
+
597
function addValue(value) {
598
if (typeof value === "object") value = JSON.stringify(value, null, 2);
599
@@ -572,7 +609,6 @@ function drawKvps(container, kvps, latex) {
609
imgElement.addEventListener("click", () => {
610
openImageModal(imgElement.src, 1000);
611
});
575
-
612
} else {
613
const pre = document.createElement("pre");
614
// pre.classList.add("kvps-val");
@@ -588,14 +624,14 @@ function drawKvps(container, kvps, latex) {
624
copyText(span.textContent, span);
625
});
626
591
- // KaTeX rendering for markdown
592
- if (latex) {
593
- span.querySelectorAll("latex").forEach((element) => {
594
- katex.render(element.innerHTML, element, {
595
- throwOnError: false,
596
- });
597
- });
598
- }
627
+ // KaTeX rendering for markdown
628
+ if (latex) {
629
+ span.querySelectorAll("latex").forEach((element) => {
630
+ katex.render(element.innerHTML, element, {
631
+ throwOnError: false,
632
+ });
633
+ });
634
+ }
635
}
636
}
637
// } else {
webui/vendor/google/google-icons.css
new
+20
@@ -0,0 +1,20 @@
1
+@font-face {
2
+ font-family: 'Material Symbols Outlined';
3
+ font-style: normal;
4
+ font-weight: 400;
5
+ src: url(./google-icons.ttf) format('truetype');
6
+}
7
+
8
+.material-symbols-outlined {
9
+ font-family: 'Material Symbols Outlined';
10
+ font-weight: normal;
11
+ font-style: normal;
12
+ font-size: 24px;
13
+ line-height: 1;
14
+ letter-spacing: normal;
15
+ text-transform: none;
16
+ display: inline-block;
17
+ white-space: nowrap;
18
+ word-wrap: normal;
19
+ direction: ltr;
20
+}
webui/vendor/google/google-icons.ttf
Binary files /dev/null and b/webui/vendor/google/google-icons.ttf differ