mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add experimental agents support (#22290)
feat: add AI chat system with agent tools and chat UI Introduce the chatd subsystem and Agents UI for AI-powered chat within Coder workspaces. - Add chatd package with chat loop, message compaction, prompt management, and LLM provider integration (OpenAI, Anthropic) - Add agent tools: create workspace, list/read templates, read/write/ edit files, execute commands - Add chat API endpoints with streaming, message editing, and durable reconnection - Add database schema and migrations for chats, chat messages, chat providers, and chat model configs - Add RBAC policies and dbauthz enforcement for chat resources - Add Agents UI pages with conversation timeline, queued messages list, diff viewer, and model configuration panel - Add comprehensive test coverage including coderd integration tests, chatd unit tests, and Storybook stories - Gate feature behind experiments flag --------- Co-authored-by: Cian Johnston <cian@coder.com> Co-authored-by: Danielle Maywood <danielle@themaywoods.com> Co-authored-by: Jeremy Ruppel <jeremy@coder.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Cian Johnston
Danielle Maywood
Jeremy Ruppel
Claude Sonnet 4.6
parent
67da4e8b56
commit
edee917d88
@@ -0,0 +1,130 @@
|
||||
package chattool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
// ReadTemplateOptions configures the read_template tool.
|
||||
type ReadTemplateOptions struct {
|
||||
DB database.Store
|
||||
OwnerID uuid.UUID
|
||||
}
|
||||
|
||||
type readTemplateArgs struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
}
|
||||
|
||||
// ReadTemplate returns a tool that retrieves details about a specific
|
||||
// template, including its configurable rich parameters. The agent
|
||||
// uses this after list_templates and before create_workspace.
|
||||
func ReadTemplate(options ReadTemplateOptions) fantasy.AgentTool {
|
||||
return fantasy.NewAgentTool(
|
||||
"read_template",
|
||||
"Get details about a workspace template, including its "+
|
||||
"configurable parameters. Use this after finding a "+
|
||||
"template with list_templates and before creating a "+
|
||||
"workspace with create_workspace.",
|
||||
func(ctx context.Context, args readTemplateArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
|
||||
if options.DB == nil {
|
||||
return fantasy.NewTextErrorResponse("database is not configured"), nil
|
||||
}
|
||||
|
||||
templateIDStr := strings.TrimSpace(args.TemplateID)
|
||||
if templateIDStr == "" {
|
||||
return fantasy.NewTextErrorResponse("template_id is required"), nil
|
||||
}
|
||||
templateID, err := uuid.Parse(templateIDStr)
|
||||
if err != nil {
|
||||
return fantasy.NewTextErrorResponse(
|
||||
xerrors.Errorf("invalid template_id: %w", err).Error(),
|
||||
), nil
|
||||
}
|
||||
|
||||
ctx, err = asOwner(ctx, options.DB, options.OwnerID)
|
||||
if err != nil {
|
||||
return fantasy.NewTextErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
template, err := options.DB.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
return fantasy.NewTextErrorResponse("template not found"), nil
|
||||
}
|
||||
|
||||
params, err := options.DB.GetTemplateVersionParameters(ctx, template.ActiveVersionID)
|
||||
if err != nil {
|
||||
return fantasy.NewTextErrorResponse(
|
||||
xerrors.Errorf("failed to get template parameters: %w", err).Error(),
|
||||
), nil
|
||||
}
|
||||
|
||||
templateInfo := map[string]any{
|
||||
"id": template.ID.String(),
|
||||
"name": template.Name,
|
||||
"active_version_id": template.ActiveVersionID.String(),
|
||||
}
|
||||
if display := strings.TrimSpace(template.DisplayName); display != "" {
|
||||
templateInfo["display_name"] = display
|
||||
}
|
||||
if desc := strings.TrimSpace(template.Description); desc != "" {
|
||||
templateInfo["description"] = desc
|
||||
}
|
||||
|
||||
paramList := make([]map[string]any, 0, len(params))
|
||||
for _, p := range params {
|
||||
param := map[string]any{
|
||||
"name": p.Name,
|
||||
"type": p.Type,
|
||||
"required": p.Required,
|
||||
}
|
||||
if display := strings.TrimSpace(p.DisplayName); display != "" {
|
||||
param["display_name"] = display
|
||||
}
|
||||
if desc := strings.TrimSpace(p.Description); desc != "" {
|
||||
param["description"] = truncateRunes(desc, 300)
|
||||
}
|
||||
if p.DefaultValue != "" {
|
||||
param["default"] = p.DefaultValue
|
||||
}
|
||||
if p.Mutable {
|
||||
param["mutable"] = true
|
||||
}
|
||||
if p.Ephemeral {
|
||||
param["ephemeral"] = true
|
||||
}
|
||||
if p.FormType != "" {
|
||||
param["form_type"] = string(p.FormType)
|
||||
}
|
||||
if len(p.Options) > 0 && string(p.Options) != "null" && string(p.Options) != "[]" {
|
||||
var opts []map[string]any
|
||||
if err := json.Unmarshal(p.Options, &opts); err == nil && len(opts) > 0 {
|
||||
param["options"] = opts
|
||||
}
|
||||
}
|
||||
if p.ValidationRegex != "" {
|
||||
param["validation_regex"] = p.ValidationRegex
|
||||
}
|
||||
if p.ValidationMin.Valid {
|
||||
param["validation_min"] = p.ValidationMin.Int32
|
||||
}
|
||||
if p.ValidationMax.Valid {
|
||||
param["validation_max"] = p.ValidationMax.Int32
|
||||
}
|
||||
|
||||
paramList = append(paramList, param)
|
||||
}
|
||||
|
||||
return toolResponse(map[string]any{
|
||||
"template": templateInfo,
|
||||
"parameters": paramList,
|
||||
}), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user