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:
rob 2026-01-30 01:35:03 -04:00
parent 0ee21f27f7
commit b00115a52e
5 changed files with 330 additions and 37 deletions

View File

@ -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()

View File

@ -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:

View File

@ -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,
})

View File

@ -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)

View File

@ -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)