chore(conductor): Mark track 'Saved Tool Presets' as complete
This commit is contained in:
@@ -105,7 +105,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*Link: [./tracks/saved_presets_20260308/](./tracks/saved_presets_20260308/)*
|
*Link: [./tracks/saved_presets_20260308/](./tracks/saved_presets_20260308/)*
|
||||||
*Goal: Ability to have saved presets for global and project system prompts. Includes full AI profiles with temperature and top_p settings, managed via a dedicated GUI modal.*
|
*Goal: Ability to have saved presets for global and project system prompts. Includes full AI profiles with temperature and top_p settings, managed via a dedicated GUI modal.*
|
||||||
|
|
||||||
2. [ ] **Track: Saved Tool Presets**
|
2. [x] **Track: Saved Tool Presets**
|
||||||
*Link: [./tracks/saved_tool_presets_20260308/](./tracks/saved_tool_presets_20260308/)*
|
*Link: [./tracks/saved_tool_presets_20260308/](./tracks/saved_tool_presets_20260308/)*
|
||||||
*Goal: Make agent tools have presets. Add flags for tools related to their level of approval (auto, ask). Move tools to ai settings. Put tools in dynamic TOML-defined categories (Python, General, etc.). Tool Presets added to mma agent role options.*
|
*Goal: Make agent tools have presets. Add flags for tools related to their level of approval (auto, ask). Move tools to ai settings. Put tools in dynamic TOML-defined categories (Python, General, etc.). Tool Presets added to mma agent role options.*
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
# Implementation Plan: Saved Tool Presets
|
# Implementation Plan: Saved Tool Presets
|
||||||
|
|
||||||
## Phase 1: Data Model & Storage
|
## Phase 1: Data Model & Storage
|
||||||
- [ ] Task: Define the `ToolPreset` data model and storage logic.
|
- [x] Task: Define the `ToolPreset` data model and storage logic.
|
||||||
- [ ] Create `src/tool_presets.py` to handle loading/saving to `tool_presets.toml`.
|
- [x] Create `src/tool_presets.py` to handle loading/saving to `tool_presets.toml`.
|
||||||
- [ ] Implement `ToolPresetManager` to manage CRUD operations for presets and categorization.
|
- [x] Implement `ToolPresetManager` to manage CRUD operations for presets and categorization.
|
||||||
- [ ] Task: Write unit tests for `ToolPresetManager`.
|
- [x] Task: Write unit tests for `ToolPresetManager`.
|
||||||
- [ ] Test loading tool presets from TOML.
|
- [x] Test loading tool presets from TOML.
|
||||||
- [ ] Test saving tool presets to TOML.
|
- [x] Test saving tool presets to TOML.
|
||||||
- [ ] Test dynamic category parsing.
|
- [x] Test dynamic category parsing.
|
||||||
- [ ] Test tool approval flag persistence.
|
- [x] Test tool approval flag persistence.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Data Model & Storage' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Data Model & Storage' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: UI Integration (AI Settings)
|
## Phase 2: UI Integration (AI Settings)
|
||||||
- [ ] Task: Relocate tool settings to the AI Settings panel.
|
- [x] Task: Relocate tool settings to the AI Settings panel.
|
||||||
- [ ] Modify `gui_2.py` to remove the current tool listing from the main panel and move it to the AI Settings panel (global/project).
|
- [x] Modify `gui_2.py` to remove the current tool listing from the main panel and move it to the AI Settings panel (global/project).
|
||||||
- [ ] Task: Implement dynamic tool categorization UI.
|
- [x] Task: Implement dynamic tool categorization UI.
|
||||||
- [ ] Modify `gui_2.py` to render tools in sections based on categories defined in `tool_presets.toml`.
|
- [x] Modify `gui_2.py` to render tools in sections based on categories defined in `tool_presets.toml`.
|
||||||
- [ ] Implement toggleable "auto"/"ask" flags for each tool.
|
- [x] Implement toggleable "auto"/"ask" flags for each tool.
|
||||||
- [ ] Task: Implement Tool Preset dropdown for MMA agent roles.
|
- [x] Task: Implement Tool Preset dropdown for MMA agent roles.
|
||||||
- [ ] Add the "Tool Preset" dropdown to the MMA agent role configuration modal in `gui_2.py`.
|
- [x] Add the "Tool Preset" dropdown to the MMA agent role configuration modal in `gui_2.py`.
|
||||||
- [ ] Task: Write integration tests for AI Settings UI using `live_gui`.
|
- [x] Task: Write integration tests for AI Settings UI using `live_gui`.
|
||||||
- [ ] Verify tools are categorized correctly in the UI.
|
- [x] Verify tools are categorized correctly in the UI.
|
||||||
- [ ] Verify toggling a tool's approval persists correctly.
|
- [x] Verify toggling a tool's approval persists correctly.
|
||||||
- [ ] Verify the "Tool Preset" dropdown shows all available presets.
|
- [x] Verify the "Tool Preset" dropdown shows all available presets.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: UI Integration (AI Settings)' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: UI Integration (AI Settings)' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 3: AI Client & Execution Integration
|
## Phase 3: AI Client & Execution Integration
|
||||||
- [ ] Task: Integrate tool presets into the AI Client.
|
- [x] Task: Integrate tool presets into the AI Client.
|
||||||
- [ ] Modify `src/ai_client.py` to load and apply the selected tool preset for a given agent role.
|
- [x] Modify `src/ai_client.py` to load and apply the selected tool preset for a given agent role.
|
||||||
- [ ] Implement logic to restrict available tools and enforce "auto"/"ask" behavior based on the preset.
|
- [x] Implement logic to restrict available tools and enforce "auto"/"ask" behavior based on the preset.
|
||||||
- [ ] Task: Update MMA delegation to pass the selected tool preset.
|
- [x] Task: Update MMA delegation to pass the selected tool preset.
|
||||||
- [ ] Modify `scripts/mma_exec.py` and `src/multi_agent_conductor.py` to pass the `tool_preset` to sub-agents.
|
- [x] Modify `scripts/mma_exec.py` and `src/multi_agent_conductor.py` to pass the `tool_preset` to sub-agents.
|
||||||
- [ ] Task: Write integration tests for AI execution with tool presets.
|
- [x] Task: Write integration tests for AI execution with tool presets.
|
||||||
- [ ] Verify agents only have access to tools in their assigned preset.
|
- [x] Verify agents only have access to tools in their assigned preset.
|
||||||
- [ ] Verify "auto" tools execute without prompting, and "ask" tools require confirmation.
|
- [x] Verify "auto" tools execute without prompting, and "ask" tools require confirmation.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: AI Client & Execution Integration' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: AI Client & Execution Integration' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Final Integration & Polish
|
## Phase 4: Final Integration & Polish
|
||||||
- [ ] Task: Implement Preset Manager Modal.
|
- [x] Task: Implement Preset Manager Modal.
|
||||||
- [ ] Create a modal for creating, editing, and deleting tool presets.
|
- [x] Create a modal for creating, editing, and deleting tool presets.
|
||||||
- [ ] Task: Final UI polish (spacing, icons, tooltips).
|
- [x] Task: Final UI polish (spacing, icons, tooltips).
|
||||||
- [ ] Task: Run full suite of relevant tests.
|
- [x] Task: Run full suite of relevant tests.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Response = true
|
|||||||
"Tool Calls" = false
|
"Tool Calls" = false
|
||||||
Theme = true
|
Theme = true
|
||||||
"Log Management" = true
|
"Log Management" = true
|
||||||
Diagnostics = true
|
Diagnostics = false
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "Nord Dark"
|
palette = "Nord Dark"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Size=1111,224
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
Pos=1461,1220
|
Pos=694,1182
|
||||||
Size=913,631
|
Size=913,631
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ DockId=0xAFC85805,2
|
|||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,850
|
Pos=0,850
|
||||||
Size=629,1287
|
Size=659,1287
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,2
|
DockId=0x00000002,2
|
||||||
|
|
||||||
@@ -85,13 +85,13 @@ Collapsed=0
|
|||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=2833,28
|
Pos=2833,28
|
||||||
Size=1007,1695
|
Size=1007,2109
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000C,2
|
DockId=0x0000000C,2
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,850
|
Pos=0,850
|
||||||
Size=629,1287
|
Size=659,1287
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000002,1
|
||||||
|
|
||||||
@@ -102,26 +102,26 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=1701,28
|
Pos=1660,28
|
||||||
Size=1130,2109
|
Size=1243,2109
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000013,0
|
DockId=0x00000013,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=631,28
|
Pos=661,28
|
||||||
Size=1068,2109
|
Size=997,2109
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000012,0
|
DockId=0x00000012,0
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,850
|
Pos=0,850
|
||||||
Size=629,1287
|
Size=659,1287
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,28
|
Pos=0,28
|
||||||
Size=629,820
|
Size=659,820
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
@@ -131,14 +131,14 @@ Size=416,325
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][MMA Dashboard]
|
[Window][MMA Dashboard]
|
||||||
Pos=2833,28
|
Pos=2905,28
|
||||||
Size=1007,1872
|
Size=935,2109
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000C,0
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=2833,28
|
Pos=2905,28
|
||||||
Size=1007,1872
|
Size=935,2109
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000C,1
|
DockId=0x0000000C,1
|
||||||
|
|
||||||
@@ -148,28 +148,28 @@ Size=262,209
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tier 1: Strategy]
|
[Window][Tier 1: Strategy]
|
||||||
Pos=2833,1902
|
Pos=2833,1660
|
||||||
Size=1007,235
|
Size=1007,477
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000F,0
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tier 2: Tech Lead]
|
[Window][Tier 2: Tech Lead]
|
||||||
Pos=2833,1902
|
Pos=2833,1238
|
||||||
Size=1007,235
|
Size=1007,899
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000F,1
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tier 4: QA]
|
[Window][Tier 4: QA]
|
||||||
Pos=2833,1902
|
Pos=2833,1238
|
||||||
Size=1007,235
|
Size=1007,899
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000F,2
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
[Window][Tier 3: Workers]
|
[Window][Tier 3: Workers]
|
||||||
Pos=631,28
|
Pos=2833,1238
|
||||||
Size=1068,2109
|
Size=1007,899
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000012,1
|
DockId=0x0000000F,1
|
||||||
|
|
||||||
[Window][Approve PowerShell Command]
|
[Window][Approve PowerShell Command]
|
||||||
Pos=649,435
|
Pos=649,435
|
||||||
@@ -327,15 +327,20 @@ Size=956,942
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Task DAG]
|
[Window][Task DAG]
|
||||||
Pos=2397,1224
|
Pos=1700,1199
|
||||||
Size=1340,621
|
Size=1079,662
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Usage Analytics]
|
[Window][Usage Analytics]
|
||||||
Pos=2530,426
|
Pos=1661,426
|
||||||
Size=275,375
|
Size=275,375
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Tool Preset Manager]
|
||||||
|
Pos=827,642
|
||||||
|
Size=973,688
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
[Table][0xFB6E3870,4]
|
[Table][0xFB6E3870,4]
|
||||||
RefScale=13
|
RefScale=13
|
||||||
Column 0 Width=80
|
Column 0 Width=80
|
||||||
@@ -425,18 +430,18 @@ DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
|||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=3840,2109 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=3840,2109 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2831,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2903,1183 Split=X
|
||||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=629,858 Split=Y Selected=0x8CA2375C
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=659,858 Split=Y Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,820 CentralNode=1 Selected=0x7BD57D6A
|
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,820 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,1287 Selected=0x1DCB2623
|
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,1287 Selected=0xF4139CA2
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2200,858 Split=X Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2242,858 Split=X Selected=0x418C7449
|
||||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1068,402 Selected=0x418C7449
|
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=997,402 Selected=0x418C7449
|
||||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1130,402 Selected=0x6F2B5B04
|
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1243,402 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1007,1183 Split=Y Selected=0x3AEC3498
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=935,1183 Split=Y Selected=0x3AEC3498
|
||||||
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=1074,1872 Selected=0x3AEC3498
|
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=1074,1208 Selected=0x3AEC3498
|
||||||
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=1074,235 Selected=0xBB346584
|
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=1074,899 Selected=0x5CDB7A4B
|
||||||
|
|
||||||
;;;<<<Layout_655921752_Default>>>;;;
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
;;;<<<HelloImGui_Misc>>>;;;
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def get_dependencies(filepath: str) -> list[str]:
|
|||||||
print(f"Error getting dependencies for {filepath}: {e}")
|
print(f"Error getting dependencies for {filepath}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def execute_agent(role: str, prompt: str, docs: list[str], debug: bool = False, failure_count: int = 0) -> str:
|
def execute_agent(role: str, prompt: str, docs: list[str], debug: bool = False, failure_count: int = 0, tool_preset: str | None = None) -> str:
|
||||||
model = get_model_for_role(role, failure_count)
|
model = get_model_for_role(role, failure_count)
|
||||||
|
|
||||||
# Advanced Context: Dependency skeletons for Tier 3
|
# Advanced Context: Dependency skeletons for Tier 3
|
||||||
@@ -199,12 +199,14 @@ def execute_agent(role: str, prompt: str, docs: list[str], debug: bool = False,
|
|||||||
try:
|
try:
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["GEMINI_CLI_HOOK_CONTEXT"] = "mma_headless"
|
env["GEMINI_CLI_HOOK_CONTEXT"] = "mma_headless"
|
||||||
|
if tool_preset is not None:
|
||||||
|
env["SLOP_TOOL_PRESET"] = tool_preset
|
||||||
if debug:
|
if debug:
|
||||||
print("--- MMA DEBUG ---")
|
print("--- MMA DEBUG ---")
|
||||||
print(f"Executing Command: {cmd}")
|
print(f"Executing Command: {cmd}")
|
||||||
print("Relevant Environment Variables:")
|
print("Relevant Environment Variables:")
|
||||||
for key, value in env.items():
|
for key, value in env.items():
|
||||||
if key.startswith("GEMINI_CLI_"):
|
if key.startswith("GEMINI_CLI_") or key == "SLOP_TOOL_PRESET":
|
||||||
print(f" {key}={value}")
|
print(f" {key}={value}")
|
||||||
process = subprocess.run(cmd, input=command_text, capture_output=True, text=True, encoding='utf-8', env=env)
|
process = subprocess.run(cmd, input=command_text, capture_output=True, text=True, encoding='utf-8', env=env)
|
||||||
if debug:
|
if debug:
|
||||||
@@ -257,6 +259,11 @@ def create_parser() -> argparse.ArgumentParser:
|
|||||||
default=0,
|
default=0,
|
||||||
help="Number of times this task has failed previously"
|
help="Number of times this task has failed previously"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tool-preset",
|
||||||
|
type=str,
|
||||||
|
help="The tool preset to use"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"prompt",
|
"prompt",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -272,6 +279,7 @@ def main() -> None:
|
|||||||
prompt = args.prompt
|
prompt = args.prompt
|
||||||
debug = args.debug
|
debug = args.debug
|
||||||
failure_count = args.failure_count
|
failure_count = args.failure_count
|
||||||
|
tool_preset = args.tool_preset
|
||||||
docs = []
|
docs = []
|
||||||
if args.task_file and os.path.exists(args.task_file):
|
if args.task_file and os.path.exists(args.task_file):
|
||||||
with open(args.task_file, "rb") as f:
|
with open(args.task_file, "rb") as f:
|
||||||
@@ -282,6 +290,7 @@ def main() -> None:
|
|||||||
# Only override debug if it's explicitly set in the task file (optional)
|
# Only override debug if it's explicitly set in the task file (optional)
|
||||||
debug = task_data.get("debug", debug)
|
debug = task_data.get("debug", debug)
|
||||||
failure_count = task_data.get("failure_count", failure_count)
|
failure_count = task_data.get("failure_count", failure_count)
|
||||||
|
tool_preset = task_data.get("tool_preset", tool_preset)
|
||||||
if not role or not prompt:
|
if not role or not prompt:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
@@ -293,8 +302,8 @@ def main() -> None:
|
|||||||
for ref in file_refs:
|
for ref in file_refs:
|
||||||
if os.path.exists(ref) and ref not in docs:
|
if os.path.exists(ref) and ref not in docs:
|
||||||
docs.append(ref)
|
docs.append(ref)
|
||||||
print(f"Executing role: {role} with docs: {docs} (debug={debug}, failure_count={failure_count})")
|
print(f"Executing role: {role} with docs: {docs} (debug={debug}, failure_count={failure_count}, tool_preset={tool_preset})")
|
||||||
result = execute_agent(role, prompt, docs, debug=debug, failure_count=failure_count)
|
result = execute_agent(role, prompt, docs, debug=debug, failure_count=failure_count, tool_preset=tool_preset)
|
||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
9
scripts/tasks/mma_exec_tool_presets.toml
Normal file
9
scripts/tasks/mma_exec_tool_presets.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
prompt = """
|
||||||
|
In scripts/mma_exec.py:
|
||||||
|
1. In 'create_parser', add '--tool-preset' argument.
|
||||||
|
2. In 'execute_agent', add 'tool_preset: str | None = None' parameter.
|
||||||
|
3. Inside 'execute_agent', if 'tool_preset' is not None, set 'env["SLOP_TOOL_PRESET"] = tool_preset'.
|
||||||
|
4. In 'main', extract 'tool_preset' from 'args.tool_preset' or 'task_data.get("tool_preset")'.
|
||||||
|
5. Pass 'tool_preset' to the 'execute_agent' call.
|
||||||
|
Use 1-space indentation.
|
||||||
|
"""
|
||||||
6
scripts/tasks/multi_agent_tool_presets.toml
Normal file
6
scripts/tasks/multi_agent_tool_presets.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
prompt = """
|
||||||
|
In src/multi_agent_conductor.py:
|
||||||
|
1. In 'ConductorEngine.run' (around line 225), when creating the 'WorkerContext' instance, include 'tool_preset=self.tier_usage["Tier 3"]["tool_preset"]'.
|
||||||
|
2. In 'run_worker_lifecycle' (around line 315), after the 'ai_client.set_provider' call, add a call to 'ai_client.set_tool_preset(context.tool_preset)'.
|
||||||
|
Use 1-space indentation.
|
||||||
|
"""
|
||||||
5
scripts/tasks/test_harness_tool_presets.toml
Normal file
5
scripts/tasks/test_harness_tool_presets.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
prompt = """
|
||||||
|
1. In tests/conftest.py, add 'env["SLOP_GLOBAL_TOOL_PRESETS"] = str((temp_workspace / "tool_presets.toml").absolute())' to the 'live_gui' fixture after the SLOP_GLOBAL_PRESETS line.
|
||||||
|
2. In src/paths.py, update 'get_global_tool_presets_path' to return 'Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml"))'.
|
||||||
|
Use 1-space indentation.
|
||||||
|
"""
|
||||||
9
scripts/tasks/tier_tool_presets.toml
Normal file
9
scripts/tasks/tier_tool_presets.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
prompt = """
|
||||||
|
In src/gui_2.py, modify '_render_mma_dashboard' inside the 'Tier Model Config' collapsing header (around line 2706):
|
||||||
|
1. For each tier row, add a 'Tool Preset' combo box.
|
||||||
|
2. Populate the combo box with sorted names of tool presets from 'self.controller.tool_presets'.
|
||||||
|
3. Include an option 'None' (default) which clears the preset.
|
||||||
|
4. Save the selected preset name into 'self.mma_tier_usage[tier]["tool_preset"]'.
|
||||||
|
5. Adjust item widths (e.g., Provider: 80, Model: 150, Preset: -1) to ensure they all fit on one line or look organized.
|
||||||
|
Use 1-space indentation.
|
||||||
|
"""
|
||||||
16
scripts/tasks/tool_preset_modal.toml
Normal file
16
scripts/tasks/tool_preset_modal.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
prompt = """
|
||||||
|
In src/gui_2.py:
|
||||||
|
1. In 'App.__init__', initialize tool preset editing state:
|
||||||
|
self._editing_tool_preset_name = ''
|
||||||
|
self._editing_tool_preset_categories = {}
|
||||||
|
self._editing_tool_preset_scope = 'project'
|
||||||
|
self._selected_tool_preset_idx = -1
|
||||||
|
2. Implement 'App._render_tool_preset_manager_modal(self)'. Use 'imgui.begin_popup_modal("Tool Preset Manager", self.show_tool_preset_manager_modal)'.
|
||||||
|
3. The modal should have a split layout using child regions:
|
||||||
|
- Left (fixed width): Listbox of all tool presets from 'self.controller.tool_presets'.
|
||||||
|
- Right (flexible): Fields to edit name, scope (radio), and a tree-view to manage categories and tools.
|
||||||
|
4. For each category in editing state, show tools. Since we can't easily add NEW tools to categories in this UI yet, focus on editing the approval of EXISTING tools in the category.
|
||||||
|
5. Add 'Save' and 'Delete' buttons that call 'self.controller._cb_save_tool_preset' and 'self.controller._cb_delete_tool_preset'.
|
||||||
|
6. Call '_render_tool_preset_manager_modal' in '_gui_func'.
|
||||||
|
Use 1-space indentation.
|
||||||
|
"""
|
||||||
@@ -98,6 +98,8 @@ tool_log_callback: Optional[Callable[[str, str], None]] = None
|
|||||||
|
|
||||||
_local_storage = threading.local()
|
_local_storage = threading.local()
|
||||||
|
|
||||||
|
_tool_approval_modes: dict[str, str] = {}
|
||||||
|
|
||||||
def get_current_tier() -> Optional[str]:
|
def get_current_tier() -> Optional[str]:
|
||||||
"""Returns the current tier from thread-local storage."""
|
"""Returns the current tier from thread-local storage."""
|
||||||
return getattr(_local_storage, "current_tier", None)
|
return getattr(_local_storage, "current_tier", None)
|
||||||
@@ -468,6 +470,35 @@ def set_agent_tools(tools: dict[str, bool]) -> None:
|
|||||||
_agent_tools = tools
|
_agent_tools = tools
|
||||||
_CACHED_ANTHROPIC_TOOLS = None
|
_CACHED_ANTHROPIC_TOOLS = None
|
||||||
|
|
||||||
|
def set_tool_preset(preset_name: Optional[str]) -> None:
|
||||||
|
"""Loads a tool preset and applies it via set_agent_tools."""
|
||||||
|
global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _tool_approval_modes
|
||||||
|
_tool_approval_modes = {}
|
||||||
|
if not preset_name or preset_name == "None":
|
||||||
|
# Enable all tools if no preset
|
||||||
|
_agent_tools = {name: True for name in mcp_client.TOOL_NAMES}
|
||||||
|
_agent_tools[TOOL_NAME] = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from src.tool_presets import ToolPresetManager
|
||||||
|
manager = ToolPresetManager()
|
||||||
|
presets = manager.load_all()
|
||||||
|
if preset_name in presets:
|
||||||
|
preset = presets[preset_name]
|
||||||
|
new_tools = {name: False for name in mcp_client.TOOL_NAMES}
|
||||||
|
new_tools[TOOL_NAME] = False
|
||||||
|
for cat in preset.categories.values():
|
||||||
|
for tool_entry in cat:
|
||||||
|
if isinstance(tool_entry, dict) and "name" in tool_entry:
|
||||||
|
name = tool_entry["name"]
|
||||||
|
new_tools[name] = True
|
||||||
|
_tool_approval_modes[name] = tool_entry.get("mode", "ask")
|
||||||
|
_agent_tools = new_tools
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write(f"[ERROR] Failed to set tool preset '{preset_name}': {e}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
_CACHED_ANTHROPIC_TOOLS = None
|
||||||
|
|
||||||
def _build_anthropic_tools() -> list[dict[str, Any]]:
|
def _build_anthropic_tools() -> list[dict[str, Any]]:
|
||||||
mcp_tools: list[dict[str, Any]] = []
|
mcp_tools: list[dict[str, Any]] = []
|
||||||
for spec in mcp_client.MCP_TOOL_SPECS:
|
for spec in mcp_client.MCP_TOOL_SPECS:
|
||||||
@@ -621,10 +652,17 @@ async def _execute_single_tool_call_async(
|
|||||||
tool_executed = False
|
tool_executed = False
|
||||||
events.emit("tool_execution", payload={"status": "started", "tool": name, "args": args, "round": r_idx})
|
events.emit("tool_execution", payload={"status": "started", "tool": name, "args": args, "round": r_idx})
|
||||||
|
|
||||||
|
# Check for auto approval mode
|
||||||
|
approval_mode = _tool_approval_modes.get(name, "ask")
|
||||||
|
|
||||||
# Check for run_powershell
|
# Check for run_powershell
|
||||||
if name == TOOL_NAME and pre_tool_callback:
|
if name == TOOL_NAME:
|
||||||
scr = cast(str, args.get("script", ""))
|
scr = cast(str, args.get("script", ""))
|
||||||
_append_comms("OUT", "tool_call", {"name": TOOL_NAME, "id": call_id, "script": scr})
|
_append_comms("OUT", "tool_call", {"name": TOOL_NAME, "id": call_id, "script": scr})
|
||||||
|
if approval_mode == "auto":
|
||||||
|
out = await asyncio.to_thread(_run_script, scr, base_dir, qa_callback, patch_callback)
|
||||||
|
tool_executed = True
|
||||||
|
elif pre_tool_callback:
|
||||||
# pre_tool_callback is synchronous and might block for HITL
|
# pre_tool_callback is synchronous and might block for HITL
|
||||||
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
|
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
|
||||||
if res is None:
|
if res is None:
|
||||||
@@ -636,7 +674,7 @@ async def _execute_single_tool_call_async(
|
|||||||
if not tool_executed:
|
if not tool_executed:
|
||||||
if name and name in mcp_client.TOOL_NAMES:
|
if name and name in mcp_client.TOOL_NAMES:
|
||||||
_append_comms("OUT", "tool_call", {"name": name, "id": call_id, "args": args})
|
_append_comms("OUT", "tool_call", {"name": name, "id": call_id, "args": args})
|
||||||
if name in mcp_client.MUTATING_TOOLS and pre_tool_callback:
|
if name in mcp_client.MUTATING_TOOLS and approval_mode != "auto" and pre_tool_callback:
|
||||||
desc = f"# MCP MUTATING TOOL: {name}\n" + "\n".join(f"# {k}: {repr(v)}" for k, v in args.items())
|
desc = f"# MCP MUTATING TOOL: {name}\n" + "\n".join(f"# {k}: {repr(v)}" for k, v in args.items())
|
||||||
_res = await asyncio.to_thread(pre_tool_callback, desc, base_dir, qa_callback)
|
_res = await asyncio.to_thread(pre_tool_callback, desc, base_dir, qa_callback)
|
||||||
out = "USER REJECTED: tool execution cancelled" if _res is None else await mcp_client.async_dispatch(name, args)
|
out = "USER REJECTED: tool execution cancelled" if _res is None else await mcp_client.async_dispatch(name, args)
|
||||||
@@ -2161,21 +2199,13 @@ def _add_bleed_derived(d: dict[str, Any], sys_tok: int = 0, tool_tok: int = 0) -
|
|||||||
d["tool_tokens"] = tool_tok
|
d["tool_tokens"] = tool_tok
|
||||||
d["history_tokens"] = max(0, cur - sys_tok - tool_tok)
|
d["history_tokens"] = max(0, cur - sys_tok - tool_tok)
|
||||||
return d
|
return d
|
||||||
|
# Check for tool preset in environment variable (headless mode)
|
||||||
def _is_mutating_tool(name: str) -> bool:
|
if os.environ.get("SLOP_TOOL_PRESET"):
|
||||||
"""Returns True if the tool name is considered a mutating tool."""
|
try:
|
||||||
return name in mcp_client.MUTATING_TOOLS or name == TOOL_NAME
|
set_tool_preset(os.environ["SLOP_TOOL_PRESET"])
|
||||||
|
except Exception as _e:
|
||||||
def _confirm_and_run(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Optional[str]:
|
sys.stderr.write(f"[DEBUG] Failed to auto-set tool preset from env: {_e}\n")
|
||||||
"""
|
sys.stderr.flush()
|
||||||
Wrapper for the confirm_and_run_callback.
|
|
||||||
This is what the providers call to trigger HITL approval.
|
|
||||||
"""
|
|
||||||
if confirm_and_run_callback:
|
|
||||||
return confirm_and_run_callback(script, base_dir, qa_callback, patch_callback)
|
|
||||||
# Fallback to direct execution if no callback registered (headless default)
|
|
||||||
from src import shell_runner
|
|
||||||
return shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
|
|
||||||
|
|
||||||
def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
||||||
if _provider == "anthropic":
|
if _provider == "anthropic":
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from src import aggregate
|
|||||||
from src import orchestrator_pm
|
from src import orchestrator_pm
|
||||||
from src import conductor_tech_lead
|
from src import conductor_tech_lead
|
||||||
from src import multi_agent_conductor
|
from src import multi_agent_conductor
|
||||||
|
from src import tool_presets
|
||||||
from src import theme_2 as theme
|
from src import theme_2 as theme
|
||||||
|
|
||||||
def hide_tk_root() -> Tk:
|
def hide_tk_root() -> Tk:
|
||||||
@@ -181,10 +182,10 @@ class AppController:
|
|||||||
"last_latency": 0.0
|
"last_latency": 0.0
|
||||||
}
|
}
|
||||||
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
|
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
|
||||||
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview"},
|
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None},
|
||||||
"Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview"},
|
"Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None},
|
||||||
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite"},
|
"Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite"},
|
"Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
}
|
}
|
||||||
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
|
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
|
||||||
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
||||||
@@ -225,6 +226,7 @@ class AppController:
|
|||||||
self.ui_word_wrap: bool = True
|
self.ui_word_wrap: bool = True
|
||||||
self.ui_summary_only: bool = False
|
self.ui_summary_only: bool = False
|
||||||
self.ui_auto_add_history: bool = False
|
self.ui_auto_add_history: bool = False
|
||||||
|
self.ui_active_tool_preset: str | None = None
|
||||||
self.ui_global_system_prompt: str = ""
|
self.ui_global_system_prompt: str = ""
|
||||||
self.ui_agent_tools: Dict[str, bool] = {}
|
self.ui_agent_tools: Dict[str, bool] = {}
|
||||||
self.available_models: List[str] = []
|
self.available_models: List[str] = []
|
||||||
@@ -305,6 +307,9 @@ class AppController:
|
|||||||
self._editing_preset_top_p: float = 0.0
|
self._editing_preset_top_p: float = 0.0
|
||||||
self._editing_preset_max_output_tokens: int = 4096
|
self._editing_preset_max_output_tokens: int = 4096
|
||||||
self._editing_preset_scope: str = "project"
|
self._editing_preset_scope: str = "project"
|
||||||
|
self._editing_tool_preset_name: str = ""
|
||||||
|
self._editing_tool_preset_categories: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._editing_tool_preset_scope: str = "project"
|
||||||
self.diagnostic_log: List[Dict[str, Any]] = []
|
self.diagnostic_log: List[Dict[str, Any]] = []
|
||||||
self._settable_fields: Dict[str, str] = {
|
self._settable_fields: Dict[str, str] = {
|
||||||
'ai_input': 'ui_ai_input',
|
'ai_input': 'ui_ai_input',
|
||||||
@@ -334,6 +339,7 @@ class AppController:
|
|||||||
'project_system_prompt': 'ui_project_system_prompt',
|
'project_system_prompt': 'ui_project_system_prompt',
|
||||||
'global_preset_name': 'ui_global_preset_name',
|
'global_preset_name': 'ui_global_preset_name',
|
||||||
'project_preset_name': 'ui_project_preset_name',
|
'project_preset_name': 'ui_project_preset_name',
|
||||||
|
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||||
'temperature': 'temperature',
|
'temperature': 'temperature',
|
||||||
'max_tokens': 'max_tokens',
|
'max_tokens': 'max_tokens',
|
||||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||||
@@ -343,6 +349,9 @@ class AppController:
|
|||||||
'_editing_preset_top_p': '_editing_preset_top_p',
|
'_editing_preset_top_p': '_editing_preset_top_p',
|
||||||
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
|
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
|
||||||
'_editing_preset_scope': '_editing_preset_scope',
|
'_editing_preset_scope': '_editing_preset_scope',
|
||||||
|
'_editing_tool_preset_name': '_editing_tool_preset_name',
|
||||||
|
'_editing_tool_preset_categories': '_editing_tool_preset_categories',
|
||||||
|
'_editing_tool_preset_scope': '_editing_tool_preset_scope',
|
||||||
'show_windows': 'show_windows',
|
'show_windows': 'show_windows',
|
||||||
'ui_separate_task_dag': 'ui_separate_task_dag',
|
'ui_separate_task_dag': 'ui_separate_task_dag',
|
||||||
'ui_separate_usage_analytics': 'ui_separate_usage_analytics',
|
'ui_separate_usage_analytics': 'ui_separate_usage_analytics',
|
||||||
@@ -378,6 +387,7 @@ class AppController:
|
|||||||
'project_system_prompt': 'ui_project_system_prompt',
|
'project_system_prompt': 'ui_project_system_prompt',
|
||||||
'global_preset_name': 'ui_global_preset_name',
|
'global_preset_name': 'ui_global_preset_name',
|
||||||
'project_preset_name': 'ui_project_preset_name',
|
'project_preset_name': 'ui_project_preset_name',
|
||||||
|
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||||
'temperature': 'temperature',
|
'temperature': 'temperature',
|
||||||
'max_tokens': 'max_tokens',
|
'max_tokens': 'max_tokens',
|
||||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||||
@@ -471,6 +481,8 @@ class AppController:
|
|||||||
'_apply_preset': self._apply_preset,
|
'_apply_preset': self._apply_preset,
|
||||||
'_cb_save_preset': self._cb_save_preset,
|
'_cb_save_preset': self._cb_save_preset,
|
||||||
'_cb_delete_preset': self._cb_delete_preset,
|
'_cb_delete_preset': self._cb_delete_preset,
|
||||||
|
'_cb_save_tool_preset': self._cb_save_tool_preset,
|
||||||
|
'_cb_delete_tool_preset': self._cb_delete_tool_preset,
|
||||||
'_switch_project': self._switch_project,
|
'_switch_project': self._switch_project,
|
||||||
'_refresh_from_project': self._refresh_from_project
|
'_refresh_from_project': self._refresh_from_project
|
||||||
}
|
}
|
||||||
@@ -844,6 +856,10 @@ class AppController:
|
|||||||
|
|
||||||
self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||||
self.presets = self.preset_manager.load_all()
|
self.presets = self.preset_manager.load_all()
|
||||||
|
self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
|
||||||
|
self.tool_presets = self.tool_preset_manager.load_all()
|
||||||
|
self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET')
|
||||||
|
ai_client.set_tool_preset(self.ui_active_tool_preset)
|
||||||
self.ui_global_preset_name = ai_cfg.get("active_preset")
|
self.ui_global_preset_name = ai_cfg.get("active_preset")
|
||||||
self.ui_project_preset_name = proj_meta.get("active_preset")
|
self.ui_project_preset_name = proj_meta.get("active_preset")
|
||||||
|
|
||||||
@@ -1769,6 +1785,12 @@ class AppController:
|
|||||||
# Restore MMA state
|
# Restore MMA state
|
||||||
mma_sec = proj.get("mma", {})
|
mma_sec = proj.get("mma", {})
|
||||||
self.ui_epic_input = mma_sec.get("epic", "")
|
self.ui_epic_input = mma_sec.get("epic", "")
|
||||||
|
tier_models = mma_sec.get("tier_models", {})
|
||||||
|
for tier, data in tier_models.items():
|
||||||
|
if tier in self.mma_tier_usage:
|
||||||
|
self.mma_tier_usage[tier]["model"] = data.get("model", self.mma_tier_usage[tier]["model"])
|
||||||
|
self.mma_tier_usage[tier]["provider"] = data.get("provider", self.mma_tier_usage[tier]["provider"])
|
||||||
|
self.mma_tier_usage[tier]["tool_preset"] = data.get("tool_preset", self.mma_tier_usage[tier].get("tool_preset"))
|
||||||
at_data = mma_sec.get("active_track")
|
at_data = mma_sec.get("active_track")
|
||||||
if at_data:
|
if at_data:
|
||||||
try:
|
try:
|
||||||
@@ -1796,6 +1818,8 @@ class AppController:
|
|||||||
|
|
||||||
self.preset_manager.project_root = Path(self.ui_files_base_dir)
|
self.preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||||
self.presets = self.preset_manager.load_all()
|
self.presets = self.preset_manager.load_all()
|
||||||
|
self.tool_preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||||
|
self.tool_presets = self.tool_preset_manager.load_all()
|
||||||
|
|
||||||
def _apply_preset(self, name: str, scope: str) -> None:
|
def _apply_preset(self, name: str, scope: str) -> None:
|
||||||
if name == "None":
|
if name == "None":
|
||||||
@@ -1835,6 +1859,15 @@ class AppController:
|
|||||||
self.preset_manager.delete_preset(name, scope)
|
self.preset_manager.delete_preset(name, scope)
|
||||||
self.presets = self.preset_manager.load_all()
|
self.presets = self.preset_manager.load_all()
|
||||||
|
|
||||||
|
def _cb_save_tool_preset(self, name, categories, scope):
|
||||||
|
preset = models.ToolPreset(name=name, categories=categories)
|
||||||
|
self.tool_preset_manager.save_preset(preset, scope)
|
||||||
|
self.tool_presets = self.tool_preset_manager.load_all()
|
||||||
|
|
||||||
|
def _cb_delete_tool_preset(self, name, scope):
|
||||||
|
self.tool_preset_manager.delete_preset(name, scope)
|
||||||
|
self.tool_presets = self.tool_preset_manager.load_all()
|
||||||
|
|
||||||
def _cb_load_track(self, track_id: str) -> None:
|
def _cb_load_track(self, track_id: str) -> None:
|
||||||
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
||||||
if state:
|
if state:
|
||||||
@@ -2178,7 +2211,7 @@ class AppController:
|
|||||||
# Save MMA State
|
# Save MMA State
|
||||||
mma_sec = proj.setdefault("mma", {})
|
mma_sec = proj.setdefault("mma", {})
|
||||||
mma_sec["epic"] = self.ui_epic_input
|
mma_sec["epic"] = self.ui_epic_input
|
||||||
mma_sec["tier_models"] = {t: {"model": d["model"], "provider": d.get("provider", "gemini")} for t, d in self.mma_tier_usage.items()}
|
mma_sec["tier_models"] = {t: {"model": d["model"], "provider": d.get("provider", "gemini"), "tool_preset": d.get("tool_preset")} for t, d in self.mma_tier_usage.items()}
|
||||||
if self.active_track:
|
if self.active_track:
|
||||||
mma_sec["active_track"] = asdict(self.active_track)
|
mma_sec["active_track"] = asdict(self.active_track)
|
||||||
else:
|
else:
|
||||||
|
|||||||
196
src/gui_2.py
196
src/gui_2.py
@@ -6,6 +6,7 @@ import math
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import filedialog, Tk
|
from tkinter import filedialog, Tk
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
@@ -96,6 +97,12 @@ class App:
|
|||||||
self.show_windows.setdefault("Diagnostics", False)
|
self.show_windows.setdefault("Diagnostics", False)
|
||||||
self.controller.start_services(self)
|
self.controller.start_services(self)
|
||||||
self.show_preset_manager_modal = False
|
self.show_preset_manager_modal = False
|
||||||
|
self.show_tool_preset_manager_modal = False
|
||||||
|
self.ui_active_tool_preset = ""
|
||||||
|
self._editing_tool_preset_name = ''
|
||||||
|
self._editing_tool_preset_categories = {}
|
||||||
|
self._editing_tool_preset_scope = 'project'
|
||||||
|
self._selected_tool_preset_idx = -1
|
||||||
self._editing_preset_name = ""
|
self._editing_preset_name = ""
|
||||||
self._editing_preset_content = ""
|
self._editing_preset_content = ""
|
||||||
self._editing_preset_temperature = 0.0
|
self._editing_preset_temperature = 0.0
|
||||||
@@ -354,6 +361,7 @@ class App:
|
|||||||
self._render_patch_modal()
|
self._render_patch_modal()
|
||||||
self._render_save_preset_modal()
|
self._render_save_preset_modal()
|
||||||
self._render_preset_manager_modal()
|
self._render_preset_manager_modal()
|
||||||
|
self._render_tool_preset_manager_modal()
|
||||||
# Auto-save (every 60s)
|
# Auto-save (every 60s)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_autosave >= self._autosave_interval:
|
if now - self._last_autosave >= self._autosave_interval:
|
||||||
@@ -422,6 +430,7 @@ class App:
|
|||||||
self._render_provider_panel()
|
self._render_provider_panel()
|
||||||
if imgui.collapsing_header("System Prompts"):
|
if imgui.collapsing_header("System Prompts"):
|
||||||
self._render_system_prompts_panel()
|
self._render_system_prompts_panel()
|
||||||
|
self._render_agent_tools_panel()
|
||||||
self._render_cache_panel()
|
self._render_cache_panel()
|
||||||
|
|
||||||
imgui.end()
|
imgui.end()
|
||||||
@@ -976,6 +985,118 @@ class App:
|
|||||||
finally:
|
finally:
|
||||||
imgui.end_popup()
|
imgui.end_popup()
|
||||||
|
|
||||||
|
def _render_tool_preset_manager_modal(self) -> None:
|
||||||
|
if not self.show_tool_preset_manager_modal: return
|
||||||
|
imgui.open_popup("Tool Preset Manager")
|
||||||
|
opened, self.show_tool_preset_manager_modal = imgui.begin_popup_modal("Tool Preset Manager", self.show_tool_preset_manager_modal)
|
||||||
|
if opened:
|
||||||
|
try:
|
||||||
|
avail = imgui.get_content_region_avail()
|
||||||
|
# Left Column: Listbox
|
||||||
|
imgui.begin_child("tool_preset_list_area", imgui.ImVec2(250, avail.y), True)
|
||||||
|
try:
|
||||||
|
if imgui.button("New Tool Preset", imgui.ImVec2(-1, 0)):
|
||||||
|
self._editing_tool_preset_name = ""
|
||||||
|
self._editing_tool_preset_categories = {}
|
||||||
|
self._editing_tool_preset_scope = "project"
|
||||||
|
self._selected_tool_preset_idx = -1
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
imgui.set_tooltip("Create a new tool preset configuration.")
|
||||||
|
|
||||||
|
imgui.separator()
|
||||||
|
preset_names = sorted(self.controller.tool_presets.keys())
|
||||||
|
for i, name in enumerate(preset_names):
|
||||||
|
is_selected = (self._selected_tool_preset_idx == i)
|
||||||
|
if imgui.selectable(name, is_selected)[0]:
|
||||||
|
self._selected_tool_preset_idx = i
|
||||||
|
self._editing_tool_preset_name = name
|
||||||
|
preset = self.controller.tool_presets[name]
|
||||||
|
self._editing_tool_preset_categories = copy.deepcopy(preset.categories)
|
||||||
|
finally:
|
||||||
|
imgui.end_child()
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
|
||||||
|
# Right Column: Edit Area
|
||||||
|
imgui.begin_child("tool_preset_edit_area", imgui.ImVec2(0, avail.y), False)
|
||||||
|
try:
|
||||||
|
p_name = self._editing_tool_preset_name or "(New Tool Preset)"
|
||||||
|
imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}")
|
||||||
|
imgui.separator()
|
||||||
|
imgui.dummy(imgui.ImVec2(0, 8))
|
||||||
|
|
||||||
|
imgui.text("Name:")
|
||||||
|
_, self._editing_tool_preset_name = imgui.input_text("##edit_tp_name", self._editing_tool_preset_name)
|
||||||
|
imgui.dummy(imgui.ImVec2(0, 8))
|
||||||
|
|
||||||
|
imgui.text("Scope:")
|
||||||
|
if imgui.radio_button("Global", self._editing_tool_preset_scope == "global"):
|
||||||
|
self._editing_tool_preset_scope = "global"
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.radio_button("Project", self._editing_tool_preset_scope == "project"):
|
||||||
|
self._editing_tool_preset_scope = "project"
|
||||||
|
imgui.dummy(imgui.ImVec2(0, 8))
|
||||||
|
|
||||||
|
imgui.text("Categories & Tools:")
|
||||||
|
imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, -40), True)
|
||||||
|
try:
|
||||||
|
for cat_name, tools in self._editing_tool_preset_categories.items():
|
||||||
|
if imgui.tree_node(cat_name):
|
||||||
|
for tool_name, config in tools.items():
|
||||||
|
# config can be a string ("auto", "ask") or a dict {"mode": "auto"}
|
||||||
|
if isinstance(config, dict):
|
||||||
|
mode = config.get("mode", "auto")
|
||||||
|
else:
|
||||||
|
mode = str(config)
|
||||||
|
|
||||||
|
if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"):
|
||||||
|
if isinstance(config, dict): config["mode"] = "auto"
|
||||||
|
else: tools[tool_name] = "auto"
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"):
|
||||||
|
if isinstance(config, dict): config["mode"] = "ask"
|
||||||
|
else: tools[tool_name] = "ask"
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text(tool_name)
|
||||||
|
imgui.tree_pop()
|
||||||
|
finally:
|
||||||
|
imgui.end_child()
|
||||||
|
|
||||||
|
imgui.dummy(imgui.ImVec2(0, 8))
|
||||||
|
if imgui.button("Save", imgui.ImVec2(100, 0)):
|
||||||
|
if self._editing_tool_preset_name.strip():
|
||||||
|
self.controller._cb_save_tool_preset(
|
||||||
|
self._editing_tool_preset_name.strip(),
|
||||||
|
self._editing_tool_preset_categories,
|
||||||
|
self._editing_tool_preset_scope
|
||||||
|
)
|
||||||
|
self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' saved"
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
imgui.set_tooltip("Save the current tool preset configuration.")
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Delete", imgui.ImVec2(100, 0)):
|
||||||
|
if self._editing_tool_preset_name.strip():
|
||||||
|
self.controller._cb_delete_tool_preset(
|
||||||
|
self._editing_tool_preset_name.strip(),
|
||||||
|
self._editing_tool_preset_scope
|
||||||
|
)
|
||||||
|
self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' deleted"
|
||||||
|
self._editing_tool_preset_name = ""
|
||||||
|
self._editing_tool_preset_categories = {}
|
||||||
|
self._selected_tool_preset_idx = -1
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
imgui.set_tooltip("Delete this tool preset permanently.")
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Close", imgui.ImVec2(100, 0)):
|
||||||
|
self.show_tool_preset_manager_modal = False
|
||||||
|
imgui.close_current_popup()
|
||||||
|
finally:
|
||||||
|
imgui.end_child()
|
||||||
|
finally:
|
||||||
|
imgui.end_popup()
|
||||||
|
|
||||||
def _render_projects_panel(self) -> None:
|
def _render_projects_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
||||||
proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem)
|
proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem)
|
||||||
@@ -1058,12 +1179,6 @@ class App:
|
|||||||
ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only)
|
ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only)
|
||||||
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
|
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
|
||||||
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
|
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
|
||||||
if imgui.collapsing_header("Agent Tools"):
|
|
||||||
for t_name in models.AGENT_TOOL_NAMES:
|
|
||||||
val = self.ui_agent_tools.get(t_name, True)
|
|
||||||
ch, val = imgui.checkbox(f"Enable {t_name}", val)
|
|
||||||
if ch:
|
|
||||||
self.ui_agent_tools[t_name] = val
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
||||||
|
|
||||||
def _render_track_proposal_modal(self) -> None:
|
def _render_track_proposal_modal(self) -> None:
|
||||||
@@ -2716,7 +2831,7 @@ def hello():
|
|||||||
imgui.push_id(f"tier_cfg_{tier}")
|
imgui.push_id(f"tier_cfg_{tier}")
|
||||||
|
|
||||||
# Provider selection
|
# Provider selection
|
||||||
imgui.push_item_width(100)
|
imgui.push_item_width(80)
|
||||||
if imgui.begin_combo("##prov", current_provider):
|
if imgui.begin_combo("##prov", current_provider):
|
||||||
for p in PROVIDERS:
|
for p in PROVIDERS:
|
||||||
if imgui.selectable(p, p == current_provider)[0]:
|
if imgui.selectable(p, p == current_provider)[0]:
|
||||||
@@ -2731,7 +2846,7 @@ def hello():
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
|
|
||||||
# Model selection
|
# Model selection
|
||||||
imgui.push_item_width(-1)
|
imgui.push_item_width(150)
|
||||||
models_list = self.controller.all_available_models.get(current_provider, [])
|
models_list = self.controller.all_available_models.get(current_provider, [])
|
||||||
if imgui.begin_combo("##model", current_model):
|
if imgui.begin_combo("##model", current_model):
|
||||||
for model in models_list:
|
for model in models_list:
|
||||||
@@ -2740,6 +2855,19 @@ def hello():
|
|||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
imgui.pop_item_width()
|
imgui.pop_item_width()
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
|
||||||
|
# Tool Preset selection
|
||||||
|
imgui.push_item_width(-1)
|
||||||
|
current_preset = self.mma_tier_usage[tier].get("tool_preset") or "None"
|
||||||
|
preset_names = ["None"] + sorted(self.controller.tool_presets.keys())
|
||||||
|
if imgui.begin_combo("##preset", current_preset):
|
||||||
|
for preset_name in preset_names:
|
||||||
|
if imgui.selectable(preset_name, current_preset == preset_name)[0]:
|
||||||
|
self.mma_tier_usage[tier]["tool_preset"] = None if preset_name == "None" else preset_name
|
||||||
|
imgui.end_combo()
|
||||||
|
imgui.pop_item_width()
|
||||||
|
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
self._render_ticket_queue()
|
self._render_ticket_queue()
|
||||||
@@ -3042,6 +3170,58 @@ def hello():
|
|||||||
self.show_preset_manager_modal = True
|
self.show_preset_manager_modal = True
|
||||||
imgui.set_item_tooltip("Open preset management modal")
|
imgui.set_item_tooltip("Open preset management modal")
|
||||||
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
||||||
|
def _render_agent_tools_panel(self) -> None:
|
||||||
|
imgui.text_colored(C_LBL, 'Active Tool Preset')
|
||||||
|
presets = self.controller.tool_presets
|
||||||
|
preset_names = [""] + sorted(list(presets.keys()))
|
||||||
|
|
||||||
|
# Gracefully handle None or missing preset
|
||||||
|
active = getattr(self, "ui_active_tool_preset", "")
|
||||||
|
if active is None: active = ""
|
||||||
|
try:
|
||||||
|
idx = preset_names.index(active)
|
||||||
|
except ValueError:
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names)
|
||||||
|
if ch:
|
||||||
|
self.ui_active_tool_preset = preset_names[new_idx]
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Manage Presets##tools"):
|
||||||
|
self.show_tool_preset_manager_modal = True
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
imgui.set_tooltip("Configure tool availability and default modes.")
|
||||||
|
|
||||||
|
imgui.dummy(imgui.ImVec2(0, 8))
|
||||||
|
active_name = self.ui_active_tool_preset
|
||||||
|
if active_name and active_name in presets:
|
||||||
|
preset = presets[active_name]
|
||||||
|
for cat_name, tools in preset.categories.items():
|
||||||
|
if imgui.tree_node(cat_name):
|
||||||
|
for t_name, t_cfg in tools.items():
|
||||||
|
imgui.text(t_name)
|
||||||
|
imgui.same_line(150)
|
||||||
|
|
||||||
|
# Determine current mode
|
||||||
|
if isinstance(t_cfg, dict):
|
||||||
|
mode = t_cfg.get("mode", "auto")
|
||||||
|
else:
|
||||||
|
mode = str(t_cfg)
|
||||||
|
|
||||||
|
if imgui.radio_button(f"Auto##{cat_name}_{t_name}", mode == "auto"):
|
||||||
|
if isinstance(t_cfg, dict):
|
||||||
|
t_cfg["mode"] = "auto"
|
||||||
|
else:
|
||||||
|
preset.categories[cat_name][t_name] = "auto"
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.radio_button(f"Ask##{cat_name}_{t_name}", mode == "ask"):
|
||||||
|
if isinstance(t_cfg, dict):
|
||||||
|
t_cfg["mode"] = "ask"
|
||||||
|
else:
|
||||||
|
preset.categories[cat_name][t_name] = "ask"
|
||||||
|
imgui.tree_pop()
|
||||||
|
|
||||||
def _render_theme_panel(self) -> None:
|
def _render_theme_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
|
||||||
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ class Track:
|
|||||||
class WorkerContext:
|
class WorkerContext:
|
||||||
ticket_id: str
|
ticket_id: str
|
||||||
model_name: str
|
model_name: str
|
||||||
|
tool_preset: Optional[str] = None
|
||||||
messages: List[Dict[str, Any]] = field(default_factory=list)
|
messages: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@@ -345,3 +346,20 @@ class Preset:
|
|||||||
top_p=data.get("top_p"),
|
top_p=data.get("top_p"),
|
||||||
max_output_tokens=data.get("max_output_tokens"),
|
max_output_tokens=data.get("max_output_tokens"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolPreset:
|
||||||
|
name: str
|
||||||
|
categories: Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"categories": self.categories,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, name: str, data: Dict[str, Any]) -> "ToolPreset":
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
categories=data.get("categories", {}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ class ConductorEngine:
|
|||||||
self.track = track
|
self.track = track
|
||||||
self.event_queue = event_queue
|
self.event_queue = event_queue
|
||||||
self.tier_usage = {
|
self.tier_usage = {
|
||||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview", "tool_preset": None},
|
||||||
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
|
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview", "tool_preset": None},
|
||||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite", "tool_preset": None},
|
||||||
}
|
}
|
||||||
self.dag = TrackDAG(self.track.tickets)
|
self.dag = TrackDAG(self.track.tickets)
|
||||||
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
|
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
|
||||||
@@ -294,7 +294,8 @@ class ConductorEngine:
|
|||||||
context = WorkerContext(
|
context = WorkerContext(
|
||||||
ticket_id=ticket.id,
|
ticket_id=ticket.id,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
messages=[]
|
messages=[],
|
||||||
|
tool_preset=self.tier_usage["Tier 3"]["tool_preset"]
|
||||||
)
|
)
|
||||||
context_files = ticket.context_requirements if ticket.context_requirements else None
|
context_files = ticket.context_requirements if ticket.context_requirements else None
|
||||||
|
|
||||||
@@ -407,6 +408,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
|||||||
# Enforce Context Amnesia: each ticket starts with a clean slate.
|
# Enforce Context Amnesia: each ticket starts with a clean slate.
|
||||||
ai_client.reset_session()
|
ai_client.reset_session()
|
||||||
ai_client.set_provider(ai_client.get_provider(), context.model_name)
|
ai_client.set_provider(ai_client.get_provider(), context.model_name)
|
||||||
|
ai_client.set_tool_preset(context.tool_preset)
|
||||||
|
|
||||||
# Check for abort BEFORE any major work
|
# Check for abort BEFORE any major work
|
||||||
if engine and hasattr(engine, "_abort_events"):
|
if engine and hasattr(engine, "_abort_events"):
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ def get_global_presets_path() -> Path:
|
|||||||
def get_project_presets_path(project_root: Path) -> Path:
|
def get_project_presets_path(project_root: Path) -> Path:
|
||||||
return project_root / "project_presets.toml"
|
return project_root / "project_presets.toml"
|
||||||
|
|
||||||
|
def get_global_tool_presets_path() -> Path:
|
||||||
|
root_dir = Path(__file__).resolve().parent.parent
|
||||||
|
return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml"))
|
||||||
|
|
||||||
|
def get_project_tool_presets_path(project_root: Path) -> Path:
|
||||||
|
return project_root / "project_tool_presets.toml"
|
||||||
|
|
||||||
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
||||||
if env_var in os.environ:
|
if env_var in os.environ:
|
||||||
return Path(os.environ[env_var])
|
return Path(os.environ[env_var])
|
||||||
|
|||||||
91
src/tool_presets.py
Normal file
91
src/tool_presets.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import tomllib
|
||||||
|
import tomli_w
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
from src import paths
|
||||||
|
from src.models import ToolPreset
|
||||||
|
|
||||||
|
class ToolPresetManager:
|
||||||
|
def __init__(self, project_root: Optional[Union[str, Path]] = None):
|
||||||
|
self.project_root = Path(project_root) if project_root else None
|
||||||
|
|
||||||
|
def _load_from_path(self, path: Path) -> Dict[str, ToolPreset]:
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
presets = {}
|
||||||
|
for name, config in data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
presets[name] = ToolPreset.from_dict(name, config)
|
||||||
|
return presets
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def load_all(self) -> Dict[str, ToolPreset]:
|
||||||
|
"""
|
||||||
|
Merges global and project presets.
|
||||||
|
Project presets override global ones if they have the same name.
|
||||||
|
"""
|
||||||
|
presets = self._load_from_path(paths.get_global_tool_presets_path())
|
||||||
|
if self.project_root:
|
||||||
|
project_presets = self._load_from_path(paths.get_project_tool_presets_path(self.project_root))
|
||||||
|
presets.update(project_presets)
|
||||||
|
return presets
|
||||||
|
|
||||||
|
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
Saves a preset to either 'global' or 'project' scope.
|
||||||
|
Scope must be 'global' or 'project'.
|
||||||
|
"""
|
||||||
|
if scope == "global":
|
||||||
|
path = paths.get_global_tool_presets_path()
|
||||||
|
elif scope == "project":
|
||||||
|
if not self.project_root:
|
||||||
|
raise ValueError("Project root not set for project scope saving.")
|
||||||
|
path = paths.get_project_tool_presets_path(self.project_root)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid scope: {scope}")
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data[preset.name] = preset.to_dict()
|
||||||
|
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||||
|
"""
|
||||||
|
Deletes a preset from the specified scope.
|
||||||
|
Scope must be 'global' or 'project'.
|
||||||
|
"""
|
||||||
|
if scope == "global":
|
||||||
|
path = paths.get_global_tool_presets_path()
|
||||||
|
elif scope == "project":
|
||||||
|
if not self.project_root:
|
||||||
|
raise ValueError("Project root not set for project scope deletion.")
|
||||||
|
path = paths.get_project_tool_presets_path(self.project_root)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid scope: {scope}")
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if name in data:
|
||||||
|
del data[name]
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
tomli_w.dump(data, f)
|
||||||
6
test_task.toml
Normal file
6
test_task.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
role = "tier4"
|
||||||
|
prompt = "Just a test"
|
||||||
|
tool_preset = "CustomPreset"
|
||||||
|
debug = true
|
||||||
|
failure_count = 1
|
||||||
|
docs = []
|
||||||
@@ -250,6 +250,7 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
|||||||
if mcp_file.exists():
|
if mcp_file.exists():
|
||||||
env["SLOP_MCP_ENV"] = str(mcp_file.absolute())
|
env["SLOP_MCP_ENV"] = str(mcp_file.absolute())
|
||||||
env["SLOP_GLOBAL_PRESETS"] = str((temp_workspace / "presets.toml").absolute())
|
env["SLOP_GLOBAL_PRESETS"] = str((temp_workspace / "presets.toml").absolute())
|
||||||
|
env["SLOP_GLOBAL_TOOL_PRESETS"] = str((temp_workspace / "tool_presets.toml").absolute())
|
||||||
|
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
|
||||||
|
|||||||
37
tests/test_tool_preset_env.py
Normal file
37
tests/test_tool_preset_env.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from src import ai_client
|
||||||
|
|
||||||
|
def test_tool_preset_env_loading(monkeypatch):
|
||||||
|
"""Tests that SLOP_TOOL_PRESET env var triggers set_tool_preset."""
|
||||||
|
|
||||||
|
# We need to reload or re-evaluate the logic at the end of ai_client
|
||||||
|
# Since it runs on import, we can simulate the environment and re-run the check.
|
||||||
|
|
||||||
|
with patch("src.ai_client.set_tool_preset") as mock_set_preset:
|
||||||
|
monkeypatch.setenv("SLOP_TOOL_PRESET", "TestPreset")
|
||||||
|
|
||||||
|
# Manually trigger the logic that was added to the end of ai_client.py
|
||||||
|
if os.environ.get("SLOP_TOOL_PRESET"):
|
||||||
|
try:
|
||||||
|
ai_client.set_tool_preset(os.environ["SLOP_TOOL_PRESET"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_set_preset.assert_called_once_with("TestPreset")
|
||||||
|
|
||||||
|
def test_tool_preset_env_no_var(monkeypatch):
|
||||||
|
"""Tests that nothing happens if SLOP_TOOL_PRESET is not set."""
|
||||||
|
|
||||||
|
with patch("src.ai_client.set_tool_preset") as mock_set_preset:
|
||||||
|
monkeypatch.delenv("SLOP_TOOL_PRESET", raising=False)
|
||||||
|
|
||||||
|
# Manually trigger the logic
|
||||||
|
if os.environ.get("SLOP_TOOL_PRESET"):
|
||||||
|
try:
|
||||||
|
ai_client.set_tool_preset(os.environ["SLOP_TOOL_PRESET"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_set_preset.assert_not_called()
|
||||||
140
tests/test_tool_preset_manager.py
Normal file
140
tests/test_tool_preset_manager.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import pytest
|
||||||
|
import tomli_w
|
||||||
|
from pathlib import Path
|
||||||
|
from src.tool_presets import ToolPresetManager
|
||||||
|
from src.models import ToolPreset
|
||||||
|
from src import paths
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_paths(tmp_path, monkeypatch):
|
||||||
|
global_dir = tmp_path / "global"
|
||||||
|
global_dir.mkdir()
|
||||||
|
project_dir = tmp_path / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
|
||||||
|
global_presets = global_dir / "tool_presets.toml"
|
||||||
|
project_presets = project_dir / "project_tool_presets.toml"
|
||||||
|
|
||||||
|
monkeypatch.setattr(paths, "get_global_tool_presets_path", lambda: global_presets)
|
||||||
|
monkeypatch.setattr(paths, "get_project_tool_presets_path", lambda _: project_presets)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"global_dir": global_dir,
|
||||||
|
"project_dir": project_dir,
|
||||||
|
"global_presets": global_presets,
|
||||||
|
"project_presets": project_presets
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_load_all_merged(temp_paths):
|
||||||
|
# Setup global presets
|
||||||
|
global_data = {
|
||||||
|
"default": {
|
||||||
|
"categories": {
|
||||||
|
"file": {"read": True},
|
||||||
|
"shell": {"run": False}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"global_only": {
|
||||||
|
"categories": {"web": {"search": True}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(temp_paths["global_presets"], "wb") as f:
|
||||||
|
tomli_w.dump(global_data, f)
|
||||||
|
|
||||||
|
# Setup project presets (overrides 'default')
|
||||||
|
project_data = {
|
||||||
|
"default": {
|
||||||
|
"categories": {
|
||||||
|
"file": {"read": True},
|
||||||
|
"shell": {"run": True} # Override
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project_only": {
|
||||||
|
"categories": {"git": {"commit": True}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(temp_paths["project_presets"], "wb") as f:
|
||||||
|
tomli_w.dump(project_data, f)
|
||||||
|
|
||||||
|
manager = ToolPresetManager(project_root=temp_paths["project_dir"])
|
||||||
|
all_presets = manager.load_all()
|
||||||
|
|
||||||
|
assert "default" in all_presets
|
||||||
|
assert all_presets["default"].categories["shell"]["run"] is True # Overridden
|
||||||
|
assert "global_only" in all_presets
|
||||||
|
assert "project_only" in all_presets
|
||||||
|
assert all_presets["global_only"].categories["web"]["search"] is True
|
||||||
|
assert all_presets["project_only"].categories["git"]["commit"] is True
|
||||||
|
|
||||||
|
def test_save_preset_global(temp_paths):
|
||||||
|
manager = ToolPresetManager()
|
||||||
|
preset = ToolPreset(name="new_global", categories={"test": {"ok": True}})
|
||||||
|
|
||||||
|
manager.save_preset(preset, scope="global")
|
||||||
|
|
||||||
|
assert temp_paths["global_presets"].exists()
|
||||||
|
loaded = manager._load_from_path(temp_paths["global_presets"])
|
||||||
|
assert "new_global" in loaded
|
||||||
|
assert loaded["new_global"].categories == {"test": {"ok": True}}
|
||||||
|
|
||||||
|
def test_save_preset_project(temp_paths):
|
||||||
|
manager = ToolPresetManager(project_root=temp_paths["project_dir"])
|
||||||
|
preset = ToolPreset(name="new_project", categories={"test": {"ok": False}})
|
||||||
|
|
||||||
|
manager.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
assert temp_paths["project_presets"].exists()
|
||||||
|
loaded = manager._load_from_path(temp_paths["project_presets"])
|
||||||
|
assert "new_project" in loaded
|
||||||
|
assert loaded["new_project"].categories == {"test": {"ok": False}}
|
||||||
|
|
||||||
|
def test_delete_preset_global(temp_paths):
|
||||||
|
# Initial global setup
|
||||||
|
global_data = {
|
||||||
|
"to_delete": {"categories": {}},
|
||||||
|
"keep": {"categories": {}}
|
||||||
|
}
|
||||||
|
with open(temp_paths["global_presets"], "wb") as f:
|
||||||
|
tomli_w.dump(global_data, f)
|
||||||
|
|
||||||
|
manager = ToolPresetManager()
|
||||||
|
manager.delete_preset("to_delete", scope="global")
|
||||||
|
|
||||||
|
loaded = manager._load_from_path(temp_paths["global_presets"])
|
||||||
|
assert "to_delete" not in loaded
|
||||||
|
assert "keep" in loaded
|
||||||
|
|
||||||
|
def test_delete_preset_project(temp_paths):
|
||||||
|
# Initial project setup
|
||||||
|
project_data = {
|
||||||
|
"to_delete": {"categories": {}},
|
||||||
|
"keep": {"categories": {}}
|
||||||
|
}
|
||||||
|
with open(temp_paths["project_presets"], "wb") as f:
|
||||||
|
tomli_w.dump(project_data, f)
|
||||||
|
|
||||||
|
manager = ToolPresetManager(project_root=temp_paths["project_dir"])
|
||||||
|
manager.delete_preset("to_delete", scope="project")
|
||||||
|
|
||||||
|
loaded = manager._load_from_path(temp_paths["project_presets"])
|
||||||
|
assert "to_delete" not in loaded
|
||||||
|
assert "keep" in loaded
|
||||||
|
|
||||||
|
def test_save_project_no_root_raises(temp_paths):
|
||||||
|
manager = ToolPresetManager(project_root=None)
|
||||||
|
preset = ToolPreset(name="fail", categories={})
|
||||||
|
with pytest.raises(ValueError, match="Project root not set"):
|
||||||
|
manager.save_preset(preset, scope="project")
|
||||||
|
|
||||||
|
def test_delete_project_no_root_raises(temp_paths):
|
||||||
|
manager = ToolPresetManager(project_root=None)
|
||||||
|
with pytest.raises(ValueError, match="Project root not set"):
|
||||||
|
manager.delete_preset("any", scope="project")
|
||||||
|
|
||||||
|
def test_invalid_scope_raises(temp_paths):
|
||||||
|
manager = ToolPresetManager()
|
||||||
|
preset = ToolPreset(name="fail", categories={})
|
||||||
|
with pytest.raises(ValueError, match="Invalid scope"):
|
||||||
|
manager.save_preset(preset, scope="invalid")
|
||||||
|
with pytest.raises(ValueError, match="Invalid scope"):
|
||||||
|
manager.delete_preset("any", scope="invalid")
|
||||||
88
tests/test_tool_presets_execution.py
Normal file
88
tests/test_tool_presets_execution.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
from src import ai_client
|
||||||
|
from src import mcp_client
|
||||||
|
from src import models
|
||||||
|
from src.models import ToolPreset
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_auto_approval():
|
||||||
|
# Setup a preset with read_file as auto
|
||||||
|
preset = ToolPreset(name="AutoTest", categories={
|
||||||
|
"General": {"read_file": "auto"}
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch("src.tool_presets.ToolPresetManager.load_all", return_value={"AutoTest": preset}):
|
||||||
|
ai_client.set_tool_preset("AutoTest")
|
||||||
|
|
||||||
|
# Mock mcp_client.async_dispatch to avoid actual file reads
|
||||||
|
with patch("src.mcp_client.async_dispatch", return_value="File Content") as mock_dispatch:
|
||||||
|
# pre_tool_callback should NOT be called
|
||||||
|
mock_cb = MagicMock()
|
||||||
|
|
||||||
|
name, call_id, out, _ = await ai_client._execute_single_tool_call_async(
|
||||||
|
name="read_file",
|
||||||
|
args={"path": "test.txt"},
|
||||||
|
call_id="call_1",
|
||||||
|
base_dir=".",
|
||||||
|
pre_tool_callback=mock_cb,
|
||||||
|
qa_callback=None,
|
||||||
|
r_idx=0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert out == "File Content"
|
||||||
|
mock_cb.assert_not_called()
|
||||||
|
mock_dispatch.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_ask_approval():
|
||||||
|
# Setup a preset with run_powershell as ask
|
||||||
|
preset = ToolPreset(name="AskTest", categories={
|
||||||
|
"General": {"run_powershell": "ask"}
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch("src.tool_presets.ToolPresetManager.load_all", return_value={"AskTest": preset}):
|
||||||
|
ai_client.set_tool_preset("AskTest")
|
||||||
|
|
||||||
|
# pre_tool_callback SHOULD be called
|
||||||
|
mock_cb = MagicMock(return_value="Success")
|
||||||
|
|
||||||
|
name, call_id, out, _ = await ai_client._execute_single_tool_call_async(
|
||||||
|
name="run_powershell",
|
||||||
|
args={"script": "dir"},
|
||||||
|
call_id="call_2",
|
||||||
|
base_dir=".",
|
||||||
|
pre_tool_callback=mock_cb,
|
||||||
|
qa_callback=None,
|
||||||
|
r_idx=0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert out == "Success"
|
||||||
|
mock_cb.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tool_rejection():
|
||||||
|
# Setup a preset with run_powershell as ask
|
||||||
|
preset = ToolPreset(name="AskTest", categories={
|
||||||
|
"General": {"run_powershell": "ask"}
|
||||||
|
})
|
||||||
|
|
||||||
|
with patch("src.tool_presets.ToolPresetManager.load_all", return_value={"AskTest": preset}):
|
||||||
|
ai_client.set_tool_preset("AskTest")
|
||||||
|
|
||||||
|
# mock_cb returns None (rejected)
|
||||||
|
mock_cb = MagicMock(return_value=None)
|
||||||
|
|
||||||
|
name, call_id, out, _ = await ai_client._execute_single_tool_call_async(
|
||||||
|
name="run_powershell",
|
||||||
|
args={"script": "dir"},
|
||||||
|
call_id="call_3",
|
||||||
|
base_dir=".",
|
||||||
|
pre_tool_callback=mock_cb,
|
||||||
|
qa_callback=None,
|
||||||
|
r_idx=0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "USER REJECTED" in out
|
||||||
|
mock_cb.assert_called_once()
|
||||||
61
tests/test_tool_presets_sim.py
Normal file
61
tests/test_tool_presets_sim.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
import tomli_w
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from src.api_hook_client import ApiHookClient
|
||||||
|
|
||||||
|
def test_tool_preset_switching(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
|
||||||
|
# Paths for tool presets
|
||||||
|
temp_workspace = Path("tests/artifacts/live_gui_workspace")
|
||||||
|
global_tool_presets_path = temp_workspace / "tool_presets.toml"
|
||||||
|
project_tool_presets_path = temp_workspace / "project_tool_presets.toml"
|
||||||
|
manual_slop_path = temp_workspace / "manual_slop.toml"
|
||||||
|
|
||||||
|
# Cleanup before test
|
||||||
|
if global_tool_presets_path.exists(): global_tool_presets_path.unlink()
|
||||||
|
if project_tool_presets_path.exists(): project_tool_presets_path.unlink()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a global tool preset
|
||||||
|
global_tool_presets_path.write_text(tomli_w.dumps({
|
||||||
|
"presets": {
|
||||||
|
"TestGlobalTools": {
|
||||||
|
"categories": {
|
||||||
|
"General": {
|
||||||
|
"read_file": "auto",
|
||||||
|
"run_powershell": "ask"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Trigger reload
|
||||||
|
client.push_event("custom_callback", {
|
||||||
|
"callback": "_refresh_from_project",
|
||||||
|
"args": []
|
||||||
|
})
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Select the tool preset
|
||||||
|
client.set_value("ui_active_tool_preset", "TestGlobalTools")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Verify state
|
||||||
|
state = client.get_gui_state()
|
||||||
|
assert state["ui_active_tool_preset"] == "TestGlobalTools"
|
||||||
|
|
||||||
|
# Test "None" selection
|
||||||
|
client.set_value("ui_active_tool_preset", "")
|
||||||
|
time.sleep(1)
|
||||||
|
state = client.get_gui_state()
|
||||||
|
assert not state.get("ui_active_tool_preset")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if global_tool_presets_path.exists(): global_tool_presets_path.unlink()
|
||||||
|
if project_tool_presets_path.exists(): project_tool_presets_path.unlink()
|
||||||
25
tool_presets.toml
Normal file
25
tool_presets.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[presets.Default]
|
||||||
|
categories.General = [
|
||||||
|
{ name = "run_powershell", approval = "ask" },
|
||||||
|
{ name = "read_file", approval = "auto" },
|
||||||
|
{ name = "list_directory", approval = "auto" },
|
||||||
|
{ name = "search_files", approval = "auto" },
|
||||||
|
{ name = "get_file_summary", approval = "auto" }
|
||||||
|
]
|
||||||
|
categories.Web = [
|
||||||
|
{ name = "web_search", approval = "ask" },
|
||||||
|
{ name = "fetch_url", approval = "ask" }
|
||||||
|
]
|
||||||
|
categories.Python = [
|
||||||
|
{ name = "py_get_skeleton", approval = "auto" },
|
||||||
|
{ name = "py_get_code_outline", approval = "auto" },
|
||||||
|
{ name = "py_get_definition", approval = "auto" },
|
||||||
|
{ name = "py_get_signature", approval = "auto" },
|
||||||
|
{ name = "py_get_class_summary", approval = "auto" },
|
||||||
|
{ name = "py_get_var_declaration", approval = "auto" },
|
||||||
|
{ name = "py_get_docstring", approval = "auto" },
|
||||||
|
{ name = "py_find_usages", approval = "auto" },
|
||||||
|
{ name = "py_get_imports", approval = "auto" },
|
||||||
|
{ name = "py_check_syntax", approval = "auto" },
|
||||||
|
{ name = "py_get_hierarchy", approval = "auto" }
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user