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