chore(conductor): Mark track 'Saved Tool Presets' as complete
This commit is contained in:
@@ -98,6 +98,8 @@ tool_log_callback: Optional[Callable[[str, str], None]] = None
|
||||
|
||||
_local_storage = threading.local()
|
||||
|
||||
_tool_approval_modes: dict[str, str] = {}
|
||||
|
||||
def get_current_tier() -> Optional[str]:
|
||||
"""Returns the current tier from thread-local storage."""
|
||||
return getattr(_local_storage, "current_tier", None)
|
||||
@@ -468,6 +470,35 @@ def set_agent_tools(tools: dict[str, bool]) -> None:
|
||||
_agent_tools = tools
|
||||
_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]]:
|
||||
mcp_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.MCP_TOOL_SPECS:
|
||||
@@ -621,22 +652,29 @@ async def _execute_single_tool_call_async(
|
||||
tool_executed = False
|
||||
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
|
||||
if name == TOOL_NAME and pre_tool_callback:
|
||||
if name == TOOL_NAME:
|
||||
scr = cast(str, args.get("script", ""))
|
||||
_append_comms("OUT", "tool_call", {"name": TOOL_NAME, "id": call_id, "script": scr})
|
||||
# pre_tool_callback is synchronous and might block for HITL
|
||||
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
|
||||
if res is None:
|
||||
out = "USER REJECTED: tool execution cancelled"
|
||||
else:
|
||||
out = res
|
||||
tool_executed = True
|
||||
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
|
||||
res = await asyncio.to_thread(pre_tool_callback, scr, base_dir, qa_callback)
|
||||
if res is None:
|
||||
out = "USER REJECTED: tool execution cancelled"
|
||||
else:
|
||||
out = res
|
||||
tool_executed = True
|
||||
|
||||
if not tool_executed:
|
||||
if name and name in mcp_client.TOOL_NAMES:
|
||||
_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())
|
||||
_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)
|
||||
@@ -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["history_tokens"] = max(0, cur - sys_tok - tool_tok)
|
||||
return d
|
||||
|
||||
def _is_mutating_tool(name: str) -> bool:
|
||||
"""Returns True if the tool name is considered a mutating tool."""
|
||||
return name in mcp_client.MUTATING_TOOLS or name == TOOL_NAME
|
||||
|
||||
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]:
|
||||
"""
|
||||
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)
|
||||
# Check for tool preset in environment variable (headless mode)
|
||||
if os.environ.get("SLOP_TOOL_PRESET"):
|
||||
try:
|
||||
set_tool_preset(os.environ["SLOP_TOOL_PRESET"])
|
||||
except Exception as _e:
|
||||
sys.stderr.write(f"[DEBUG] Failed to auto-set tool preset from env: {_e}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
||||
if _provider == "anthropic":
|
||||
|
||||
@@ -33,6 +33,7 @@ from src import aggregate
|
||||
from src import orchestrator_pm
|
||||
from src import conductor_tech_lead
|
||||
from src import multi_agent_conductor
|
||||
from src import tool_presets
|
||||
from src import theme_2 as theme
|
||||
|
||||
def hide_tk_root() -> Tk:
|
||||
@@ -181,10 +182,10 @@ class AppController:
|
||||
"last_latency": 0.0
|
||||
}
|
||||
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
|
||||
"Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview"},
|
||||
"Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview"},
|
||||
"Tier 3": {"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"},
|
||||
"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", "tool_preset": None},
|
||||
"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", "tool_preset": None},
|
||||
}
|
||||
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
|
||||
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
||||
@@ -225,6 +226,7 @@ class AppController:
|
||||
self.ui_word_wrap: bool = True
|
||||
self.ui_summary_only: 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_agent_tools: Dict[str, bool] = {}
|
||||
self.available_models: List[str] = []
|
||||
@@ -305,6 +307,9 @@ class AppController:
|
||||
self._editing_preset_top_p: float = 0.0
|
||||
self._editing_preset_max_output_tokens: int = 4096
|
||||
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._settable_fields: Dict[str, str] = {
|
||||
'ai_input': 'ui_ai_input',
|
||||
@@ -334,6 +339,7 @@ class AppController:
|
||||
'project_system_prompt': 'ui_project_system_prompt',
|
||||
'global_preset_name': 'ui_global_preset_name',
|
||||
'project_preset_name': 'ui_project_preset_name',
|
||||
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||
'temperature': 'temperature',
|
||||
'max_tokens': 'max_tokens',
|
||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||
@@ -343,6 +349,9 @@ class AppController:
|
||||
'_editing_preset_top_p': '_editing_preset_top_p',
|
||||
'_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
|
||||
'_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',
|
||||
'ui_separate_task_dag': 'ui_separate_task_dag',
|
||||
'ui_separate_usage_analytics': 'ui_separate_usage_analytics',
|
||||
@@ -378,6 +387,7 @@ class AppController:
|
||||
'project_system_prompt': 'ui_project_system_prompt',
|
||||
'global_preset_name': 'ui_global_preset_name',
|
||||
'project_preset_name': 'ui_project_preset_name',
|
||||
'ui_active_tool_preset': 'ui_active_tool_preset',
|
||||
'temperature': 'temperature',
|
||||
'max_tokens': 'max_tokens',
|
||||
'show_preset_manager_modal': 'show_preset_manager_modal',
|
||||
@@ -471,6 +481,8 @@ class AppController:
|
||||
'_apply_preset': self._apply_preset,
|
||||
'_cb_save_preset': self._cb_save_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,
|
||||
'_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.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_project_preset_name = proj_meta.get("active_preset")
|
||||
|
||||
@@ -1769,6 +1785,12 @@ class AppController:
|
||||
# Restore MMA state
|
||||
mma_sec = proj.get("mma", {})
|
||||
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")
|
||||
if at_data:
|
||||
try:
|
||||
@@ -1796,6 +1818,8 @@ class AppController:
|
||||
|
||||
self.preset_manager.project_root = Path(self.ui_files_base_dir)
|
||||
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:
|
||||
if name == "None":
|
||||
@@ -1835,6 +1859,15 @@ class AppController:
|
||||
self.preset_manager.delete_preset(name, scope)
|
||||
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:
|
||||
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
||||
if state:
|
||||
@@ -2178,7 +2211,7 @@ class AppController:
|
||||
# Save MMA State
|
||||
mma_sec = proj.setdefault("mma", {})
|
||||
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:
|
||||
mma_sec["active_track"] = asdict(self.active_track)
|
||||
else:
|
||||
|
||||
196
src/gui_2.py
196
src/gui_2.py
@@ -6,6 +6,7 @@ import math
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import copy
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Any
|
||||
@@ -96,6 +97,12 @@ class App:
|
||||
self.show_windows.setdefault("Diagnostics", False)
|
||||
self.controller.start_services(self)
|
||||
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_content = ""
|
||||
self._editing_preset_temperature = 0.0
|
||||
@@ -354,6 +361,7 @@ class App:
|
||||
self._render_patch_modal()
|
||||
self._render_save_preset_modal()
|
||||
self._render_preset_manager_modal()
|
||||
self._render_tool_preset_manager_modal()
|
||||
# Auto-save (every 60s)
|
||||
now = time.time()
|
||||
if now - self._last_autosave >= self._autosave_interval:
|
||||
@@ -422,6 +430,7 @@ class App:
|
||||
self._render_provider_panel()
|
||||
if imgui.collapsing_header("System Prompts"):
|
||||
self._render_system_prompts_panel()
|
||||
self._render_agent_tools_panel()
|
||||
self._render_cache_panel()
|
||||
|
||||
imgui.end()
|
||||
@@ -976,6 +985,118 @@ class App:
|
||||
finally:
|
||||
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:
|
||||
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)
|
||||
@@ -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_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)
|
||||
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")
|
||||
|
||||
def _render_track_proposal_modal(self) -> None:
|
||||
@@ -2716,7 +2831,7 @@ def hello():
|
||||
imgui.push_id(f"tier_cfg_{tier}")
|
||||
|
||||
# Provider selection
|
||||
imgui.push_item_width(100)
|
||||
imgui.push_item_width(80)
|
||||
if imgui.begin_combo("##prov", current_provider):
|
||||
for p in PROVIDERS:
|
||||
if imgui.selectable(p, p == current_provider)[0]:
|
||||
@@ -2731,7 +2846,7 @@ def hello():
|
||||
imgui.same_line()
|
||||
|
||||
# Model selection
|
||||
imgui.push_item_width(-1)
|
||||
imgui.push_item_width(150)
|
||||
models_list = self.controller.all_available_models.get(current_provider, [])
|
||||
if imgui.begin_combo("##model", current_model):
|
||||
for model in models_list:
|
||||
@@ -2739,6 +2854,19 @@ def hello():
|
||||
self.mma_tier_usage[tier]["model"] = model
|
||||
imgui.end_combo()
|
||||
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.separator()
|
||||
@@ -3042,6 +3170,58 @@ def hello():
|
||||
self.show_preset_manager_modal = True
|
||||
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))
|
||||
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:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel")
|
||||
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
||||
|
||||
@@ -208,6 +208,7 @@ class Track:
|
||||
class WorkerContext:
|
||||
ticket_id: str
|
||||
model_name: str
|
||||
tool_preset: Optional[str] = None
|
||||
messages: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
@@ -345,3 +346,20 @@ class Preset:
|
||||
top_p=data.get("top_p"),
|
||||
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.event_queue = event_queue
|
||||
self.tier_usage = {
|
||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
|
||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
"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", "tool_preset": None},
|
||||
"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", "tool_preset": None},
|
||||
}
|
||||
self.dag = TrackDAG(self.track.tickets)
|
||||
self.engine = ExecutionEngine(self.dag, auto_queue=auto_queue)
|
||||
@@ -294,7 +294,8 @@ class ConductorEngine:
|
||||
context = WorkerContext(
|
||||
ticket_id=ticket.id,
|
||||
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
|
||||
|
||||
@@ -407,6 +408,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
||||
# Enforce Context Amnesia: each ticket starts with a clean slate.
|
||||
ai_client.reset_session()
|
||||
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
|
||||
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:
|
||||
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:
|
||||
if env_var in os.environ:
|
||||
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)
|
||||
Reference in New Issue
Block a user