mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
59cec5be65
## Summary The `chattool` `list_templates` tool previously returned all templates in a single response with no popularity signal. On deployments with many templates (e.g. 71 on dogfood), this wastes tokens and makes it hard for the AI to pick the right template for broad user questions. ## Changes Single file: `coderd/chatd/chattool/listtemplates.go` - **`page` parameter** — optional, 1-indexed, 10 results per page - **Popularity sort** — queries `GetWorkspaceUniqueOwnerCountByTemplateIDs` to get active developer counts, then sorts descending (most popular first). The DB query returns templates alphabetically, so this explicit sort is needed. - **`active_developers`** — included on each template item so the agent can see the signal - **Pagination metadata** — `page`, `total_pages`, `total_count` in the response so the agent knows there are more results - **Updated tool description** — tells the agent that results are ordered by popularity and paginated ## Frontend No frontend changes needed. The renderer already reads `rec.templates` and `rec.count` from the response — the new fields (`page`, `total_pages`, `total_count`) are additive and safely ignored.
149 lines
4.3 KiB
Go
149 lines
4.3 KiB
Go
package chattool
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"sort"
|
|
"strings"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
)
|
|
|
|
const listTemplatesPageSize = 10
|
|
|
|
// ListTemplatesOptions configures the list_templates tool.
|
|
type ListTemplatesOptions struct {
|
|
DB database.Store
|
|
OwnerID uuid.UUID
|
|
}
|
|
|
|
type listTemplatesArgs struct {
|
|
Query string `json:"query,omitempty"`
|
|
Page int `json:"page,omitempty"`
|
|
}
|
|
|
|
// ListTemplates returns a tool that lists available workspace templates.
|
|
// The agent uses this to discover templates before creating a workspace.
|
|
// Results are ordered by number of active developers (most popular first)
|
|
// and paginated at 10 per page.
|
|
func ListTemplates(options ListTemplatesOptions) fantasy.AgentTool {
|
|
return fantasy.NewAgentTool(
|
|
"list_templates",
|
|
"List available workspace templates. Optionally filter by a "+
|
|
"search query matching template name or description. "+
|
|
"Use this to find a template before creating a workspace. "+
|
|
"Results are ordered by number of active developers (most popular first). "+
|
|
"Returns 10 per page. Use the page parameter to paginate through results.",
|
|
func(ctx context.Context, args listTemplatesArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
|
if options.DB == nil {
|
|
return fantasy.NewTextErrorResponse("database is not configured"), nil
|
|
}
|
|
|
|
ctx, err := asOwner(ctx, options.DB, options.OwnerID)
|
|
if err != nil {
|
|
return fantasy.NewTextErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
filterParams := database.GetTemplatesWithFilterParams{
|
|
Deleted: false,
|
|
Deprecated: sql.NullBool{
|
|
Bool: false,
|
|
Valid: true,
|
|
},
|
|
}
|
|
query := strings.TrimSpace(args.Query)
|
|
if query != "" {
|
|
filterParams.FuzzyName = query
|
|
}
|
|
|
|
templates, err := options.DB.GetTemplatesWithFilter(ctx, filterParams)
|
|
if err != nil {
|
|
return fantasy.NewTextErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
// Look up active developer counts so we can sort by popularity.
|
|
templateIDs := make([]uuid.UUID, len(templates))
|
|
for i, t := range templates {
|
|
templateIDs[i] = t.ID
|
|
}
|
|
ownerCounts := make(map[uuid.UUID]int64)
|
|
if len(templateIDs) > 0 {
|
|
rows, countErr := options.DB.GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx, templateIDs)
|
|
if countErr == nil {
|
|
for _, row := range rows {
|
|
ownerCounts[row.TemplateID] = row.UniqueOwnersSum
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by active developer count descending.
|
|
sort.SliceStable(templates, func(i, j int) bool {
|
|
return ownerCounts[templates[i].ID] > ownerCounts[templates[j].ID]
|
|
})
|
|
|
|
// Paginate.
|
|
page := args.Page
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
totalCount := len(templates)
|
|
totalPages := (totalCount + listTemplatesPageSize - 1) / listTemplatesPageSize
|
|
if totalPages == 0 {
|
|
totalPages = 1
|
|
}
|
|
start := (page - 1) * listTemplatesPageSize
|
|
end := start + listTemplatesPageSize
|
|
if start > totalCount {
|
|
start = totalCount
|
|
}
|
|
if end > totalCount {
|
|
end = totalCount
|
|
}
|
|
pageTemplates := templates[start:end]
|
|
|
|
items := make([]map[string]any, 0, len(pageTemplates))
|
|
for _, t := range pageTemplates {
|
|
item := map[string]any{
|
|
"id": t.ID.String(),
|
|
"name": t.Name,
|
|
}
|
|
if display := strings.TrimSpace(t.DisplayName); display != "" {
|
|
item["display_name"] = display
|
|
}
|
|
if desc := strings.TrimSpace(t.Description); desc != "" {
|
|
item["description"] = truncateRunes(desc, 200)
|
|
}
|
|
if count, ok := ownerCounts[t.ID]; ok && count > 0 {
|
|
item["active_developers"] = count
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
return toolResponse(map[string]any{
|
|
"templates": items,
|
|
"count": len(items),
|
|
"page": page,
|
|
"total_pages": totalPages,
|
|
"total_count": totalCount,
|
|
}), nil
|
|
},
|
|
)
|
|
}
|
|
|
|
// asOwner sets up a dbauthz context for the given owner so that
|
|
// subsequent database calls are scoped to what that user can access.
|
|
func asOwner(ctx context.Context, db database.Store, ownerID uuid.UUID) (context.Context, error) {
|
|
actor, _, err := httpmw.UserRBACSubject(ctx, db, ownerID, rbac.ScopeAll)
|
|
if err != nil {
|
|
return ctx, xerrors.Errorf("load user authorization: %w", err)
|
|
}
|
|
return dbauthz.As(ctx, actor), nil
|
|
}
|