yao/neo/api.go
Max d8f267694d Refactor API handler functions for assistants in Neo package
- Updated the assistant-related API handler functions in neo/api.go to follow Go naming conventions by capitalizing the function names, making them exported for use in openapi/agent.
- This change enhances code readability and consistency across the API implementation.
- Added new LLM and MCP server management handlers in their respective packages, improving the overall structure and organization of the OpenAPI module.
- Removed the obsolete agent.go file, streamlining the codebase.
2025-10-25 16:02:01 +08:00

1368 lines
36 KiB
Go

package neo
import (
"fmt"
"io"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yaoapp/gou/api"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/attachment"
"github.com/yaoapp/yao/helper"
"github.com/yaoapp/yao/neo/assistant"
chatctx "github.com/yaoapp/yao/neo/context"
"github.com/yaoapp/yao/neo/message"
"github.com/yaoapp/yao/neo/store"
"github.com/yaoapp/yao/openapi/oauth"
)
// API registers the Neo API endpoints
func (neo *DSL) API(router *gin.Engine, path string) error {
// Get the guards
middlewares, err := neo.getGuardHandlers()
if err != nil {
return err
}
// Register OPTIONS handlers for all endpoints
router.OPTIONS(path, neo.optionsHandler)
router.OPTIONS(path+"/status", neo.optionsHandler)
router.OPTIONS(path+"/chats", neo.optionsHandler)
router.OPTIONS(path+"/chats/:id", neo.optionsHandler)
router.OPTIONS(path+"/history", neo.optionsHandler)
router.OPTIONS(path+"/upload/:storage", neo.optionsHandler)
router.OPTIONS(path+"/download", neo.optionsHandler)
router.OPTIONS(path+"/mentions", neo.optionsHandler)
router.OPTIONS(path+"/generate", neo.optionsHandler)
router.OPTIONS(path+"/generate/title", neo.optionsHandler)
router.OPTIONS(path+"/generate/prompts", neo.optionsHandler)
router.OPTIONS(path+"/dangerous/clear_chats", neo.optionsHandler)
router.OPTIONS(path+"/assistants", neo.optionsHandler)
router.OPTIONS(path+"/assistants/:id", neo.optionsHandler)
router.OPTIONS(path+"/assistants/:id/call", neo.optionsHandler)
// Chat endpoint
// Chat endpoint
// Example:
// curl -X GET 'http://localhost:5099/api/__yao/neo?content=Hello&chat_id=chat_123&context=previous_context&token=xxx'
// curl -X POST 'http://localhost:5099/api/__yao/neo' \
// -H 'Content-Type: application/json' \
// -d '{"content": "Hello", "chat_id": "chat_123", "context": "previous_context", "token": "xxx"}'
router.GET(path, append(middlewares, neo.handleChat)...)
router.POST(path, append(middlewares, neo.handleChat)...)
// Status check endpoint
// Example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/status?token=xxx'
router.GET(path+"/status", append(middlewares, neo.handleStatus)...)
// Assistant API endpoints
// List assistants example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants?page=1&pagesize=20&tags=tag1,tag2&token=xxx'
router.GET(path+"/assistants", append(middlewares, neo.HandleAssistantList)...)
// Get all assistant tags example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants/tags?token=xxx'
router.GET(path+"/assistants/tags", append(middlewares, neo.HandleAssistantTags)...)
// Get assistant details example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants/assistant_123?token=xxx'
router.GET(path+"/assistants/:id", append(middlewares, neo.HandleAssistantDetail)...)
// Execute assistant API example:
// curl -X POST 'http://localhost:5099/api/__yao/neo/assistants/assistant_123/api' \
// -H 'Content-Type: application/json' \
// -d '{"name": "Test", "payload": {"name": "yao", "age": 18}}'
router.POST(path+"/assistants/:id/call", append(middlewares, neo.HandleAssistantCall)...)
// Create/Update assistant example:
// curl -X POST 'http://localhost:5099/api/__yao/neo/assistants' \
// -H 'Content-Type: application/json' \
// -d '{"name": "My Assistant", "type": "chat", "tags": ["tag1", "tag2"], "mentionable": true, "avatar": "path/to/avatar.png", "token": "xxx"}'
router.POST(path+"/assistants", append(middlewares, neo.HandleAssistantSave)...)
// Delete assistant example:
// curl -X DELETE 'http://localhost:5099/api/__yao/neo/assistants/assistant_123?token=xxx'
router.DELETE(path+"/assistants/:id", append(middlewares, neo.HandleAssistantDelete)...)
// Chat management endpoints
// List chats example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/chats?page=1&pagesize=20&keywords=search+term&order=desc&token=xxx'
router.GET(path+"/chats", append(middlewares, neo.handleChatList)...)
// Get latest chat example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/chats/latest?assistant_id=assistant_123&token=xxx'
router.GET(path+"/chats/latest", append(middlewares, neo.handleChatLatest)...)
// Get chat details example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/chats/chat_123?token=xxx'
router.GET(path+"/chats/:id", append(middlewares, neo.handleChatDetail)...)
// Update chat example:
// curl -X POST 'http://localhost:5099/api/__yao/neo/chats/chat_123' \
// -H 'Content-Type: application/json' \
// -d '{"title": "New Title", "content": "Chat content for title generation", "token": "xxx"}'
router.POST(path+"/chats/:id", append(middlewares, neo.handleChatUpdate)...)
// Delete chat example:
// curl -X DELETE 'http://localhost:5099/api/__yao/neo/chats/chat_123?token=xxx'
router.DELETE(path+"/chats/:id", append(middlewares, neo.handleChatDelete)...)
// Chat history endpoint
// Example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/history?chat_id=chat_123&token=xxx'
router.GET(path+"/history", append(middlewares, neo.handleChatHistory)...)
// File management endpoints
// Upload file example:
// curl -X POST 'http://localhost:5099/api/__yao/neo/upload?chat_id=chat_123&token=xxx' \
// -F 'file=@/path/to/file.txt'
router.POST(path+"/upload/:storage", append(middlewares, neo.handleUpload)...)
// Download file example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/download?file_id=file_123&disposition=attachment&token=xxx' \
// -o downloaded_file.txt
router.GET(path+"/download", append(middlewares, neo.handleDownload)...)
// Mentions endpoint
// Example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/mentions?keywords=assistant&token=xxx'
router.GET(path+"/mentions", append(middlewares, neo.handleMentions)...)
// Generate title example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/generate/title?content=Chat+content&chat_id=chat_123&token=xxx'
// curl -X POST 'http://localhost:5099/api/__yao/neo/generate/title' \
// -H 'Content-Type: application/json' \
// -d '{"content": "Chat content", "chat_id": "chat_123", "token": "xxx"}'
router.GET(path+"/generate/title", append(middlewares, neo.handleGenerateTitle)...)
router.POST(path+"/generate/title", append(middlewares, neo.handleGenerateTitle)...)
// Generate prompts example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/generate/prompts?content=Generate+prompts&chat_id=chat_123&token=xxx'
// curl -X POST 'http://localhost:5099/api/__yao/neo/generate/prompts' \
// -H 'Content-Type: application/json' \
// -d '{"content": "Generate prompts", "chat_id": "chat_123", "token": "xxx"}'
router.GET(path+"/generate/prompts", append(middlewares, neo.handleGeneratePrompts)...)
router.POST(path+"/generate/prompts", append(middlewares, neo.handleGeneratePrompts)...)
// Utility endpoints
// List connectors example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/utility/connectors?token=xxx'
router.GET(path+"/utility/connectors", append(middlewares, neo.handleConnectors)...)
// Dangerous operations
// Dangerous operations
// Clear all chats example:
// curl -X DELETE 'http://localhost:5099/api/__yao/neo/dangerous/clear_chats?token=xxx'
router.DELETE(path+"/dangerous/clear_chats", append(middlewares, neo.handleChatsDeleteAll)...)
return nil
}
// handleStatus handles the status request
func (neo *DSL) handleStatus(c *gin.Context) {
c.Status(200)
c.Done()
}
// handleUpload handles the upload request
func (neo *DSL) handleUpload(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
sid = uuid.New().String()
}
uid, isGuest, err := neo.UserOrGuestID(sid)
if err != nil {
c.JSON(401, gin.H{"message": fmt.Sprintf("Unauthorized, %s", err.Error()), "code": 401})
c.Done()
return
}
if uid == nil || uid == "" {
c.JSON(401, gin.H{"message": "Unauthorized", "code": 401})
c.Done()
return
}
// Storage name must be chat, knowledge or assets
storage := c.Param("storage")
if storage != "chat" && storage != "knowledge" && storage != "assets" {
c.JSON(400, gin.H{"message": "Invalid storage", "code": 400})
c.Done()
return
}
// Get the manager
var manager, ok = attachment.Managers[storage]
if !ok {
c.JSON(400, gin.H{"message": "Invalid storage: " + storage, "code": 400})
c.Done()
return
}
// Get Option from form data
var option UploadOption
err = c.ShouldBind(&option)
if err != nil {
c.JSON(400, gin.H{"message": err.Error(), "code": 400})
c.Done()
return
}
// Validate the option with the storage
option.UserID = fmt.Sprintf("%v", uid)
// Build multi-level groups based on storage type and IDs
var groups []string
switch storage {
case "chat":
if option.ChatID == "" {
c.JSON(400, gin.H{"message": "chat_id is required", "code": 400})
c.Done()
return
}
// Build groups: ["users", "user123", "chats", "chat456"]
groups = []string{"users", option.UserID, "chats", option.ChatID}
if option.AssistantID != "" {
// Add assistant level: ["users", "user123", "chats", "chat456", "assistants", "assistant789"]
groups = append(groups, "assistants", option.AssistantID)
}
case "knowledge":
if option.CollectionID == "" {
c.JSON(400, gin.H{"message": "collection_id is required", "code": 400})
c.Done()
return
}
// Build groups: ["knowledge", "collection123", "users", "user456"]
groups = []string{"knowledge", option.CollectionID, "users", option.UserID}
case "assets":
// Build groups: ["assets", "users", "user123"]
groups = []string{"assets", "users", option.UserID}
}
// Set the groups in the attachment upload option
option.UploadOption.Groups = groups
// Get the file
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"message": err.Error(), "code": 400})
c.Done()
return
}
// Open the file
reader, err := file.Open()
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
defer func() {
reader.Close()
os.Remove(file.Filename)
}()
// Upload the file
header := attachment.GetHeader(c.Request.Header, file.Header, file.Size)
res, err := manager.Upload(c.Request.Context(), header, reader, option.UploadOption)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// if storage is chat or knowledge, save the file to the store
if storage == "chat" || storage == "knowledge" {
attachment := map[string]interface{}{
"file_id": res.ID,
"uid": uid,
"guest": isGuest,
"manager": storage,
"public": option.Public,
"name": option.OriginalFilename,
"content_type": res.ContentType,
"bytes": res.Bytes,
"gzip": option.Gzip,
"status": res.Status,
}
// Set the scope
if option.Scope != nil {
attachment["scope"] = option.Scope
}
// Set the collection_id
if option.CollectionID != "" {
attachment["collection_id"] = option.CollectionID
}
_, err = neo.Store.SaveAttachment(attachment)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
}
c.JSON(200, map[string]interface{}{"data": res})
c.Done()
}
// handleDownload handles the download request
func (neo *DSL) handleDownload(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
uid, _, err := neo.UserOrGuestID(sid)
if err != nil {
c.JSON(401, gin.H{"message": fmt.Sprintf("Unauthorized, %s", err.Error()), "code": 401})
c.Done()
return
}
if uid == nil || uid == "" {
c.JSON(401, gin.H{"message": "Unauthorized", "code": 401})
c.Done()
return
}
fileID := c.Query("file_id")
if fileID == "" {
c.JSON(400, gin.H{"message": "file_id is required", "code": 400})
c.Done()
return
}
// Get the attachment
attach, err := neo.Store.GetAttachment(fileID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Validate the permission ( Will be supported scope validation in the future )
if (attach["public"] == 0 || attach["public"] == false) && attach["uid"] != uid {
c.JSON(403, gin.H{"message": "Forbidden", "code": 403})
c.Done()
return
}
storage, ok := attach["manager"].(string)
if !ok {
c.JSON(400, gin.H{"message": "Invalid storage", "code": 400})
c.Done()
return
}
// Get the manager
manager, ok := attachment.Managers[storage]
if !ok {
c.JSON(400, gin.H{"message": "Invalid storage", "code": 400})
c.Done()
return
}
name, ok := attach["name"].(string)
if !ok {
c.JSON(400, gin.H{"message": "Invalid name", "code": 400})
c.Done()
return
}
name = strings.TrimSuffix(name, ".gz")
contentType, ok := attach["content_type"].(string)
if !ok {
c.JSON(400, gin.H{"message": "Invalid content type", "code": 400})
c.Done()
return
}
handle, err := manager.Download(c.Request.Context(), fileID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
defer handle.Reader.Close()
// Set the response headers
encoded := url.PathEscape(name)
disposition := fmt.Sprintf(`attachment; filename="%s"`, encoded)
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", disposition)
// Copy the file content to response
_, err = io.Copy(c.Writer, handle.Reader)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
return
}
c.Done()
}
// handleChat handles the chat request
func (neo *DSL) handleChat(c *gin.Context) {
// Set headers for SSE
c.Header("Content-Type", "text/event-stream;charset=utf-8")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
sid := c.GetString("__sid")
if sid == "" {
sid = uuid.New().String()
}
content := c.Query("content")
if content == "" {
msg := message.New().Error("content is required").Done()
msg.Write(c.Writer)
return
}
chatID := c.Query("chat_id")
if chatID == "" {
// Only generate new chat_id if not provided
chatID = fmt.Sprintf("chat_%d", time.Now().UnixNano())
}
// Set the context with validated chat_id
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
defer cancel()
defer ctx.Release() // Release the context after the request is done
// Set the assistant ID
assistantID := c.Query("assistant_id")
if assistantID != "" {
ctx = chatctx.WithAssistantID(ctx, assistantID)
}
// Set the silent mode
silent := c.Query("silent")
if silent == "true" || silent == "1" {
ctx = chatctx.WithSilent(ctx, true)
}
// Set the history visible
historyVisible := c.Query("history_visible")
if historyVisible != "" {
ctx = chatctx.WithHistoryVisible(ctx, historyVisible == "true" || historyVisible == "1")
}
// Set the client type
clientType := c.Query("client_type")
if clientType != "" {
ctx = chatctx.WithClientType(ctx, clientType)
}
err := neo.Answer(ctx, content, c)
// Error handling
if err != nil {
message.New().Done().Error(err).Write(c.Writer)
c.Done()
return
}
}
// handleChatList handles the chat list request
func (neo *DSL) handleChatList(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
// Create filter from query parameters
filter := store.ChatFilter{
Keywords: c.Query("keywords"),
Order: c.Query("order"),
}
// Parse page and pagesize
if page := c.Query("page"); page != "" {
if n, err := strconv.Atoi(page); err == nil {
filter.Page = n
}
}
if pageSize := c.Query("pagesize"); pageSize != "" {
if n, err := strconv.Atoi(pageSize); err == nil {
filter.PageSize = n
}
}
locale := "en-us"
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
response, err := neo.Store.GetChats(sid, filter, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, map[string]interface{}{"data": response})
c.Done()
}
// handleChatHistory handles the chat history request
func (neo *DSL) handleChatHistory(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
cid := c.Query("chat_id")
history, err := neo.Store.GetHistory(sid, cid)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, map[string]interface{}{"data": history})
c.Done()
}
// getCorsHandlers returns CORS middleware handlers
func (neo *DSL) getCorsHandlers() ([]gin.HandlerFunc, error) {
if len(neo.Allows) == 0 {
return []gin.HandlerFunc{}, nil
}
allowsMap := map[string]bool{}
for _, allow := range neo.Allows {
allow = strings.TrimPrefix(allow, "http://")
allow = strings.TrimPrefix(allow, "https://")
allowsMap[allow] = true
}
return []gin.HandlerFunc{neo.corsMiddleware(allowsMap)}, nil
}
// corsMiddleware handles CORS requests
func (neo *DSL) corsMiddleware(allowsMap map[string]bool) gin.HandlerFunc {
return func(c *gin.Context) {
origin := neo.getOrigin(c)
if origin == "" {
c.Next()
return
}
// Check if origin is allowed
if !api.IsAllowed(c, allowsMap) {
c.AbortWithStatusJSON(403, gin.H{
"message": origin + " not allowed",
"code": 403,
})
return
}
// Set CORS headers
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Disposition, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, Origin, Cache-Control, X-Requested-With, Content-Sync, Content-Fingerprint, Content-Uid, Content-Range")
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Header("Access-Control-Expose-Headers", "Content-Type, Content-Disposition, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, Origin, Cache-Control, X-Requested-With, Content-Sync, Content-Fingerprint, Content-Uid, Content-Range")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// optionsHandler handles OPTIONS requests
func (neo *DSL) optionsHandler(c *gin.Context) {
origin := neo.getOrigin(c)
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Disposition, Authorization, Accept, Content-Sync, Content-Fingerprint, Content-Uid, Content-Range")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Max-Age", "86400") // 24 hours
c.Header("Access-Control-Expose-Headers", "Content-Type, Content-Disposition, Authorization, Accept, Content-Sync, Content-Fingerprint, Content-Uid, Content-Range")
}
c.AbortWithStatus(204)
}
// getOrigin returns the request origin
func (neo *DSL) getOrigin(c *gin.Context) string {
origin := c.Request.Header.Get("Origin")
if origin == "" {
origin = c.Request.Referer()
if origin != "" {
if u, err := url.Parse(origin); err == nil {
origin = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
}
}
return origin
}
// getGuardHandlers returns authentication middleware handlers
func (neo *DSL) getGuardHandlers() ([]gin.HandlerFunc, error) {
// Cross-Domain handlers
cors, err := neo.getCorsHandlers()
if err != nil {
return nil, err
}
if neo.Guard == "" {
middlewares := append(cors, neo.defaultGuard)
return middlewares, nil
}
// Validate the custom guard
_, err = process.Of(neo.Guard)
if err != nil {
return nil, err
}
middlewares := append(cors, api.ProcessGuard(neo.Guard, cors...))
return middlewares, nil
}
// defaultGuard is the default authentication handler
func (neo *DSL) defaultGuard(c *gin.Context) {
// Check if the request is for OpenAPI OAuth
if oauth.OAuth != nil {
neo.guardOpenapiOauth(c)
return
}
token := strings.TrimSpace(strings.TrimPrefix(c.Query("token"), "Bearer "))
if token == "" {
c.JSON(403, gin.H{"message": "token is required", "code": 403})
c.Abort()
return
}
user := helper.JwtValidate(token)
c.Set("__sid", user.SID)
c.Next()
}
// Openapi Oauth
func (neo *DSL) guardOpenapiOauth(c *gin.Context) {
s := oauth.OAuth
token := neo.getAccessToken(c)
if token == "" {
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
c.Abort()
return
}
// Validate the token
_, err := s.VerifyToken(token)
if err != nil {
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
c.Abort()
return
}
// Get the session ID
sid := neo.getSessionID(c)
if sid == "" {
c.JSON(403, gin.H{"code": 403, "message": "Not Authorized"})
c.Abort()
return
}
c.Set("__sid", sid)
}
func (neo *DSL) getAccessToken(c *gin.Context) string {
token := c.GetHeader("Authorization")
if token == "" || token == "Bearer undefined" {
cookie, err := c.Cookie("__Host-access_token")
if err != nil {
return ""
}
token = cookie
}
return strings.TrimPrefix(token, "Bearer ")
}
func (neo *DSL) getSessionID(c *gin.Context) string {
sid, err := c.Cookie("__Host-session_id")
if err != nil {
return ""
}
return sid
}
// handleChatLatest handles getting the latest chat
func (neo *DSL) handleChatLatest(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
locale := "en-us"
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
// Get the chats
chats, err := neo.Store.GetChats(sid, store.ChatFilter{Page: 1}, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Create a new chat
if len(chats.Groups) == 0 || len(chats.Groups[0].Chats) == 0 {
assistantID := neo.Use.Default
queryAssistantID := c.Query("assistant_id")
if queryAssistantID != "" {
assistantID = queryAssistantID
}
// Get the assistant info
ast, err := assistant.Get(assistantID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, map[string]interface{}{"data": map[string]interface{}{
"placeholder": ast.GetPlaceholder(locale),
"assistant_id": ast.ID,
"assistant_name": ast.GetName(locale),
"assistant_avatar": ast.Avatar,
"assistant_deleteable": neo.Use.Default != ast.ID,
}})
c.Done()
return
}
// Get the chat_id
chatID, ok := chats.Groups[0].Chats[0]["chat_id"].(string)
if !ok {
c.JSON(404, gin.H{"message": "chat_id not found", "code": 404})
c.Done()
return
}
chat, err := neo.Store.GetChat(sid, chatID, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// assistant_id is nil return the default assistant
if chat.Chat["assistant_id"] == nil {
chat.Chat["assistant_id"] = neo.Use.Default
// Get the assistant info
ast, err := assistant.Get(neo.Use.Default)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
chat.Chat["assistant_name"] = ast.GetName(locale)
chat.Chat["assistant_avatar"] = ast.Avatar
}
chat.Chat["assistant_deleteable"] = neo.Use.Default != chat.Chat["assistant_id"]
c.JSON(200, map[string]interface{}{"data": chat})
c.Done()
}
// handleChatDetail handles getting a single chat's details
func (neo *DSL) handleChatDetail(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
chatID := c.Param("id")
if chatID == "" {
c.JSON(400, gin.H{"message": "chat id is required", "code": 400})
c.Done()
return
}
locale := "en-us"
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
// Get the chat details
chat, err := neo.Store.GetChat(sid, chatID, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// assistant_id is nil return the default assistant
if chat.Chat["assistant_id"] == nil {
chat.Chat["assistant_id"] = neo.Use.Default
// Get the assistant info
ast, err := assistant.Get(neo.Use.Default)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
chat.Chat["assistant_name"] = ast.GetName(locale)
chat.Chat["assistant_avatar"] = ast.Avatar
}
chat.Chat["assistant_deleteable"] = neo.Use.Default != chat.Chat["assistant_id"]
c.JSON(200, map[string]interface{}{"data": chat})
c.Done()
}
// handleMentions handles getting mentions for a chat
func (neo *DSL) handleMentions(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
locale := "en-us"
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
// Get keywords from query parameter
keywords := strings.ToLower(c.Query("keywords"))
mentionable := true
// Query mentionable assistants
filter := store.AssistantFilter{
Keywords: keywords,
Mentionable: &mentionable,
Page: 1,
PageSize: 20,
}
response, err := neo.Store.GetAssistants(filter, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Convert assistants to mentions
mentions := []Mention{}
for _, item := range response.Data {
mention := Mention{
ID: item["assistant_id"].(string),
Name: item["name"].(string),
Type: item["type"].(string),
Avatar: item["avatar"].(string),
}
mentions = append(mentions, mention)
}
c.JSON(200, map[string]interface{}{"data": mentions})
c.Done()
}
// handleChatUpdate handles updating a chat's details
func (neo *DSL) handleChatUpdate(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
chatID := c.Param("id")
if chatID == "" {
c.JSON(400, gin.H{"message": "chat id is required", "code": 400})
c.Done()
return
}
// Get title from request body
var body struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := c.BindJSON(&body); err != nil {
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
c.Done()
return
}
if body.Title == "" {
c.JSON(400, gin.H{"message": "title is required", "code": 400})
c.Done()
return
}
err := neo.Store.UpdateChatTitle(sid, chatID, body.Title)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, gin.H{"message": "ok", "title": body.Title, "chat_id": chatID})
c.Done()
}
// handleChatDelete handles deleting a single chat
func (neo *DSL) handleChatDelete(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
chatID := c.Param("id")
if chatID == "" {
c.JSON(400, gin.H{"message": "chat id is required", "code": 400})
c.Done()
return
}
err := neo.Store.DeleteChat(sid, chatID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, gin.H{"message": "ok"})
c.Done()
}
// handleChatsDeleteAll handles deleting all chats for a user
func (neo *DSL) handleChatsDeleteAll(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
err := neo.Store.DeleteAllChats(sid)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, gin.H{"message": "ok"})
c.Done()
}
// handleGenerateTitle handles generating a chat title
func (neo *DSL) handleGenerateTitle(c *gin.Context) {
// Set headers for SSE
c.Header("Content-Type", "text/event-stream;charset=utf-8")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
sid := c.GetString("__sid")
if sid == "" {
sid = uuid.New().String()
}
content := c.Query("content")
if content == "" {
msg := message.New().Error("content is required").Done()
msg.Write(c.Writer)
return
}
chatID := fmt.Sprintf("generate_title_%d", time.Now().UnixNano())
// Set the context with validated chat_id
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
defer cancel()
defer ctx.Release() // Release the context after the request is done
// Set the assistant ID
ctx = chatctx.WithHistoryVisible(ctx, false)
ctx = chatctx.WithAssistantID(ctx, neo.Use.Title)
err := neo.Answer(ctx, content, c)
// Error handling
if err != nil {
message.New().Done().Error(err).Write(c.Writer)
c.Done()
return
}
}
// handleGeneratePrompts handles generating prompts
func (neo *DSL) handleGeneratePrompts(c *gin.Context) {
// Set headers for SSE
c.Header("Content-Type", "text/event-stream;charset=utf-8")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
sid := c.GetString("__sid")
if sid == "" {
sid = uuid.New().String()
}
content := c.Query("content")
if content == "" {
msg := message.New().Error("content is required").Done()
msg.Write(c.Writer)
return
}
chatID := fmt.Sprintf("generate_prompts_%d", time.Now().UnixNano())
// Set the context with validated chat_id
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
defer cancel()
defer ctx.Release() // Release the context after the request is done
// Set the assistant ID
ctx = chatctx.WithHistoryVisible(ctx, false)
ctx = chatctx.WithAssistantID(ctx, neo.Use.Prompt)
err := neo.Answer(ctx, content, c)
// Error handling
if err != nil {
message.New().Done().Error(err).Write(c.Writer)
c.Done()
return
}
}
// HandleAssistantList handles listing assistants (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantList(c *gin.Context) {
// Parse filter parameters
filter := store.AssistantFilter{
Type: "assistant",
Page: 1,
PageSize: 20,
}
// Parse page and pagesize
if page := c.Query("page"); page != "" {
if n, err := strconv.Atoi(page); err == nil {
filter.Page = n
}
}
if pageSize := c.Query("pagesize"); pageSize != "" {
if n, err := strconv.Atoi(pageSize); err == nil {
filter.PageSize = n
}
}
// Parse tags
if tags := c.Query("tags"); tags != "" {
filter.Tags = strings.Split(tags, ",")
}
// Parse keywords
if keywords := c.Query("keywords"); keywords != "" {
filter.Keywords = keywords
}
// Parse connector
if connector := c.Query("connector"); connector != "" {
filter.Connector = connector
}
// Parse select fields
if selectFields := c.Query("select"); selectFields != "" {
filter.Select = strings.Split(selectFields, ",")
}
// Parse built_in (support various boolean formats)
if builtIn := c.Query("built_in"); builtIn != "" {
val := parseBoolValue(builtIn)
if val != nil {
filter.BuiltIn = val
}
}
// Parse mentionable (support various boolean formats)
if mentionable := c.Query("mentionable"); mentionable != "" {
val := parseBoolValue(mentionable)
if val != nil {
filter.Mentionable = val
}
}
// Parse automated (support various boolean formats)
if automated := c.Query("automated"); automated != "" {
val := parseBoolValue(automated)
if val != nil {
filter.Automated = val
}
}
// Parse assistant_id
if assistantID := c.Query("assistant_id"); assistantID != "" {
filter.AssistantID = assistantID
}
locale := "en-us" // Default locale
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
response, err := neo.Store.GetAssistants(filter, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, response)
c.Done()
}
// parseBoolValue parses various string formats into a boolean pointer
// Supports: 1, 0, "1", "0", "true", "false", etc.
func parseBoolValue(value string) *bool {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "1", "true", "yes", "on":
v := true
return &v
case "0", "false", "no", "off":
v := false
return &v
default:
return nil
}
}
// HandleAssistantCall handles the assistant API call (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantCall(c *gin.Context) {
assistantID := c.Param("id")
if assistantID == "" {
c.JSON(400, gin.H{"message": "assistant id is required", "code": 400})
c.Done()
return
}
ast, err := assistant.Get(assistantID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
payload := assistant.APIPayload{Sid: sid}
if err := c.BindJSON(&payload); err != nil {
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
c.Done()
return
}
result, err := ast.Call(c, payload)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, result)
c.Done()
}
// HandleAssistantDetail handles getting a single assistant's details (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantDetail(c *gin.Context) {
assistantID := c.Param("id")
if assistantID == "" {
c.JSON(400, gin.H{"message": "assistant id is required", "code": 400})
c.Done()
return
}
filter := store.AssistantFilter{
AssistantID: assistantID,
Type: "assistant",
Page: 1,
PageSize: 1,
}
locale := "en-us" // Default locale
// Translate the response
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
response, err := neo.Store.GetAssistants(filter, locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
if len(response.Data) == 0 {
c.JSON(404, gin.H{"message": "assistant not found", "code": 404})
c.Done()
return
}
c.JSON(200, map[string]interface{}{"data": response.Data[0]})
c.Done()
}
// HandleAssistantSave handles creating or updating an assistant (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantSave(c *gin.Context) {
var assistantData map[string]interface{}
if err := c.BindJSON(&assistantData); err != nil {
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
c.Done()
return
}
id, err := neo.Store.SaveAssistant(assistantData)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Update the assistant map with the returned ID if it's not already set
if _, ok := assistantData["assistant_id"]; !ok {
assistantData["assistant_id"] = id
}
// Remove the assistant from cache to ensure fresh data on next load
cache := assistant.GetCache()
if cache != nil {
cache.Remove(id.(string))
}
// Reload the assistant to ensure it's available in cache with updated data
_, err = assistant.Get(id.(string))
if err != nil {
// Just log the error, don't fail the request
fmt.Printf("Error reloading assistant %s: %v\n", id, err)
}
c.JSON(200, gin.H{"message": "ok", "data": assistantData})
c.Done()
}
// HandleAssistantDelete handles deleting an assistant (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantDelete(c *gin.Context) {
assistantID := c.Param("id")
if assistantID == "" {
c.JSON(400, gin.H{"message": "assistant id is required", "code": 400})
c.Done()
return
}
err := neo.Store.DeleteAssistant(assistantID)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Remove the assistant from cache to ensure it's fully deleted
cache := assistant.GetCache()
if cache != nil {
cache.Remove(assistantID)
}
c.JSON(200, gin.H{"message": "ok"})
c.Done()
}
// handleConnectors handles listing connectors
func (neo *DSL) handleConnectors(c *gin.Context) {
options := []map[string]interface{}{}
// Filter and format connectors
for id, conn := range connector.Connectors {
if conn.Is(connector.OPENAI) || conn.Is(connector.MOAPI) {
setting := conn.Setting()
label := setting["label"]
if label == nil || label == "" {
label = setting["name"]
}
if label == nil || label == "" {
label = id
}
options = append(options, map[string]interface{}{
"label": label,
"value": id,
})
}
}
c.JSON(200, gin.H{"data": options})
c.Done()
}
// HandleAssistantTags handles getting all assistant tags (exported for use in openapi/agent)
func (neo *DSL) HandleAssistantTags(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
c.JSON(400, gin.H{"message": "sid is required", "code": 400})
c.Done()
return
}
locale := "en-us" // Default locale
if loc := c.Query("locale"); loc != "" {
locale = strings.ToLower(strings.TrimSpace(loc))
}
tags, err := neo.Store.GetAssistantTags(locale)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, gin.H{"data": tags})
c.Done()
}