- Added ExecuteWithAuthorized method to the Script struct, allowing scripts to be executed with user authorization information. - Updated existing Execute method to utilize ExecuteWithAuthorized for improved authorization handling. - Enhanced script execution tests to verify behavior with and without authorized information, ensuring accurate context management. - Implemented authorized information conversion to a map for easier integration with runtime environments. - Refactored context handling in script execution to ensure authorized data is correctly passed and utilized.
373 lines
10 KiB
Go
373 lines
10 KiB
Go
package kb
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/gou/process"
|
|
"github.com/yaoapp/kun/exception"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/kb"
|
|
kbapi "github.com/yaoapp/yao/kb/api"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
// ProcessCreateCollection creates a new collection via Yao process
|
|
// Process: kb.collection.Create
|
|
//
|
|
// Args[0]: params (map) - Collection creation parameters
|
|
//
|
|
// {
|
|
// "id": "collection_id",
|
|
// "metadata": {
|
|
// "name": "Collection Name",
|
|
// "description": "Description"
|
|
// },
|
|
// "embedding_provider_id": "__yao.openai",
|
|
// "embedding_option_id": "text-embedding-3-small",
|
|
// "locale": "en",
|
|
// "config": {
|
|
// "distance": "cosine",
|
|
// "index_type": "hnsw",
|
|
// "m": 16,
|
|
// "ef_construction": 200,
|
|
// "ef_search": 64
|
|
// }
|
|
// }
|
|
//
|
|
// Returns: map with collection_id and message
|
|
func ProcessCreateCollection(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
// Get authorized info from process
|
|
authInfo := authorized.ProcessAuthInfo(process)
|
|
|
|
// Parse parameters using JSON for type safety
|
|
paramsJSON, err := json.Marshal(process.Args[0])
|
|
if err != nil {
|
|
exception.New("Failed to encode parameters: "+err.Error(), 400).Throw()
|
|
}
|
|
|
|
var params kbapi.CreateCollectionParams
|
|
if err := json.Unmarshal(paramsJSON, ¶ms); err != nil {
|
|
exception.New("Failed to decode parameters: "+err.Error(), 400).Throw()
|
|
}
|
|
|
|
// Apply auth scope from authorized info
|
|
if authInfo != nil {
|
|
authScope := authInfo.WithCreateScope(maps.MapStrAny{})
|
|
params.AuthScope = authScope
|
|
}
|
|
|
|
// Call API
|
|
result, err := kb.API.CreateCollection(process.Context, ¶ms)
|
|
if err != nil {
|
|
log.Error("Failed to create collection: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return maps.MapStrAny{
|
|
"collection_id": result.CollectionID,
|
|
"message": result.Message,
|
|
}
|
|
}
|
|
|
|
// ProcessRemoveCollection removes a collection via Yao process
|
|
// Process: kb.collection.Remove
|
|
//
|
|
// Args[0]: collection_id (string) - Collection ID to remove
|
|
//
|
|
// Returns: map with collection_id, removed status, documents_removed count, and message
|
|
func ProcessRemoveCollection(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
// Get authorized info from process
|
|
authInfo := authorized.ProcessAuthInfo(process)
|
|
|
|
collectionID := process.ArgsString(0)
|
|
if collectionID == "" {
|
|
exception.New("Collection ID is required", 400).Throw()
|
|
}
|
|
|
|
// Check remove permission
|
|
hasPermission, err := checkCollectionPermission(authInfo, collectionID)
|
|
if err != nil {
|
|
exception.New(err.Error(), 403).Throw()
|
|
}
|
|
|
|
if !hasPermission {
|
|
exception.New("Forbidden: No permission to remove collection", 403).Throw()
|
|
}
|
|
|
|
result, err := kb.API.RemoveCollection(process.Context, collectionID)
|
|
if err != nil {
|
|
log.Error("Failed to remove collection: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return maps.MapStrAny{
|
|
"collection_id": result.CollectionID,
|
|
"removed": result.Removed,
|
|
"documents_removed": result.DocumentsRemoved,
|
|
"message": result.Message,
|
|
}
|
|
}
|
|
|
|
// ProcessGetCollection retrieves a collection by ID via Yao process
|
|
// Process: kb.collection.Get
|
|
//
|
|
// Args[0]: collection_id (string) - Collection ID to retrieve
|
|
//
|
|
// Returns: map containing collection details
|
|
func ProcessGetCollection(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
collectionID := process.ArgsString(0)
|
|
if collectionID == "" {
|
|
exception.New("Collection ID is required", 400).Throw()
|
|
}
|
|
|
|
collection, err := kb.API.GetCollection(process.Context, collectionID)
|
|
if err != nil {
|
|
log.Error("Failed to get collection: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return collection
|
|
}
|
|
|
|
// ProcessCollectionExists checks if a collection exists via Yao process
|
|
// Process: kb.collection.Exists
|
|
//
|
|
// Args[0]: collection_id (string) - Collection ID to check
|
|
//
|
|
// Returns: map with collection_id and exists status
|
|
func ProcessCollectionExists(process *process.Process) interface{} {
|
|
process.ValidateArgNums(1)
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
collectionID := process.ArgsString(0)
|
|
if collectionID == "" {
|
|
exception.New("Collection ID is required", 400).Throw()
|
|
}
|
|
|
|
result, err := kb.API.CollectionExists(process.Context, collectionID)
|
|
if err != nil {
|
|
log.Error("Failed to check collection existence: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return maps.MapStrAny{
|
|
"collection_id": result.CollectionID,
|
|
"exists": result.Exists,
|
|
}
|
|
}
|
|
|
|
// ProcessListCollections lists collections with pagination via Yao process
|
|
// Process: kb.collection.List
|
|
//
|
|
// Args[0]: filter (map) - Optional filter parameters
|
|
//
|
|
// {
|
|
// "page": 1,
|
|
// "pagesize": 20,
|
|
// "keywords": "search term",
|
|
// "status": ["active"],
|
|
// "embedding_provider_id": "__yao.openai",
|
|
// "system": false,
|
|
// "select": ["id", "name", "status"],
|
|
// "sort": [{"column": "created_at", "option": "desc"}]
|
|
// }
|
|
//
|
|
// Returns: map with data array and pagination info
|
|
func ProcessListCollections(process *process.Process) interface{} {
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
// Get authorized info from process
|
|
authInfo := authorized.ProcessAuthInfo(process)
|
|
|
|
// Default filter
|
|
filter := &kbapi.ListCollectionsFilter{
|
|
Page: kbapi.DefaultPage,
|
|
PageSize: kbapi.DefaultPageSize,
|
|
}
|
|
|
|
// Parse filter parameters using JSON (optional)
|
|
if process.NumOfArgs() > 0 {
|
|
filterJSON, err := json.Marshal(process.Args[0])
|
|
if err != nil {
|
|
exception.New("Failed to encode filter: "+err.Error(), 400).Throw()
|
|
}
|
|
|
|
if err := json.Unmarshal(filterJSON, filter); err != nil {
|
|
exception.New("Failed to decode filter: "+err.Error(), 400).Throw()
|
|
}
|
|
}
|
|
|
|
// Apply auth filters from authorized info
|
|
if authInfo != nil {
|
|
filter.AuthFilters = processAuthFilter(authInfo)
|
|
}
|
|
|
|
result, err := kb.API.ListCollections(process.Context, filter)
|
|
if err != nil {
|
|
log.Error("Failed to list collections: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return maps.MapStrAny{
|
|
"data": result.Data,
|
|
"next": result.Next,
|
|
"prev": result.Prev,
|
|
"page": result.Page,
|
|
"pagesize": result.PageSize,
|
|
"total": result.Total,
|
|
"pagecnt": result.PageCnt,
|
|
}
|
|
}
|
|
|
|
// ProcessUpdateCollectionMetadata updates collection metadata via Yao process
|
|
// Process: kb.collection.UpdateMetadata
|
|
//
|
|
// Args[0]: collection_id (string) - Collection ID
|
|
// Args[1]: params (map) - Update parameters
|
|
//
|
|
// {
|
|
// "metadata": {
|
|
// "name": "New Name",
|
|
// "description": "New Description"
|
|
// }
|
|
// }
|
|
//
|
|
// Returns: map with collection_id and message
|
|
func ProcessUpdateCollectionMetadata(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
if kb.API == nil {
|
|
exception.New("Knowledge base not initialized", 500).Throw()
|
|
}
|
|
|
|
// Get authorized info from process
|
|
authInfo := authorized.ProcessAuthInfo(process)
|
|
|
|
collectionID := process.ArgsString(0)
|
|
if collectionID == "" {
|
|
exception.New("Collection ID is required", 400).Throw()
|
|
}
|
|
|
|
// Check update permission
|
|
hasPermission, err := checkCollectionPermission(authInfo, collectionID)
|
|
if err != nil {
|
|
exception.New(err.Error(), 403).Throw()
|
|
}
|
|
|
|
if !hasPermission {
|
|
exception.New("Forbidden: No permission to update collection", 403).Throw()
|
|
}
|
|
|
|
// Parse parameters using JSON
|
|
paramsJSON, err := json.Marshal(process.Args[1])
|
|
if err != nil {
|
|
exception.New("Failed to encode parameters: "+err.Error(), 400).Throw()
|
|
}
|
|
|
|
var params kbapi.UpdateMetadataParams
|
|
if err := json.Unmarshal(paramsJSON, ¶ms); err != nil {
|
|
exception.New("Failed to decode parameters: "+err.Error(), 400).Throw()
|
|
}
|
|
|
|
if len(params.Metadata) == 0 {
|
|
exception.New("Metadata is required and cannot be empty", 400).Throw()
|
|
}
|
|
|
|
// Apply auth scope from authorized info
|
|
if authInfo != nil {
|
|
authScope := authInfo.WithUpdateScope(maps.MapStrAny{})
|
|
params.AuthScope = authScope
|
|
}
|
|
|
|
result, err := kb.API.UpdateCollectionMetadata(process.Context, collectionID, ¶ms)
|
|
if err != nil {
|
|
log.Error("Failed to update collection metadata: %v", err)
|
|
exception.New(err.Error(), 500).Throw()
|
|
}
|
|
|
|
return maps.MapStrAny{
|
|
"collection_id": result.CollectionID,
|
|
"message": result.Message,
|
|
}
|
|
}
|
|
|
|
// Helper functions for Process handlers
|
|
|
|
// processAuthFilter applies permission-based filtering to query wheres for process handlers
|
|
// This function builds where clauses based on the user's authorization constraints
|
|
func processAuthFilter(authInfo *oauthtypes.AuthorizedInfo) []model.QueryWhere {
|
|
if authInfo == nil {
|
|
return []model.QueryWhere{}
|
|
}
|
|
|
|
var wheres []model.QueryWhere
|
|
scope := authInfo.AccessScope()
|
|
|
|
// Team only - User can access:
|
|
// 1. Public records (public = true)
|
|
// 2. Records in their team where:
|
|
// - They created the record (__yao_created_by matches)
|
|
// - OR the record is shared with team (share = "team")
|
|
if authInfo.Constraints.TeamOnly && authInfo.TeamID != "" && authInfo.UserID != "" {
|
|
wheres = append(wheres, model.QueryWhere{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "public", Value: true, Method: "orwhere"},
|
|
{Wheres: []model.QueryWhere{
|
|
{Column: "__yao_team_id", Value: scope.TeamID},
|
|
{Wheres: []model.QueryWhere{
|
|
{Column: "__yao_created_by", Value: scope.CreatedBy},
|
|
{Column: "share", Value: "team", Method: "orwhere"},
|
|
}},
|
|
}, Method: "orwhere"},
|
|
},
|
|
})
|
|
return wheres
|
|
}
|
|
|
|
// Owner only - User can access:
|
|
// 1. Public records (public = true)
|
|
// 2. Records they created where:
|
|
// - __yao_team_id is null (not team records)
|
|
// - __yao_created_by matches their user ID
|
|
if authInfo.Constraints.OwnerOnly && authInfo.UserID != "" {
|
|
wheres = append(wheres, model.QueryWhere{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "public", Value: true, Method: "orwhere"},
|
|
{Wheres: []model.QueryWhere{
|
|
{Column: "__yao_team_id", OP: "null"},
|
|
{Column: "__yao_created_by", Value: scope.CreatedBy},
|
|
}, Method: "orwhere"},
|
|
},
|
|
})
|
|
return wheres
|
|
}
|
|
|
|
return wheres
|
|
}
|