Add registry sort/pagination features and improve code step dialog
Registry:
- Add owner and rating sort options to both server endpoints
- Add letter-based prefix filtering (A-Z, #) with server-side validation
- LEFT JOIN tool_stats for average_rating/rating_count in responses
- Add interactive numbered page buttons with sliding window and ellipsis
- Add letter bar UI (shown when sorting by name) with highlight state
- Auto-select asc/desc order based on sort field
- Disable cell editing on results table
- Client: add order and prefix params to list_tools/search_tools
Code step dialog:
- Split AI prompt into user instruction input and collapsible wrapper
- User types plain instruction, wrapper is hidden by default
- Injection of user text into wrapper via {user_instruction} placeholder
- Increase dialog minimum height to 750px
Runner:
- Support variable substitution in prompt step provider name
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0ee21f27f7
commit
b00115a52e
|
|
@ -177,7 +177,7 @@ class CodeStepDialog(QDialog):
|
|||
def __init__(self, parent, step: CodeStep = None, available_vars: list = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Code Step" if step else "Add Code Step")
|
||||
self.setMinimumSize(900, 600)
|
||||
self.setMinimumSize(900, 750)
|
||||
self._step = step
|
||||
self._available_vars = available_vars or ["input"]
|
||||
self._worker = None
|
||||
|
|
@ -254,16 +254,30 @@ class CodeStepDialog(QDialog):
|
|||
provider_layout.addStretch()
|
||||
ai_layout.addLayout(provider_layout)
|
||||
|
||||
# Prompt editor
|
||||
prompt_label = QLabel("Describe what you want the code to do:")
|
||||
ai_layout.addWidget(prompt_label)
|
||||
# User instruction input (always visible)
|
||||
instruction_label = QLabel("Describe what you want the code to do:")
|
||||
ai_layout.addWidget(instruction_label)
|
||||
|
||||
self.ai_prompt_input = QPlainTextEdit()
|
||||
self.ai_prompt_input.setPlaceholderText(
|
||||
self.user_instruction_input = QPlainTextEdit()
|
||||
self.user_instruction_input.setPlaceholderText(
|
||||
"Example: Parse the input as JSON and extract the 'name' field"
|
||||
)
|
||||
# Set default prompt template
|
||||
self.user_instruction_input.setMaximumHeight(100)
|
||||
ai_layout.addWidget(self.user_instruction_input, 1)
|
||||
|
||||
# Collapsible prompt wrapper section
|
||||
self.wrapper_toggle = QPushButton("Edit prompt wrapper ▶")
|
||||
self.wrapper_toggle.setFlat(True)
|
||||
self.wrapper_toggle.setStyleSheet(
|
||||
"QPushButton { color: #4a5568; font-size: 12px; text-align: left; "
|
||||
"padding: 2px 0; } QPushButton:hover { color: #2d3748; }"
|
||||
)
|
||||
self.wrapper_toggle.clicked.connect(self._toggle_wrapper)
|
||||
ai_layout.addWidget(self.wrapper_toggle)
|
||||
|
||||
self.ai_prompt_input = QPlainTextEdit()
|
||||
self._set_default_prompt()
|
||||
self.ai_prompt_input.hide()
|
||||
ai_layout.addWidget(self.ai_prompt_input, 2)
|
||||
|
||||
# Generate button
|
||||
|
|
@ -306,6 +320,15 @@ class CodeStepDialog(QDialog):
|
|||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _toggle_wrapper(self):
|
||||
"""Toggle visibility of the prompt wrapper editor."""
|
||||
if self.ai_prompt_input.isVisible():
|
||||
self.ai_prompt_input.hide()
|
||||
self.wrapper_toggle.setText("Edit prompt wrapper ▶")
|
||||
else:
|
||||
self.ai_prompt_input.show()
|
||||
self.wrapper_toggle.setText("Edit prompt wrapper ▼")
|
||||
|
||||
def _set_default_prompt(self):
|
||||
"""Set the default AI prompt template."""
|
||||
vars_formatted = ', '.join(f'"{{{v}}}"' for v in self._available_vars)
|
||||
|
|
@ -317,7 +340,7 @@ Example:
|
|||
my_var = \"\"\"{{input}}\"\"\"
|
||||
result = my_var.upper()
|
||||
|
||||
INSTRUCTION: [Describe what you want the code to do]
|
||||
INSTRUCTION: {{user_instruction}}
|
||||
|
||||
CURRENT CODE:
|
||||
{{code}}
|
||||
|
|
@ -329,14 +352,20 @@ IMPORTANT: Return ONLY executable inline Python code. No function definitions, n
|
|||
|
||||
def _generate_code(self):
|
||||
"""Generate code using AI."""
|
||||
prompt_template = self.ai_prompt_input.toPlainText().strip()
|
||||
if not prompt_template:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Please enter a prompt</span>")
|
||||
user_instruction = self.user_instruction_input.toPlainText().strip()
|
||||
if not user_instruction:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Please describe what you want the code to do</span>")
|
||||
return
|
||||
|
||||
# Replace {code} placeholder with current code
|
||||
prompt_template = self.ai_prompt_input.toPlainText().strip()
|
||||
if not prompt_template:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Prompt wrapper is empty</span>")
|
||||
return
|
||||
|
||||
# Inject user instruction and current code into the wrapper
|
||||
current_code = self.code_input.toPlainText().strip() or "# No code yet"
|
||||
prompt = prompt_template.replace("{code}", current_code)
|
||||
prompt = prompt_template.replace("{user_instruction}", user_instruction)
|
||||
prompt = prompt.replace("{code}", current_code)
|
||||
|
||||
provider = self.ai_provider_combo.currentText()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,17 +14,24 @@ from ...config import load_config
|
|||
from ...tool import list_tools, load_tool, get_all_categories
|
||||
|
||||
|
||||
# Sort fields that should default to ascending order
|
||||
ASC_SORTS = {"name", "owner"}
|
||||
|
||||
|
||||
class SearchWorker(QThread):
|
||||
"""Background worker for registry search."""
|
||||
finished = Signal(object) # PaginatedResponse
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, query: str = "", category: str = None, sort: str = "downloads",
|
||||
order: str = "desc", prefix: str = None,
|
||||
tags: list = None, page: int = 1, per_page: int = 20):
|
||||
super().__init__()
|
||||
self.query = query
|
||||
self.category = category
|
||||
self.sort = sort
|
||||
self.order = order
|
||||
self.prefix = prefix
|
||||
self.tags = tags
|
||||
self.page = page
|
||||
self.per_page = per_page
|
||||
|
|
@ -40,7 +47,9 @@ class SearchWorker(QThread):
|
|||
category=category,
|
||||
page=self.page,
|
||||
per_page=self.per_page,
|
||||
sort=self.sort
|
||||
sort=self.sort,
|
||||
order=self.order,
|
||||
prefix=self.prefix
|
||||
)
|
||||
else:
|
||||
result = client.search_tools(
|
||||
|
|
@ -50,6 +59,8 @@ class SearchWorker(QThread):
|
|||
page=self.page,
|
||||
per_page=self.per_page,
|
||||
sort=self.sort,
|
||||
order=self.order,
|
||||
prefix=self.prefix,
|
||||
include_facets=True
|
||||
)
|
||||
self.finished.emit(result)
|
||||
|
|
@ -111,6 +122,7 @@ class RegistryPage(QWidget):
|
|||
self._current_page = 1
|
||||
self._total_pages = 1
|
||||
self._current_tags = []
|
||||
self._current_prefix = None # Active letter filter (None = no filter)
|
||||
self._installed_tools = {} # name -> version
|
||||
self._setup_ui()
|
||||
|
||||
|
|
@ -152,8 +164,11 @@ class RegistryPage(QWidget):
|
|||
cat_label = QLabel("Category:")
|
||||
filters_layout.addWidget(cat_label)
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.setEditable(True)
|
||||
self.category_combo.lineEdit().setReadOnly(True)
|
||||
self.category_combo.addItems(["All"] + get_all_categories())
|
||||
self.category_combo.setMinimumWidth(100)
|
||||
self.category_combo.setMinimumHeight(28)
|
||||
self.category_combo.setToolTip("Filter by tool category")
|
||||
self.category_combo.currentTextChanged.connect(self._on_filter_changed)
|
||||
filters_layout.addWidget(self.category_combo)
|
||||
|
|
@ -162,12 +177,17 @@ class RegistryPage(QWidget):
|
|||
sort_label = QLabel("Sort:")
|
||||
filters_layout.addWidget(sort_label)
|
||||
self.sort_combo = QComboBox()
|
||||
self.sort_combo.setEditable(True)
|
||||
self.sort_combo.lineEdit().setReadOnly(True)
|
||||
self.sort_combo.addItem("Most Popular", "downloads")
|
||||
self.sort_combo.addItem("Newest", "published_at")
|
||||
self.sort_combo.addItem("Name (A-Z)", "name")
|
||||
self.sort_combo.addItem("Owner", "owner")
|
||||
self.sort_combo.addItem("Rating", "average_rating")
|
||||
self.sort_combo.setMinimumWidth(120)
|
||||
self.sort_combo.setMinimumHeight(28)
|
||||
self.sort_combo.setToolTip("Change sort order of results")
|
||||
self.sort_combo.currentIndexChanged.connect(self._on_filter_changed)
|
||||
self.sort_combo.currentIndexChanged.connect(self._on_sort_changed)
|
||||
filters_layout.addWidget(self.sort_combo)
|
||||
|
||||
# Search button
|
||||
|
|
@ -190,6 +210,42 @@ class RegistryPage(QWidget):
|
|||
self.tags_widget.hide()
|
||||
layout.addWidget(self.tags_widget)
|
||||
|
||||
# Letter bar (shown only when sorting by name)
|
||||
self.letter_bar_widget = QWidget()
|
||||
self.letter_bar_widget.setMinimumHeight(40)
|
||||
letter_bar_layout = QHBoxLayout(self.letter_bar_widget)
|
||||
letter_bar_layout.setContentsMargins(0, 4, 0, 4)
|
||||
letter_bar_layout.setSpacing(2)
|
||||
self._letter_buttons = {}
|
||||
letters = ["#"] + [chr(c) for c in range(ord("A"), ord("Z") + 1)]
|
||||
for letter in letters:
|
||||
btn = QPushButton(letter)
|
||||
btn.setMinimumSize(26, 26)
|
||||
btn.setMaximumSize(30, 30)
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn.clicked.connect(lambda checked, l=letter: self._on_letter_clicked(l))
|
||||
letter_bar_layout.addWidget(btn)
|
||||
self._letter_buttons[letter] = btn
|
||||
letter_bar_layout.addStretch()
|
||||
# "All" button to clear letter filter
|
||||
btn_all = QPushButton("All")
|
||||
btn_all.setMinimumHeight(26)
|
||||
btn_all.setMaximumHeight(30)
|
||||
btn_all.setStyleSheet(
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn_all.clicked.connect(self._clear_letter_filter)
|
||||
letter_bar_layout.addWidget(btn_all)
|
||||
self._letter_buttons["All"] = btn_all
|
||||
self.letter_bar_widget.hide()
|
||||
layout.addWidget(self.letter_bar_widget)
|
||||
|
||||
# Content splitter
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
|
|
@ -209,6 +265,7 @@ class RegistryPage(QWidget):
|
|||
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
|
||||
self.results_table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.results_table.setSelectionMode(QTableWidget.SingleSelection)
|
||||
self.results_table.verticalHeader().setVisible(False)
|
||||
|
|
@ -217,10 +274,11 @@ class RegistryPage(QWidget):
|
|||
|
||||
# Pagination controls
|
||||
pagination = QWidget()
|
||||
pagination.setMinimumHeight(44)
|
||||
pag_layout = QHBoxLayout(pagination)
|
||||
pag_layout.setContentsMargins(0, 8, 0, 0)
|
||||
pag_layout.setContentsMargins(0, 6, 0, 6)
|
||||
|
||||
self.btn_prev = QPushButton("← Previous")
|
||||
self.btn_prev = QPushButton("← Prev")
|
||||
self.btn_prev.setToolTip("Go to previous page of results")
|
||||
self.btn_prev.clicked.connect(self._prev_page)
|
||||
self.btn_prev.setEnabled(False)
|
||||
|
|
@ -228,9 +286,13 @@ class RegistryPage(QWidget):
|
|||
|
||||
pag_layout.addStretch()
|
||||
|
||||
self.page_label = QLabel("Page 1 of 1")
|
||||
self.page_label.setStyleSheet("color: #718096;")
|
||||
pag_layout.addWidget(self.page_label)
|
||||
# Page buttons container (replaces static label)
|
||||
self.page_buttons_widget = QWidget()
|
||||
self.page_buttons_widget.setMinimumHeight(32)
|
||||
self.page_buttons_layout = QHBoxLayout(self.page_buttons_widget)
|
||||
self.page_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.page_buttons_layout.setSpacing(4)
|
||||
pag_layout.addWidget(self.page_buttons_widget)
|
||||
|
||||
pag_layout.addStretch()
|
||||
|
||||
|
|
@ -323,18 +385,79 @@ class RegistryPage(QWidget):
|
|||
"""Browse all tools (no search query)."""
|
||||
self.search_input.clear()
|
||||
self._current_page = 1
|
||||
self._current_prefix = None
|
||||
self._do_search()
|
||||
|
||||
def _get_sort_order(self, sort_field: str) -> str:
|
||||
"""Get the appropriate sort order for a sort field."""
|
||||
if sort_field in ASC_SORTS:
|
||||
return "asc"
|
||||
return "desc"
|
||||
|
||||
def _on_filter_changed(self):
|
||||
"""Handle filter/sort change."""
|
||||
"""Handle category filter change."""
|
||||
self._current_page = 1
|
||||
self._current_prefix = None
|
||||
self._do_search()
|
||||
|
||||
def _on_sort_changed(self):
|
||||
"""Handle sort change — show/hide letter bar, reset prefix."""
|
||||
sort = self.sort_combo.itemData(self.sort_combo.currentIndex())
|
||||
if sort == "name":
|
||||
self.letter_bar_widget.show()
|
||||
else:
|
||||
self.letter_bar_widget.hide()
|
||||
self._current_prefix = None
|
||||
self._update_letter_bar_highlight()
|
||||
self._current_page = 1
|
||||
self._do_search()
|
||||
|
||||
def _on_letter_clicked(self, letter: str):
|
||||
"""Handle letter bar click."""
|
||||
self._current_prefix = letter
|
||||
self._current_page = 1
|
||||
self._update_letter_bar_highlight()
|
||||
self._do_search()
|
||||
|
||||
def _clear_letter_filter(self):
|
||||
"""Clear letter filter — show all."""
|
||||
self._current_prefix = None
|
||||
self._current_page = 1
|
||||
self._update_letter_bar_highlight()
|
||||
self._do_search()
|
||||
|
||||
def _update_letter_bar_highlight(self):
|
||||
"""Highlight the active letter button."""
|
||||
active_style = (
|
||||
"QPushButton { border: 1px solid #4299e1; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #4299e1; color: white; font-weight: bold; }"
|
||||
)
|
||||
normal_style = (
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
normal_all_style = (
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
active_all_style = (
|
||||
"QPushButton { border: 1px solid #4299e1; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #4299e1; color: white; font-weight: bold; }"
|
||||
)
|
||||
for letter, btn in self._letter_buttons.items():
|
||||
if letter == "All":
|
||||
btn.setStyleSheet(active_all_style if self._current_prefix is None else normal_all_style)
|
||||
else:
|
||||
btn.setStyleSheet(active_style if letter == self._current_prefix else normal_style)
|
||||
|
||||
def _do_search(self):
|
||||
"""Perform search with current filters."""
|
||||
query = self.search_input.text().strip()
|
||||
category = self.category_combo.currentText()
|
||||
sort = self.sort_combo.currentData()
|
||||
sort = self.sort_combo.itemData(self.sort_combo.currentIndex())
|
||||
order = self._get_sort_order(sort)
|
||||
|
||||
self.btn_search.setEnabled(False)
|
||||
self.btn_browse.setEnabled(False)
|
||||
|
|
@ -345,6 +468,8 @@ class RegistryPage(QWidget):
|
|||
query=query,
|
||||
category=category,
|
||||
sort=sort,
|
||||
order=order,
|
||||
prefix=self._current_prefix,
|
||||
tags=self._current_tags if self._current_tags else None,
|
||||
page=self._current_page,
|
||||
per_page=20
|
||||
|
|
@ -363,11 +488,11 @@ class RegistryPage(QWidget):
|
|||
total = result.total or len(tools)
|
||||
|
||||
self.status_label.setText(f"Found {total} tools")
|
||||
self.page_label.setText(f"Page {self._current_page} of {self._total_pages}")
|
||||
|
||||
# Update pagination buttons
|
||||
self.btn_prev.setEnabled(self._current_page > 1)
|
||||
self.btn_next.setEnabled(self._current_page < self._total_pages)
|
||||
self._update_pagination()
|
||||
|
||||
self.results_table.setRowCount(len(tools))
|
||||
for row, tool in enumerate(tools):
|
||||
|
|
@ -413,6 +538,83 @@ class RegistryPage(QWidget):
|
|||
# Version
|
||||
self.results_table.setItem(row, 5, QTableWidgetItem(tool.get("version", "1.0.0")))
|
||||
|
||||
def _update_pagination(self):
|
||||
"""Rebuild interactive page number buttons."""
|
||||
# Clear existing buttons
|
||||
while self.page_buttons_layout.count():
|
||||
item = self.page_buttons_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if self._total_pages <= 1:
|
||||
lbl = QLabel("Page 1 of 1")
|
||||
lbl.setStyleSheet("color: #718096;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
return
|
||||
|
||||
current = self._current_page
|
||||
total = self._total_pages
|
||||
|
||||
# Determine which page numbers to show
|
||||
if total <= 7:
|
||||
pages = list(range(1, total + 1))
|
||||
else:
|
||||
# Sliding window of 5 centered on current page
|
||||
half = 2
|
||||
start = max(1, current - half)
|
||||
end = min(total, current + half)
|
||||
# Adjust if near edges
|
||||
if current - half < 1:
|
||||
end = min(total, end + (1 - (current - half)))
|
||||
if current + half > total:
|
||||
start = max(1, start - (current + half - total))
|
||||
pages = list(range(start, end + 1))
|
||||
|
||||
# Leading ellipsis
|
||||
if pages and pages[0] > 1:
|
||||
self._add_page_button(1)
|
||||
if pages[0] > 2:
|
||||
lbl = QLabel("...")
|
||||
lbl.setStyleSheet("color: #718096; padding: 0 2px;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
|
||||
for p in pages:
|
||||
self._add_page_button(p)
|
||||
|
||||
# Trailing ellipsis
|
||||
if pages and pages[-1] < total:
|
||||
if pages[-1] < total - 1:
|
||||
lbl = QLabel("...")
|
||||
lbl.setStyleSheet("color: #718096; padding: 0 2px;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
self._add_page_button(total)
|
||||
|
||||
def _add_page_button(self, page_num: int):
|
||||
"""Add a single page number button."""
|
||||
btn = QPushButton(str(page_num))
|
||||
btn.setMinimumSize(32, 28)
|
||||
btn.setMaximumHeight(30)
|
||||
if page_num == self._current_page:
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { background: #4299e1; color: white; border: none; "
|
||||
"border-radius: 4px; font-weight: bold; font-size: 12px; "
|
||||
"padding: 2px 6px; }"
|
||||
)
|
||||
else:
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { background: #edf2f7; border: 1px solid #a0aec0; "
|
||||
"border-radius: 4px; color: #1a202c; font-size: 12px; "
|
||||
"padding: 2px 6px; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn.clicked.connect(lambda checked, p=page_num: self._go_to_page(p))
|
||||
self.page_buttons_layout.addWidget(btn)
|
||||
|
||||
def _go_to_page(self, page: int):
|
||||
"""Navigate to a specific page."""
|
||||
self._current_page = page
|
||||
self._do_search()
|
||||
|
||||
def _rating_to_stars(self, rating: float) -> str:
|
||||
"""Convert rating to star display."""
|
||||
if rating <= 0:
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ RATE_LIMITS = {
|
|||
}
|
||||
|
||||
ALLOWED_SORT = {
|
||||
"/tools": {"downloads", "published_at", "name"},
|
||||
"/tools/search": {"relevance", "downloads", "published_at"},
|
||||
"/tools": {"downloads", "published_at", "name", "owner", "average_rating"},
|
||||
"/tools/search": {"relevance", "downloads", "published_at", "name", "owner", "average_rating"},
|
||||
"/categories": {"name", "tool_count"},
|
||||
}
|
||||
|
||||
|
|
@ -602,8 +602,18 @@ def create_app() -> Flask:
|
|||
if error:
|
||||
return error
|
||||
category = request.args.get("category")
|
||||
prefix = request.args.get("prefix", "").strip()
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
# Validate prefix: single letter A-Z or '#'
|
||||
if prefix:
|
||||
if prefix == "#":
|
||||
pass # valid
|
||||
elif len(prefix) == 1 and prefix.isalpha():
|
||||
prefix = prefix.upper()
|
||||
else:
|
||||
prefix = "" # invalid, ignore
|
||||
|
||||
# Build visibility filter
|
||||
vis_filter, vis_params = build_visibility_filter()
|
||||
|
||||
|
|
@ -612,6 +622,11 @@ def create_app() -> Flask:
|
|||
if category:
|
||||
base_where += " AND category = ?"
|
||||
params.append(category)
|
||||
if prefix == "#":
|
||||
base_where += " AND name NOT GLOB '[A-Za-z]*'"
|
||||
elif prefix:
|
||||
base_where += " AND LOWER(name) LIKE ?"
|
||||
params.append(f"{prefix.lower()}%")
|
||||
# Add visibility filter
|
||||
base_where += vis_filter
|
||||
params.extend(vis_params)
|
||||
|
|
@ -624,7 +639,13 @@ def create_app() -> Flask:
|
|||
total = int(count_row["total"]) if count_row else 0
|
||||
|
||||
order_dir = "DESC" if order == "desc" else "ASC"
|
||||
order_sql = f"{sort} {order_dir}, published_at DESC, id DESC"
|
||||
if sort == "average_rating":
|
||||
order_sql = f"COALESCE(ts.average_rating, 0) {order_dir}, published_at DESC, t.id DESC"
|
||||
else:
|
||||
order_sql = f"{sort} {order_dir}, published_at DESC, id DESC"
|
||||
|
||||
# LEFT JOIN tool_stats for rating data
|
||||
stats_join = "LEFT JOIN tool_stats ts ON ts.tool_id = t.id"
|
||||
|
||||
rows = query_all(
|
||||
g.db,
|
||||
|
|
@ -641,7 +662,10 @@ def create_app() -> Flask:
|
|||
{base_where} AND version NOT LIKE '%-%'
|
||||
GROUP BY owner, name
|
||||
)
|
||||
SELECT t.* FROM tools t
|
||||
SELECT t.*, COALESCE(ts.average_rating, 0) AS average_rating,
|
||||
COALESCE(ts.rating_count, 0) AS rating_count
|
||||
FROM tools t
|
||||
{stats_join}
|
||||
JOIN (
|
||||
SELECT a.owner, a.name, COALESCE(s.max_id, a.max_id) AS max_id
|
||||
FROM latest_any a
|
||||
|
|
@ -665,6 +689,8 @@ def create_app() -> Flask:
|
|||
"tags": json.loads(row["tags"] or "[]"),
|
||||
"downloads": row["downloads"],
|
||||
"published_at": row["published_at"],
|
||||
"average_rating": row["average_rating"],
|
||||
"rating_count": row["rating_count"],
|
||||
})
|
||||
|
||||
return jsonify({"data": data, "meta": paginate(page, per_page, total)})
|
||||
|
|
@ -693,6 +719,16 @@ def create_app() -> Flask:
|
|||
return error
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
# Prefix filter
|
||||
prefix = request.args.get("prefix", "").strip()
|
||||
if prefix:
|
||||
if prefix == "#":
|
||||
pass
|
||||
elif len(prefix) == 1 and prefix.isalpha():
|
||||
prefix = prefix.upper()
|
||||
else:
|
||||
prefix = ""
|
||||
|
||||
# Parse filter parameters
|
||||
category = request.args.get("category") # Single category (backward compat)
|
||||
categories_param = request.args.get("categories", "") # Multi-category (OR logic)
|
||||
|
|
@ -743,6 +779,12 @@ def create_app() -> Flask:
|
|||
if not include_deprecated:
|
||||
where_clauses.append("tools.deprecated = 0")
|
||||
|
||||
if prefix == "#":
|
||||
where_clauses.append("tools.name NOT GLOB '[A-Za-z]*'")
|
||||
elif prefix:
|
||||
where_clauses.append("LOWER(tools.name) LIKE ?")
|
||||
params.append(f"{prefix.lower()}%")
|
||||
|
||||
# Add visibility filtering
|
||||
vis_filter, vis_params = build_visibility_filter("tools")
|
||||
if vis_filter:
|
||||
|
|
@ -772,9 +814,13 @@ def create_app() -> Flask:
|
|||
|
||||
order_dir = "DESC" if order == "desc" else "ASC"
|
||||
if sort == "relevance":
|
||||
order_sql = f"rank {order_dir}, downloads DESC, published_at DESC, id DESC"
|
||||
order_sql = "rank, downloads DESC, published_at DESC, f.id DESC"
|
||||
elif sort == "average_rating":
|
||||
order_sql = f"COALESCE(ts.average_rating, 0) {order_dir}, downloads DESC, published_at DESC, f.id DESC"
|
||||
else:
|
||||
order_sql = f"{sort} {order_dir}, published_at DESC, id DESC"
|
||||
order_sql = f"{sort} {order_dir}, published_at DESC, f.id DESC"
|
||||
|
||||
stats_join = "LEFT JOIN tool_stats ts ON ts.tool_id = f.id"
|
||||
|
||||
rows = query_all(
|
||||
g.db,
|
||||
|
|
@ -801,7 +847,10 @@ def create_app() -> Flask:
|
|||
WHERE version NOT LIKE '%-%'
|
||||
GROUP BY owner, name
|
||||
)
|
||||
SELECT f.* FROM filtered f
|
||||
SELECT f.*, COALESCE(ts.average_rating, 0) AS average_rating,
|
||||
COALESCE(ts.rating_count, 0) AS rating_count
|
||||
FROM filtered f
|
||||
{stats_join}
|
||||
JOIN (
|
||||
SELECT a.owner, a.name, COALESCE(s.max_id, a.max_id) AS max_id
|
||||
FROM latest_any a
|
||||
|
|
@ -847,6 +896,8 @@ def create_app() -> Flask:
|
|||
"tags": json.loads(row["tags"] or "[]"),
|
||||
"downloads": row["downloads"],
|
||||
"published_at": row["published_at"],
|
||||
"average_rating": row["average_rating"],
|
||||
"rating_count": row["rating_count"],
|
||||
"score": score,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -290,7 +290,8 @@ class RegistryClient:
|
|||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
sort: str = "downloads",
|
||||
order: str = "desc"
|
||||
order: str = "desc",
|
||||
prefix: Optional[str] = None
|
||||
) -> PaginatedResponse:
|
||||
"""
|
||||
List tools from the registry.
|
||||
|
|
@ -299,8 +300,9 @@ class RegistryClient:
|
|||
category: Filter by category
|
||||
page: Page number (1-indexed)
|
||||
per_page: Items per page (max 100)
|
||||
sort: Sort field (downloads, published_at, name)
|
||||
sort: Sort field (downloads, published_at, name, owner, average_rating)
|
||||
order: Sort order (asc, desc)
|
||||
prefix: Letter prefix filter (A-Z or '#' for non-alpha)
|
||||
|
||||
Returns:
|
||||
PaginatedResponse with tool data
|
||||
|
|
@ -313,6 +315,8 @@ class RegistryClient:
|
|||
}
|
||||
if category:
|
||||
params["category"] = category
|
||||
if prefix:
|
||||
params["prefix"] = prefix
|
||||
|
||||
response = self._request("GET", "/tools", params=params)
|
||||
|
||||
|
|
@ -345,7 +349,9 @@ class RegistryClient:
|
|||
include_facets: bool = False,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
sort: str = "relevance"
|
||||
sort: str = "relevance",
|
||||
order: str = "desc",
|
||||
prefix: Optional[str] = None
|
||||
) -> PaginatedResponse:
|
||||
"""
|
||||
Search for tools in the registry.
|
||||
|
|
@ -364,7 +370,9 @@ class RegistryClient:
|
|||
include_facets: Include category/tag/owner counts in response
|
||||
page: Page number
|
||||
per_page: Items per page
|
||||
sort: Sort field (relevance, downloads, published_at)
|
||||
sort: Sort field (relevance, downloads, published_at, name, owner, average_rating)
|
||||
order: Sort order (asc, desc)
|
||||
prefix: Letter prefix filter (A-Z or '#' for non-alpha)
|
||||
|
||||
Returns:
|
||||
PaginatedResponse with matching tools (and facets if requested)
|
||||
|
|
@ -373,7 +381,8 @@ class RegistryClient:
|
|||
"q": query,
|
||||
"page": page,
|
||||
"per_page": min(per_page, 100),
|
||||
"sort": sort
|
||||
"sort": sort,
|
||||
"order": order
|
||||
}
|
||||
if category:
|
||||
params["category"] = category
|
||||
|
|
@ -395,6 +404,8 @@ class RegistryClient:
|
|||
params["deprecated"] = "true"
|
||||
if include_facets:
|
||||
params["include_facets"] = "true"
|
||||
if prefix:
|
||||
params["prefix"] = prefix
|
||||
|
||||
response = self._request("GET", "/tools/search", params=params)
|
||||
|
||||
|
|
|
|||
|
|
@ -209,8 +209,8 @@ def execute_prompt_step(
|
|||
# Prepend system prompt to user prompt
|
||||
prompt = f"{profile.system_prompt}\n\n---\n\n{prompt}"
|
||||
|
||||
# Call provider
|
||||
provider = provider_override or step.provider
|
||||
# Call provider (support variable substitution in provider name, e.g. {settings.provider})
|
||||
provider = provider_override or substitute_variables(step.provider, variables, warn_non_scalar=verbose)
|
||||
|
||||
if provider.lower() == "mock":
|
||||
result = mock_provider(prompt)
|
||||
|
|
|
|||
Loading…
Reference in New Issue