yao/openapi/agent/assistant.go
Max 9eef569e4b feat(agent): enhance system configuration and connector management
- Updated the system configuration to include new role-level defaults for Light, Vision, and Audio connectors.
- Refactored the resolveSystemConnector function to prioritize per-agent overrides, improving connector resolution logic.
- Enhanced LLMConnector integration across various components to streamline settings retrieval and capabilities management.
- Improved error handling and logging for connector-related operations, ensuring better diagnostics and user feedback.
2026-05-01 10:31:58 +08:00

675 lines
21 KiB
Go

package agent
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent"
assistantPkg "github.com/yaoapp/yao/agent/assistant"
agenttypes "github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// ListAssistants lists assistants with pagination and filtering
func ListAssistants(c *gin.Context) {
// Get authorized information
authInfo := authorized.GetInfo(c)
// Get Agent instance from global variable
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Agent store not initialized",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Parse pagination parameters
page := 1
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pagesize := 20
if pagesizeStr := c.Query("pagesize"); pagesizeStr != "" {
if ps, err := strconv.Atoi(pagesizeStr); err == nil && ps > 0 && ps <= 100 {
pagesize = ps
}
}
// Validate pagination
if err := ValidatePagination(page, pagesize); err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Parse select parameter
var selectFields []string
if selectParam := strings.TrimSpace(c.Query("select")); selectParam != "" {
requestedFields := strings.Split(selectParam, ",")
for _, field := range requestedFields {
field = strings.TrimSpace(field)
if field != "" && availableAssistantFields[field] {
selectFields = append(selectFields, field)
}
}
// If no valid fields found, use default
if len(selectFields) == 0 {
selectFields = defaultAssistantFields
}
} else {
selectFields = defaultAssistantFields
}
// Parse filter parameters
keywords := strings.TrimSpace(c.Query("keywords"))
typeParam := strings.TrimSpace(c.Query("type"))
connector := strings.TrimSpace(c.Query("connector"))
assistantID := strings.TrimSpace(c.Query("assistant_id"))
// Parse types (multiple, comma-separated for IN query)
var types []string
if typesParam := c.Query("types"); typesParam != "" {
types = strings.Split(typesParam, ",")
// Trim spaces
for i, t := range types {
types[i] = strings.TrimSpace(t)
}
}
// Set default type only if neither type nor types is specified
if typeParam == "" && len(types) == 0 {
typeParam = "assistant" // Default type
}
// Parse assistant IDs (multiple)
var assistantIDs []string
if assistantIDsParam := c.Query("assistant_ids"); assistantIDsParam != "" {
assistantIDs = strings.Split(assistantIDsParam, ",")
// Trim spaces
for i, id := range assistantIDs {
assistantIDs[i] = strings.TrimSpace(id)
}
}
// Parse tags
var tags []string
if tagsParam := c.Query("tags"); tagsParam != "" {
tags = strings.Split(tagsParam, ",")
// Trim spaces
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
}
// Parse boolean filters
var builtIn, mentionable, automated, sandbox *bool
if builtInParam := c.Query("built_in"); builtInParam != "" {
builtIn = parseBoolValue(builtInParam)
}
if mentionableParam := c.Query("mentionable"); mentionableParam != "" {
mentionable = parseBoolValue(mentionableParam)
}
if automatedParam := c.Query("automated"); automatedParam != "" {
automated = parseBoolValue(automatedParam)
}
if sandboxParam := c.Query("sandbox"); sandboxParam != "" {
sandbox = parseBoolValue(sandboxParam)
}
// Note: public and share filters are not yet supported in AssistantFilter
// They would need to be added to the store layer for proper filtering
// Parse locale
locale := "en-us" // Default locale
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
// Build filter using the existing AssistantFilter structure
filter := BuildAssistantFilter(AssistantFilterParams{
Page: page,
PageSize: pagesize,
Keywords: keywords,
Type: typeParam,
Types: types,
Connector: connector,
AssistantID: assistantID,
AssistantIDs: assistantIDs,
Tags: tags,
SelectFields: selectFields,
BuiltIn: builtIn,
Mentionable: mentionable,
Automated: automated,
Sandbox: sandbox,
})
// Apply permission-based filtering (Scope filtering)
filter.QueryFilter = AuthQueryFilter(c, authInfo)
// Use the existing GetAssistants method from agent.Store
result, err := agentInstance.Store.GetAssistants(filter, locale)
if err != nil {
log.Error("Failed to list assistants: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to list assistants: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Convert sandbox to boolean and filter built-in sensitive fields
resp := map[string]interface{}{
"data": AssistantsToResponse(result.Data, authInfo),
"total": result.Total,
"page": result.Page,
"pagesize": result.PageSize,
"pagecount": result.PageCount,
"next": result.Next,
"prev": result.Prev,
}
// Return the result with standard response format
response.RespondWithSuccess(c, response.StatusOK, resp)
}
// GetAssistant retrieves a single assistant by ID with permission verification
func GetAssistant(c *gin.Context) {
// Get authorized information
authInfo := authorized.GetInfo(c)
// Get Agent instance from global variable
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Agent store not initialized",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Get assistant ID from URL parameter
assistantID := c.Param("id")
if assistantID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "assistant_id is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Parse select fields (optional - if not provided, returns default fields)
// Query parameter: ?select=field1,field2,field3
var fields []string
if selectParam := c.Query("select"); selectParam != "" {
fields = strings.Split(selectParam, ",")
// Trim whitespace from each field
for i, field := range fields {
fields[i] = strings.TrimSpace(field)
}
}
// Parse locale (optional - if not provided, returns raw data without i18n translation)
// This is useful for form editing scenarios where you need the original values
var assistant *agenttypes.AssistantModel
var err error
if loc := c.Query("locale"); loc != "" {
// If locale is specified, get assistant with translation
locale := strings.ToLower(strings.TrimSpace(loc))
assistant, err = agentInstance.Store.GetAssistant(assistantID, fields, locale)
} else {
// If no locale specified, get raw data without translation
assistant, err = agentInstance.Store.GetAssistant(assistantID, fields)
}
if err != nil {
log.Error("Failed to get assistant %s: %v", assistantID, err)
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Assistant not found: " + err.Error(),
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Check read permission
hasPermission, err := checkAssistantPermission(authInfo, assistantID, true)
if err != nil {
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to check permission: " + err.Error(),
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
if !hasPermission {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden: No permission to access this assistant",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
// Convert sandbox to boolean and filter built-in sensitive fields
hasSandbox := assistant.Sandbox != nil
FilterBuiltInAssistant(assistant)
resp := AssistantToResponse(assistant, hasSandbox, authInfo)
// Return the result with standard response format
response.RespondWithSuccess(c, response.StatusOK, resp)
}
// ListAssistantTags lists assistant tags with permission-based filtering
func ListAssistantTags(c *gin.Context) {
// Get authorized information
authInfo := authorized.GetInfo(c)
// Get Agent instance from global variable
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Agent store not initialized",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Parse locale
locale := "en-us" // Default locale
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
// Parse filter parameters
typeParam := strings.TrimSpace(c.Query("type"))
connector := strings.TrimSpace(c.Query("connector"))
keywords := strings.TrimSpace(c.Query("keywords"))
// Parse types (multiple, comma-separated for IN query)
var types []string
if typesParam := c.Query("types"); typesParam != "" {
for _, t := range strings.Split(typesParam, ",") {
if trimmed := strings.TrimSpace(t); trimmed != "" {
types = append(types, trimmed)
}
}
}
// Set default type only if neither type nor types is specified
if typeParam == "" && len(types) == 0 {
typeParam = "assistant"
}
// Parse boolean filters
var builtIn, mentionable, automated *bool
if builtInParam := c.Query("built_in"); builtInParam != "" {
builtIn = parseBoolValue(builtInParam)
}
if mentionableParam := c.Query("mentionable"); mentionableParam != "" {
mentionable = parseBoolValue(mentionableParam)
}
if automatedParam := c.Query("automated"); automatedParam != "" {
automated = parseBoolValue(automatedParam)
}
// Build filter
filter := BuildAssistantFilter(AssistantFilterParams{
Type: typeParam,
Types: types,
Connector: connector,
Keywords: keywords,
BuiltIn: builtIn,
Mentionable: mentionable,
Automated: automated,
})
// Apply permission-based filtering (Scope filtering)
filter.QueryFilter = AuthQueryFilter(c, authInfo)
// Get tags with filter
tags, err := agentInstance.Store.GetAssistantTags(filter, locale)
if err != nil {
log.Error("Failed to get assistant tags: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get assistant tags: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Return the result with standard response format
response.RespondWithSuccess(c, response.StatusOK, tags)
}
// CreateAssistant creates a new assistant
func CreateAssistant(c *gin.Context) {
// Get authorized information
authInfo := authorized.GetInfo(c)
// Get Agent instance from global variable
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Agent store not initialized",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Parse request body
var assistantData map[string]interface{}
if err := c.ShouldBindJSON(&assistantData); err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request body: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Convert to AssistantModel
model, err := agenttypes.ToAssistantModel(assistantData)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid assistant data: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Attach create scope to the assistant data
if authInfo != nil {
scope := authInfo.AccessScope()
model.YaoCreatedBy = scope.CreatedBy
model.YaoUpdatedBy = scope.UpdatedBy
model.YaoTeamID = scope.TeamID
model.YaoTenantID = scope.TenantID
}
// Save assistant using Store
id, err := agentInstance.Store.SaveAssistant(model)
if err != nil {
log.Error("Failed to create assistant: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to create assistant: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Update the assistant map with the returned ID
assistantData["assistant_id"] = id
// Clear cache and reload assistant to make it effective
cache := assistantPkg.GetCache()
if cache != nil {
cache.Remove(id)
}
// Reload the assistant to ensure it's available in cache with updated data
_, err = assistantPkg.Get(id)
if err != nil {
// Just log the error, don't fail the request
log.Error("Error reloading assistant %s: %v", id, err)
}
// Return success response with only assistant_id
response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{
"assistant_id": id,
})
}
// UpdateAssistant updates an existing assistant
func UpdateAssistant(c *gin.Context) {
// Get authorized information
authInfo := authorized.GetInfo(c)
// Get Agent instance from global variable
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Agent store not initialized",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Get assistant ID from URL parameter
assistantID := c.Param("id")
if assistantID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "assistant_id is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Check update permission
hasPermission, err := checkAssistantPermission(authInfo, assistantID, false)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: err.Error(),
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
// 403 Forbidden
if !hasPermission {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden: No permission to update this assistant",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
// Parse request body with update data
var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request body: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Add update metadata
if authInfo != nil {
scope := authInfo.AccessScope()
updateData["__yao_updated_by"] = scope.UpdatedBy
}
// Update assistant using Store
err = agentInstance.Store.UpdateAssistant(assistantID, updateData)
if err != nil {
log.Error("Failed to update assistant %s: %v", assistantID, err)
// Check if it's a "not found" error
if strings.Contains(err.Error(), "not found") {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Assistant not found: " + assistantID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
} else {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to update assistant: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
}
return
}
// Clear cache and reload assistant to make it effective
cache := assistantPkg.GetCache()
if cache != nil {
cache.Remove(assistantID)
}
// Reload the assistant to ensure it's available in cache with updated data
_, err = assistantPkg.Get(assistantID)
if err != nil {
// Just log the error, don't fail the request
log.Error("Error reloading assistant %s: %v", assistantID, err)
}
// Return success response with only assistant_id
response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{
"assistant_id": assistantID,
})
}
// GetAssistantInfo retrieves essential assistant information for InputArea component
func GetAssistantInfo(c *gin.Context) {
authInfo := authorized.GetInfo(c)
assistantID := c.Param("id")
if assistantID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "assistant_id is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
locale := "en-us"
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
hasPermission, err := checkAssistantPermission(authInfo, assistantID, true)
if err != nil {
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to check permission: " + err.Error(),
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
if !hasPermission {
errorResp := &response.ErrorResponse{
Code: response.ErrAccessDenied.Code,
ErrorDescription: "Forbidden: No permission to access this assistant",
}
response.RespondWithError(c, response.StatusForbidden, errorResp)
return
}
ast, err := assistantPkg.Get(assistantID)
if err != nil || ast == nil {
log.Error("Failed to get assistant info %s: %v", assistantID, err)
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Assistant not found",
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
info := ast.GetInfo(locale)
resolved, raw := resolveConnectorForResponse(info.Connector, authInfo)
info.Connector = resolved
info.ConnectorRaw = raw
response.RespondWithSuccess(c, response.StatusOK, info)
}
// checkAssistantPermission checks if the user has permission to access the assistant
// Similar logic to checkCollectionPermission in openapi/kb/collection.go
// readable: true for read permission, false for write permission
func checkAssistantPermission(authInfo *types.AuthorizedInfo, assistantID string, readable ...bool) (bool, error) {
// No auth info, allow access
if authInfo == nil {
return true, nil
}
// No constraints, allow access
if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly {
return true, nil
}
// Get Agent instance
agentInstance := agent.GetAgent()
if agentInstance == nil || agentInstance.Store == nil {
return false, fmt.Errorf("agent store not initialized")
}
// Get assistant from store - only need default fields for permission check
assistant, err := agentInstance.Store.GetAssistant(assistantID, nil)
if err != nil {
return false, fmt.Errorf("assistant not found: %s", assistantID)
}
// If readable mode, check if the assistant is accessible for reading
if len(readable) > 0 && readable[0] {
// If assistant is public, allow read access
if assistant.Public {
return true, nil
}
// Team only permission validation for read
if assistant.Share == "team" && authInfo.Constraints.TeamOnly {
return true, nil
}
}
// Check if user is the creator - always allow creator to access their own assistant
if assistant.YaoCreatedBy != "" && assistant.YaoCreatedBy == authInfo.UserID {
return true, nil
}
// Combined Team and Owner permission validation
if authInfo.Constraints.TeamOnly && authInfo.Constraints.OwnerOnly {
if assistant.YaoTeamID != "" && assistant.YaoTeamID == authInfo.TeamID {
return true, nil
}
return false, nil
}
// Team only permission validation
if authInfo.Constraints.TeamOnly && assistant.YaoTeamID != "" && assistant.YaoTeamID == authInfo.TeamID {
return true, nil
}
// Owner only permission validation (already handled above by creator check)
if authInfo.Constraints.OwnerOnly {
return false, nil
}
return false, fmt.Errorf("no permission to access assistant: %s", assistantID)
}