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 +}