project finalizing, openrouter embeddings
frdel committed
Nov 13, 2025 at 08:52 UTC
e6eae9c7dd5018f80bc27eb5e54fbc4274b58372
21 files changed
+361
-29
.gitignore
+3
-8
@@ -5,10 +5,9 @@
5
*.py[cod]
6
**/.conda/
7
8
-#Ignore cursor rules
8
+#Ignore IDE files
9
.cursor/
10
.windsurf/
11
-.cursorindexingignore
11
12
# ignore test files in root dir
13
/*.test.py
@@ -43,11 +42,7 @@ instruments/**
42
43
# Global rule to include .gitkeep files anywhere
44
!**/.gitkeep
45
+
46
+# for browser-use
47
agent_history.gif
48
48
-# specify, specstory extension data
49
-.serena
50
-.specify
51
-.specstory
52
-specs/*/.reviews
53
-specs/*/reviews
conf/model_providers.yaml
+9
@@ -107,6 +107,15 @@ embedding:
107
azure:
108
name: OpenAI Azure
109
litellm_provider: azure
110
+ # TODO: OpenRouter not yet supported by LiteLLM, replace with native litellm_provider openrouter and remove api_base when ready
111
+ openrouter:
112
+ name: OpenRouter
113
+ litellm_provider: openai
114
+ kwargs:
115
+ api_base: https://openrouter.ai/api/v1
116
+ extra_headers:
117
+ "HTTP-Referer": "https://agent-zero.ai/"
118
+ "X-Title": "Agent Zero"
119
other:
120
name: Other OpenAI compatible
121
litellm_provider: openai
\ No newline at end of file
conf/projects.default.gitignore
new
+13
@@ -0,0 +1,13 @@
1
+# A0 project meta folder
2
+.a0proj/
3
+
4
+# Python environments & cache
5
+venv/
6
+**/__pycache__/
7
+
8
+# Node.js dependencies
9
+**/node_modules/
10
+**/.npm/
11
+
12
+# Version control metadata
13
+**/.git/
prompts/agent.extras.project.file_structure.md
new
+9
@@ -0,0 +1,9 @@
1
+# File structure of project {{project_name}}
2
+- this is filtered overview not full scan
3
+- list yourself if needed
4
+- maximum depth: {{max_depth}}
5
+- ignored:
6
+{{gitignore}}
7
+
8
+## file tree
9
+{{file_structure}}
\ No newline at end of file
prompts/agent.system.projects.main.md
+1
-1
@@ -1,5 +1,5 @@
1
# Projects
2
- user can create and activate projects
3
-- projects have work folder and instructions
3
+- projects have work folder in /usr/projects/<name> and instructions and config in /usr/projects/<name>/.a0proj
4
- when activated agent works in project follows project instructions
5
- agent cannot manipulate or switch projects
\ No newline at end of file
python/api/projects.py
+14
-1
@@ -25,6 +25,8 @@ class Projects(ApiHandler):
25
data = self.activate_project(ctxid, input.get("name", None))
26
elif action == "deactivate":
27
data = self.deactivate_project(ctxid)
28
+ elif action == "file_structure":
29
+ data = self.get_file_structure(input.get("name", None), input.get("settings"))
30
else:
31
raise Exception("Invalid action")
32
@@ -75,4 +77,15 @@ class Projects(ApiHandler):
77
def deactivate_project(self, context_id: str|None):
78
if not context_id:
79
raise Exception("Context ID is required")
78
- return projects.deactivate_project(context_id)
\ No newline at end of file
80
+ return projects.deactivate_project(context_id)
81
+
82
+ def get_file_structure(self, name: str|None, settings: dict|None):
83
+ if not name:
84
+ raise Exception("Project name is required")
85
+ # project data
86
+ basic_data = projects.load_basic_project_data(name)
87
+ # override file structure settings
88
+ if settings:
89
+ basic_data["file_structure"] = settings # type: ignore
90
+ # get structure
91
+ return projects.get_file_structure(name, basic_data)
\ No newline at end of file
python/extensions/message_loop_prompts_after/_75_include_project_extras.py
new
+47
@@ -0,0 +1,47 @@
1
+from python.helpers.extension import Extension
2
+from agent import LoopData
3
+from python.helpers import projects
4
+
5
+
6
+class IncludeProjectExtras(Extension):
7
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8
+
9
+ # active project
10
+ project_name = projects.get_context_project_name(self.agent.context)
11
+ if not project_name:
12
+ return
13
+
14
+ # project config
15
+ project = projects.load_basic_project_data(project_name)
16
+
17
+ # load file structure if enabled
18
+ if project["file_structure"]["enabled"]:
19
+ file_structure = projects.get_file_structure(project_name)
20
+ gitignore = cleanup_gitignore(project["file_structure"]["gitignore"])
21
+
22
+ # read prompt
23
+ file_structure_prompt = self.agent.read_prompt(
24
+ "agent.extras.project.file_structure.md",
25
+ max_depth=project["file_structure"]["max_depth"],
26
+ gitignore=gitignore,
27
+ project_name=project_name,
28
+ file_structure=file_structure,
29
+ )
30
+ # add file structure to the prompt
31
+ loop_data.extras_temporary["project_file_structure"] = file_structure_prompt
32
+
33
+
34
+def cleanup_gitignore(gitignore_raw: str) -> str:
35
+ """Process gitignore: split lines, strip, remove comments, remove empty lines."""
36
+ gitignore_lines = []
37
+ for line in gitignore_raw.split('\n'):
38
+ # Strip whitespace
39
+ line = line.strip()
40
+ # Remove inline comments (everything after #)
41
+ if '#' in line:
42
+ line = line.split('#')[0].strip()
43
+ # Keep only non-empty lines
44
+ if line:
45
+ gitignore_lines.append(line)
46
+
47
+ return '\n'.join(gitignore_lines) if gitignore_lines else "nothing ignored"
python/helpers/file_tree.py
+4
-4
@@ -28,11 +28,11 @@ def file_tree(
28
max_depth: int = 0,
29
max_lines: int = 0,
30
folders_first: bool = True,
31
- max_folders: int | None = None,
32
- max_files: int | None = None,
33
- sort: tuple[str, str] = (SORT_BY_MODIFIED, SORT_DESC),
31
+ max_folders: int = 0,
32
+ max_files: int = 0,
33
+ sort: tuple[Literal["name", "created", "modified"], Literal["asc", "desc"]] = ("modified", "desc"),
34
ignore: str | None = None,
35
- output_mode: str = OUTPUT_MODE_STRING,
35
+ output_mode: Literal["string", "flat", "nested"] = OUTPUT_MODE_STRING,
36
) -> str | list[dict]:
37
"""Render a directory tree relative to the repository base path.
38
python/helpers/projects.py
+57
-1
@@ -1,7 +1,7 @@
1
import os
2
from typing import Literal, TypedDict, TYPE_CHECKING
3
4
-from python.helpers import files, dirty_json, persist_chat
4
+from python.helpers import files, dirty_json, persist_chat, file_tree
5
from python.helpers.print_style import PrintStyle
6
7
@@ -17,6 +17,15 @@ PROJECT_HEADER_FILE = "project.json"
17
CONTEXT_DATA_KEY_PROJECT = "project"
18
19
20
+class FileStructureInjectionSettings(TypedDict):
21
+ enabled: bool
22
+ max_depth: int
23
+ max_files: int
24
+ max_folders: int
25
+ max_lines: int
26
+ gitignore: str
27
+
28
+
29
class BasicProjectData(TypedDict):
30
title: str
31
description: str
@@ -25,6 +34,7 @@ class BasicProjectData(TypedDict):
34
memory: Literal[
35
"own", "global"
36
] # in the future we can add cutom and point to another existing folder
37
+ file_structure: FileStructureInjectionSettings
38
39
40
class EditProjectData(BasicProjectData):
@@ -73,6 +83,21 @@ def load_project_header(name: str):
83
return header
84
85
86
+def _default_file_structure_settings():
87
+ try:
88
+ gitignore = files.read_file("conf/projects.default.gitignore")
89
+ except Exception:
90
+ gitignore = ""
91
+ return FileStructureInjectionSettings(
92
+ enabled=True,
93
+ max_depth=5,
94
+ max_files=20,
95
+ max_folders=20,
96
+ max_lines=250,
97
+ gitignore=gitignore,
98
+ )
99
+
100
+
101
def _normalizeBasicData(data: BasicProjectData):
102
return BasicProjectData(
103
title=data.get("title", ""),
@@ -80,6 +105,10 @@ def _normalizeBasicData(data: BasicProjectData):
105
instructions=data.get("instructions", ""),
106
color=data.get("color", ""),
107
memory=data.get("memory", "own"),
108
+ file_structure=data.get(
109
+ "file_structure",
110
+ _default_file_structure_settings(),
111
+ ),
112
)
113
114
@@ -95,6 +124,10 @@ def _normalizeEditData(data: EditProjectData):
124
knowledge_files_count=data.get("knowledge_files_count", 0),
125
secrets=data.get("secrets", ""),
126
memory=data.get("memory", "own"),
127
+ file_structure=data.get(
128
+ "file_structure",
129
+ _default_file_structure_settings(),
130
+ ),
131
)
132
133
@@ -324,3 +357,26 @@ def get_knowledge_files_count(name: str):
357
get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR)
358
)
359
return len(files.list_files_in_dir_recursively(knowledge_folder))
360
+
361
+def get_file_structure(name: str, basic_data: BasicProjectData|None=None) -> str:
362
+ project_folder = get_project_folder(name)
363
+ if basic_data is None:
364
+ basic_data = load_basic_project_data(name)
365
+
366
+ tree = str(file_tree.file_tree(
367
+ project_folder,
368
+ max_depth=basic_data["file_structure"]["max_depth"],
369
+ max_files=basic_data["file_structure"]["max_files"],
370
+ max_folders=basic_data["file_structure"]["max_folders"],
371
+ max_lines=basic_data["file_structure"]["max_lines"],
372
+ ignore=basic_data["file_structure"]["gitignore"],
373
+ output_mode=file_tree.OUTPUT_MODE_STRING
374
+ ))
375
+
376
+ # empty?
377
+ if "\n" not in tree:
378
+ tree += "\n # Empty"
379
+
380
+ return tree
381
+
382
+
\ No newline at end of file
python/helpers/strings.py
+1
@@ -168,6 +168,7 @@ def replace_file_includes(text: str, placeholder_pattern: str = r"§§include\((
168
path = match.group(1)
169
try:
170
# read file content
171
+ path = files.fix_dev_path(path)
172
return files.read_file(path)
173
except Exception:
174
# if file not readable keep original placeholder
requirements.dev.txt
new
+3
@@ -0,0 +1,3 @@
1
+pytest>=8.4.2
2
+pytest-asyncio>=1.2.0
3
+pytest-mock>=3.15.1
\ No newline at end of file
requirements2.txt
+1
-1
@@ -1,2 +1,2 @@
1
-litellm==1.79.0
1
+litellm==1.79.3
2
openai==1.99.5
\ No newline at end of file
webui/components/chat/speech/speech-store.js
+11
-6
@@ -2,6 +2,7 @@ import { createStore } from "/js/AlpineStore.js";
2
import { updateChatInput, sendMessage } from "/index.js";
3
import { sleep } from "/js/sleep.js";
4
import { store as microphoneSettingStore } from "/components/settings/speech/microphone-setting-store.js";
5
+import * as shortcuts from "/js/shortcuts.js";
6
7
const Status = {
8
INACTIVE: "inactive",
@@ -94,7 +95,9 @@ const model = {
95
async init() {
96
// Guard against multiple initializations
97
if (this._initialized) {
97
- console.log('[Speech Store] Already initialized, skipping duplicate init()');
98
+ console.log(
99
+ "[Speech Store] Already initialized, skipping duplicate init()"
100
+ );
101
return;
102
}
103
@@ -368,11 +371,13 @@ const model = {
371
372
// Show a prompt to user to enable audio
373
showAudioPermissionPrompt() {
371
- if (window.toast) {
372
- window.toast("Click anywhere to enable audio playback", "info", 5000);
373
- } else {
374
- console.log("Please click anywhere on the page to enable audio playback");
375
- }
374
+ shortcuts.frontendNotification({
375
+ type: "info",
376
+ message: "Click anywhere to enable audio playback",
377
+ displayTime: 5000,
378
+ frontendOnly: true,
379
+ });
380
+ console.log("Please click anywhere on the page to enable audio playback");
381
},
382
383
// Browser TTS
webui/components/notifications/notification-toast-stack.html
+2
@@ -82,6 +82,8 @@
82
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
83
position: relative;
84
min-height: 60px;
85
+ max-height: 15em;
86
+ overflow-y: auto;
87
}
88
89
.toast-item:hover {
webui/components/projects/project-edit-file-structure.html
new
+102
@@ -0,0 +1,102 @@
1
+<html>
2
+
3
+<head>
4
+ <title>Create a new project</title>
5
+ <script type="module">
6
+ import { store } from "/components/projects/projects-store.js";
7
+ </script>
8
+</head>
9
+
10
+<body>
11
+ <div x-data>
12
+ <template x-if="$store.projects && $store.projects.selectedProject">
13
+ <div>
14
+ <!-- <div class="project-detail-header">
15
+ <div class="projects-project-card-title">Project details</div>
16
+ <div class="project-path" x-text="$store.projects.selectedProject.path"></div>
17
+ </div> -->
18
+
19
+
20
+ <div class="projects-setting-row" style="padding: 1rem 0;">
21
+ <div class="projects-setting-text">
22
+ <label class="projects-form-label">Inject project structure into context window</label>
23
+ <span class="projects-form-description">When turned on, the project file structure will be
24
+ injected into the context window of the agent. This is useful for agents that need to
25
+ have an overview of project folders and files at all times.</span>
26
+ </div>
27
+ <div class="projects-setting-control">
28
+ <label class="toggle">
29
+ <input type="checkbox" x-model="$store.projects.selectedProject.file_structure.enabled">
30
+ <span class="toggler"></span>
31
+ </label>
32
+ </div>
33
+ </div>
34
+
35
+ <div x-show="$store.projects.selectedProject.file_structure.enabled">
36
+
37
+
38
+ <div class="projects-form-group">
39
+ <label class="projects-form-label">Max Depth</label>
40
+ <span class="projects-form-description">Set the maximum depth for the file structure (0 =
41
+ unlimited).</span>
42
+ <input type="number" class="projects-form-input"
43
+ x-model.number="$store.projects.selectedProject.file_structure.max_depth" min="0" step="1"
44
+ placeholder="0">
45
+ </div>
46
+
47
+ <div class="projects-form-group">
48
+ <label class="projects-form-label">Max Lines</label>
49
+ <span class="projects-form-description">Maximum total lines outputted for the agent. This limits
50
+ the
51
+ space occupied in the context window (0 = unlimited).</span>
52
+ <input type="number" class="projects-form-input"
53
+ x-model.number="$store.projects.selectedProject.file_structure.max_lines" min="0" step="1"
54
+ placeholder="0">
55
+ </div>
56
+
57
+ <div class="projects-form-group">
58
+ <label class="projects-form-label">Max Folders</label>
59
+ <span class="projects-form-description">Maximum number of subfolders to display under one folder
60
+ (0 =
61
+ unlimited).</span>
62
+ <input type="number" class="projects-form-input"
63
+ x-model.number="$store.projects.selectedProject.file_structure.max_folders" min="0" step="1"
64
+ placeholder="0">
65
+ </div>
66
+
67
+ <div class="projects-form-group">
68
+ <label class="projects-form-label">Max Files</label>
69
+ <span class="projects-form-description">Maximum number of files to display under one folder (0 =
70
+ unlimited).</span>
71
+ <input type="number" class="projects-form-input"
72
+ x-model.number="$store.projects.selectedProject.file_structure.max_files" min="0" step="1"
73
+ placeholder="0">
74
+ </div>
75
+
76
+ <div class="projects-form-group">
77
+ <label class="projects-form-label">Ignored files / folders</label>
78
+ <span class="projects-form-description">Specify patterns in gitignore format. These files and
79
+ folders will be skipped during analysis, such as meta folders, cache directories, packages,
80
+ and
81
+ build artifacts.</span>
82
+ <textarea class="projects-form-textarea"
83
+ x-model="$store.projects.selectedProject.file_structure.gitignore" rows="5"
84
+ placeholder="Enter gitignore patterns (e.g., node_modules/, .git/, *.log)"></textarea>
85
+ </div>
86
+
87
+ <button class="button icon-button" x-on:click="$store.projects.testFileStructure()">
88
+ <span class="icon material-symbols-outlined">account_tree</span>
89
+ <span>Test output</span>
90
+ </button>
91
+
92
+
93
+ </div>
94
+
95
+ </div>
96
+ </template>
97
+ </div>
98
+</body>
99
+<style>
100
+</style>
101
+
102
+</html>
\ No newline at end of file
webui/components/projects/project-edit.html
+8
@@ -50,6 +50,14 @@
50
</x-component>
51
</div>
52
53
+ <div class="project-detail">
54
+ <div class="project-detail-header">
55
+ <span class="projects-project-card-title">File structure</span>
56
+ </div>
57
+ <x-component path="projects/project-edit-file-structure.html">
58
+ </x-component>
59
+ </div>
60
+
61
<div class="project-detail">
62
<div class="project-detail-header">
63
<span class="projects-project-card-title">Secrets</span>
webui/components/projects/project-file-structure-test.html
new
+25
@@ -0,0 +1,25 @@
1
+<html>
2
+
3
+<head>
4
+ <title>File structure test</title>
5
+ <script type="module">
6
+ import { store } from "/components/projects/projects-store.js";
7
+ </script>
8
+</head>
9
+
10
+<body>
11
+ <div x-data>
12
+ <template x-if="$store.projects && $store.projects.selectedProject">
13
+ <div>
14
+
15
+ <h3>File structure test - <span x-text="$store.projects.selectedProject.name"></span></h3>
16
+ <pre x-text="$store.projects.fileStructureTestOutput"></pre>
17
+
18
+ </div>
19
+ </template>
20
+ </div>
21
+</body>
22
+<style>
23
+</style>
24
+
25
+</html>
\ No newline at end of file
webui/components/projects/project-selector.html
+2
-1
@@ -26,7 +26,8 @@
26
</template>
27
28
<div x-show="open" x-init="$store.projects.loadProjectsList()" class="project-dropdown-menu" style="display: none;" x-transition>
29
- <a href="#" @click.prevent="$store.projects.openProjectsModal(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">snippet_folder</span> <span class="project-selector-item-text">Edit Projects</span></a>
29
+ <a href="#" @click.prevent="$store.projects.openProjectsModal(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">snippet_folder</span> <span class="project-selector-item-text">Projects</span></a>
30
+ <a x-show="$store.chats.selectedContext.project?.name" href="#" @click.prevent="$store.projects.editActiveProject(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">edit</span> <span class="project-selector-item-text">Edit <span x-text="$store.chats.selectedContext.project?.title"></span></span></a>
31
<a x-show="$store.chats.selectedContext.project?.name" href="#" @click.prevent="$store.projects.deactivateProject(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">close</span> <span class="project-selector-item-text">Deactivate</span></a>
32
<!-- <a href="#" @click.prevent="$store.projects.openProjectsModal(); $store.projects.openCreateModal(); open = false;" class="project-dropdown-item">Create Project</a> -->
33
webui/components/projects/projects-store.js
+27
@@ -378,6 +378,33 @@ const model = {
378
.join("/")
379
.replace(/\/+/g, "/");
380
},
381
+
382
+ async editActiveProject() {
383
+ const ctx = shortcuts.getCurrentContext();
384
+ if(!ctx) return;
385
+ this.openEditModal(ctx.project.name);
386
+ },
387
+
388
+ async testFileStructure() {
389
+ try {
390
+ const response = await api.callJsonApi("projects", {
391
+ action: "file_structure",
392
+ name: this.selectedProject.name,
393
+ settings: this.selectedProject.file_structure,
394
+ });
395
+ this.fileStructureTestOutput = response.data;
396
+ shortcuts.openModal("projects/project-file-structure-test.html");
397
+ } catch (error) {
398
+ console.error("Error testing file structure:", error);
399
+ shortcuts.frontendNotification({
400
+ type: shortcuts.NotificationType.ERROR,
401
+ message: "Error testing file structure",
402
+ priority: shortcuts.NotificationPriority.NORMAL,
403
+ displayTime: 3,
404
+ frontendOnly: true,
405
+ });
406
+ }
407
+ },
408
};
409
410
// convert it to alpine store
webui/components/sidebar/chats/chats-store.js
+4
@@ -23,6 +23,10 @@ const model = {
23
return this.selected;
24
},
25
26
+ getSelectedContext(){
27
+ return this.selectedContext;
28
+ },
29
+
30
init() {
31
// Initialize from localStorage
32
const lastSelectedChat = localStorage.getItem("lastSelectedChat");
webui/js/shortcuts.js
+18
-6
@@ -1,9 +1,10 @@
1
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
2
import { callJsonApi } from "/js/api.js";
3
+import * as modals from "/js/modals.js";
4
import {
5
NotificationType,
6
NotificationPriority,
6
- store as notificationStore
7
+ store as notificationStore,
8
} from "/components/notifications/notification-store.js";
9
10
// shortcuts utils for convenience
@@ -12,13 +13,24 @@ import {
13
export { callJsonApi };
14
15
// notifications
15
-export {
16
- NotificationType,
17
- NotificationPriority,
18
-}
19
-export const frontendNotification = notificationStore.frontendNotification.bind(notificationStore);
16
+export { NotificationType, NotificationPriority };
17
+export const frontendNotification =
18
+ notificationStore.frontendNotification.bind(notificationStore);
19
20
// chat context
21
export function getCurrentContextId() {
22
return chatsStore.getSelectedChatId();
23
}
24
+
25
+export function getCurrentContext(){
26
+ return chatsStore.getSelectedContext();
27
+}
28
+
29
+// modals
30
+export function openModal(modalPath) {
31
+ return modals.openModal(modalPath);
32
+}
33
+
34
+export function closeModal(modalPath = null) {
35
+ return modals.closeModal(modalPath);
36
+}