commit
d55bfef374
15 changed files with 2496 additions and 401 deletions
571
neo/api.go
571
neo/api.go
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/yaoapp/gou/api"
|
"github.com/yaoapp/gou/api"
|
||||||
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/yao/helper"
|
"github.com/yaoapp/yao/helper"
|
||||||
"github.com/yaoapp/yao/neo/conversation"
|
"github.com/yaoapp/yao/neo/conversation"
|
||||||
|
|
@ -36,32 +37,120 @@ func (neo *DSL) API(router *gin.Engine, path string) error {
|
||||||
router.OPTIONS(path+"/upload", neo.optionsHandler)
|
router.OPTIONS(path+"/upload", neo.optionsHandler)
|
||||||
router.OPTIONS(path+"/download", neo.optionsHandler)
|
router.OPTIONS(path+"/download", neo.optionsHandler)
|
||||||
router.OPTIONS(path+"/mentions", 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+"/dangerous/clear_chats", neo.optionsHandler)
|
||||||
|
router.OPTIONS(path+"/assistants", neo.optionsHandler)
|
||||||
|
router.OPTIONS(path+"/assistants/:id", neo.optionsHandler)
|
||||||
|
|
||||||
// Register endpoints with middlewares
|
// 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.GET(path, append(middlewares, neo.handleChat)...)
|
||||||
router.POST(path, append(middlewares, neo.handleChat)...)
|
router.POST(path, append(middlewares, neo.handleChat)...)
|
||||||
|
|
||||||
// Status check
|
// Status check endpoint
|
||||||
|
// Example:
|
||||||
|
// curl -X GET 'http://localhost:5099/api/__yao/neo/status?token=xxx'
|
||||||
router.GET(path+"/status", append(middlewares, neo.handleStatus)...)
|
router.GET(path+"/status", append(middlewares, neo.handleStatus)...)
|
||||||
|
|
||||||
// Chat api
|
// 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 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)...)
|
||||||
|
|
||||||
|
// 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)...)
|
router.GET(path+"/chats", append(middlewares, neo.handleChatList)...)
|
||||||
|
|
||||||
|
// 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)...)
|
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)...)
|
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)...)
|
router.DELETE(path+"/chats/:id", append(middlewares, neo.handleChatDelete)...)
|
||||||
|
|
||||||
// History api
|
// 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)...)
|
router.GET(path+"/history", append(middlewares, neo.handleChatHistory)...)
|
||||||
|
|
||||||
// File api
|
// 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", append(middlewares, neo.handleUpload)...)
|
router.POST(path+"/upload", 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)...)
|
router.GET(path+"/download", append(middlewares, neo.handleDownload)...)
|
||||||
|
|
||||||
// Mention api
|
// 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)...)
|
router.GET(path+"/mentions", append(middlewares, neo.handleMentions)...)
|
||||||
|
|
||||||
|
// Generation endpoints
|
||||||
|
// Generate custom content example:
|
||||||
|
// curl -X GET 'http://localhost:5099/api/__yao/neo/generate?content=Generate+something&type=custom&system_prompt=You+are+a+helpful+assistant&chat_id=chat_123&token=xxx'
|
||||||
|
// curl -X POST 'http://localhost:5099/api/__yao/neo/generate' \
|
||||||
|
// -H 'Content-Type: application/json' \
|
||||||
|
// -d '{"content": "Generate something", "type": "custom", "system_prompt": "You are a helpful assistant", "chat_id": "chat_123", "token": "xxx"}'
|
||||||
|
router.GET(path+"/generate", append(middlewares, neo.handleGenerateCustom)...)
|
||||||
|
router.POST(path+"/generate", append(middlewares, neo.handleGenerateCustom)...)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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)...)
|
router.DELETE(path+"/dangerous/clear_chats", append(middlewares, neo.handleChatsDeleteAll)...)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -383,49 +472,35 @@ func (neo *DSL) handleMentions(c *gin.Context) {
|
||||||
|
|
||||||
// Get keywords from query parameter
|
// Get keywords from query parameter
|
||||||
keywords := strings.ToLower(c.Query("keywords"))
|
keywords := strings.ToLower(c.Query("keywords"))
|
||||||
mentions, err := neo.GetMentions(keywords)
|
mentionable := true
|
||||||
|
|
||||||
|
// Query mentionable assistants
|
||||||
|
filter := conversation.AssistantFilter{
|
||||||
|
Keywords: keywords,
|
||||||
|
Mentionable: &mentionable,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := neo.Conversation.GetAssistants(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
||||||
c.Done()
|
c.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add test data
|
// Convert assistants to mentions
|
||||||
testMentions := []Mention{
|
mentions := []Mention{}
|
||||||
{
|
for _, item := range response.Data {
|
||||||
ID: "assistant_1",
|
mention := Mention{
|
||||||
Name: "Alice AI",
|
ID: item["assistant_id"].(string),
|
||||||
Type: "assistant",
|
Name: item["name"].(string),
|
||||||
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice",
|
Type: item["type"].(string),
|
||||||
},
|
Avatar: item["avatar"].(string),
|
||||||
{
|
|
||||||
ID: "assistant_2",
|
|
||||||
Name: "Bob Bot",
|
|
||||||
Type: "assistant",
|
|
||||||
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Bob",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "assistant_3",
|
|
||||||
Name: "Carol AI",
|
|
||||||
Type: "assistant",
|
|
||||||
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Carol",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter mentions by keywords
|
|
||||||
if keywords != "" {
|
|
||||||
filtered := []Mention{}
|
|
||||||
for _, m := range testMentions {
|
|
||||||
if strings.Contains(strings.ToLower(m.Name), keywords) {
|
|
||||||
filtered = append(filtered, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
testMentions = filtered
|
mentions = append(mentions, mention)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append test data to actual mentions
|
|
||||||
mentions = append(mentions, testMentions...)
|
|
||||||
|
|
||||||
c.JSON(200, map[string]interface{}{"data": mentions})
|
c.JSON(200, map[string]interface{}{"data": mentions})
|
||||||
c.Done()
|
c.Done()
|
||||||
}
|
}
|
||||||
|
|
@ -462,7 +537,7 @@ func (neo *DSL) handleChatUpdate(c *gin.Context) {
|
||||||
ctx, cancel := NewContextWithCancel(sid, c.Query("chat_id"), "")
|
ctx, cancel := NewContextWithCancel(sid, c.Query("chat_id"), "")
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
title, err := neo.GenerateChatTitle(ctx, body.Content, c)
|
title, err := neo.GenerateChatTitle(ctx, body.Content, c, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
||||||
c.Done()
|
c.Done()
|
||||||
|
|
@ -534,3 +609,417 @@ func (neo *DSL) handleChatsDeleteAll(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"message": "ok"})
|
c.JSON(200, gin.H{"message": "ok"})
|
||||||
c.Done()
|
c.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateResponse is a helper struct to handle both SSE and HTTP responses
|
||||||
|
type generateResponse struct {
|
||||||
|
c *gin.Context
|
||||||
|
sid string
|
||||||
|
content string
|
||||||
|
result interface{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate checks common validation rules
|
||||||
|
func (r *generateResponse) validate() bool {
|
||||||
|
if r.sid == "" {
|
||||||
|
if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
r.c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
r.c.Header("Cache-Control", "no-cache")
|
||||||
|
r.c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().
|
||||||
|
Error("sid is required").
|
||||||
|
Done()
|
||||||
|
msg.Write(r.c.Writer)
|
||||||
|
} else {
|
||||||
|
r.c.JSON(400, gin.H{"message": "sid is required", "code": 400})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.content == "" {
|
||||||
|
if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
r.c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
r.c.Header("Cache-Control", "no-cache")
|
||||||
|
r.c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().
|
||||||
|
Error("content is required").
|
||||||
|
Done()
|
||||||
|
msg.Write(r.c.Writer)
|
||||||
|
} else {
|
||||||
|
r.c.JSON(400, gin.H{"message": "content is required", "code": 400})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// send handles both SSE and HTTP responses
|
||||||
|
func (r *generateResponse) send(key string) {
|
||||||
|
if r.err != nil {
|
||||||
|
if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
r.c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
r.c.Header("Cache-Control", "no-cache")
|
||||||
|
r.c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().
|
||||||
|
Error(r.err.Error()).
|
||||||
|
Done()
|
||||||
|
msg.Write(r.c.Writer)
|
||||||
|
} else {
|
||||||
|
r.c.JSON(500, gin.H{"message": r.err.Error(), "code": 500})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
r.c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
r.c.Header("Cache-Control", "no-cache")
|
||||||
|
r.c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().
|
||||||
|
Map(gin.H{key: r.result}).
|
||||||
|
Done()
|
||||||
|
msg.Write(r.c.Writer)
|
||||||
|
} else {
|
||||||
|
r.c.JSON(200, gin.H{key: r.result})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGenerateTitle handles generating a chat title
|
||||||
|
func (neo *DSL) handleGenerateTitle(c *gin.Context) {
|
||||||
|
var content string
|
||||||
|
if c.Request.Method == "GET" {
|
||||||
|
content = c.Query("content")
|
||||||
|
} else {
|
||||||
|
var body struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
// For SSE requests, send error message in SSE format
|
||||||
|
if strings.Contains(c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().Error("invalid request body").Done()
|
||||||
|
msg.Write(c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = body.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &generateResponse{
|
||||||
|
c: c,
|
||||||
|
sid: c.GetString("__sid"),
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SSE requests, set headers before validation
|
||||||
|
if strings.Contains(c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.validate() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := NewContextWithCancel(resp.sid, c.Query("chat_id"), "")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use silent mode for regular HTTP requests, streaming for SSE
|
||||||
|
silent := !strings.Contains(c.GetHeader("Accept"), "text/event-stream")
|
||||||
|
resp.result, resp.err = neo.GenerateChatTitle(ctx, resp.content, c, silent)
|
||||||
|
resp.send("result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGeneratePrompts handles generating prompts
|
||||||
|
func (neo *DSL) handleGeneratePrompts(c *gin.Context) {
|
||||||
|
var content string
|
||||||
|
if c.Request.Method == "GET" {
|
||||||
|
content = c.Query("content")
|
||||||
|
} else {
|
||||||
|
var body struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
// For SSE requests, send error message in SSE format
|
||||||
|
if strings.Contains(c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
msg := message.New().Error("invalid request body").Done()
|
||||||
|
msg.Write(c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = body.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &generateResponse{
|
||||||
|
c: c,
|
||||||
|
sid: c.GetString("__sid"),
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SSE requests, set headers before validation
|
||||||
|
if strings.Contains(c.GetHeader("Accept"), "text/event-stream") {
|
||||||
|
c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.validate() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := NewContextWithCancel(resp.sid, c.Query("chat_id"), "")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use silent mode for regular HTTP requests, streaming for SSE
|
||||||
|
silent := !strings.Contains(c.GetHeader("Accept"), "text/event-stream")
|
||||||
|
resp.result, resp.err = neo.GeneratePrompts(ctx, resp.content, c, silent)
|
||||||
|
resp.send("result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGenerateCustom handles generating custom content
|
||||||
|
func (neo *DSL) handleGenerateCustom(c *gin.Context) {
|
||||||
|
var content, genType, systemPrompt string
|
||||||
|
|
||||||
|
if c.Request.Method == "GET" {
|
||||||
|
content = c.Query("content")
|
||||||
|
genType = c.Query("type")
|
||||||
|
systemPrompt = c.Query("system_prompt")
|
||||||
|
} else {
|
||||||
|
var body struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SystemPrompt string `json:"system_prompt"`
|
||||||
|
}
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = body.Content
|
||||||
|
genType = body.Type
|
||||||
|
systemPrompt = body.SystemPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &generateResponse{
|
||||||
|
c: c,
|
||||||
|
sid: c.GetString("__sid"),
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
if !resp.validate() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional validations for custom generation
|
||||||
|
if genType == "" {
|
||||||
|
c.JSON(400, gin.H{"message": "type is required", "code": 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if systemPrompt == "" {
|
||||||
|
c.JSON(400, gin.H{"message": "system_prompt is required", "code": 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := NewContextWithCancel(resp.sid, c.Query("chat_id"), "")
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use silent mode for regular HTTP requests, streaming for SSE
|
||||||
|
silent := !strings.Contains(c.GetHeader("Accept"), "text/event-stream")
|
||||||
|
resp.result, resp.err = neo.GenerateWithAI(ctx, resp.content, genType, systemPrompt, c, silent)
|
||||||
|
resp.send("result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAssistantList handles listing assistants
|
||||||
|
func (neo *DSL) handleAssistantList(c *gin.Context) {
|
||||||
|
// Parse filter parameters
|
||||||
|
filter := conversation.AssistantFilter{
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := neo.Conversation.GetAssistants(filter)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAssistantDetail handles getting a single assistant's details
|
||||||
|
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 := conversation.AssistantFilter{
|
||||||
|
AssistantID: assistantID,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := neo.Conversation.GetAssistants(filter)
|
||||||
|
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
|
||||||
|
func (neo *DSL) handleAssistantSave(c *gin.Context) {
|
||||||
|
var assistant map[string]interface{}
|
||||||
|
if err := c.BindJSON(&assistant); err != nil {
|
||||||
|
c.JSON(400, gin.H{"message": "invalid request body", "code": 400})
|
||||||
|
c.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := neo.Conversation.SaveAssistant(assistant)
|
||||||
|
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 := assistant["assistant_id"]; !ok {
|
||||||
|
assistant["assistant_id"] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"message": "ok", "data": assistant})
|
||||||
|
c.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAssistantDelete handles deleting an assistant
|
||||||
|
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.Conversation.DeleteAssistant(assistantID)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package base
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chat the chat
|
// Chat the chat
|
||||||
func (ast *Base) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error {
|
func (ast *Local) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error {
|
||||||
|
|
||||||
if ast.openai == nil {
|
if ast.openai == nil {
|
||||||
return fmt.Errorf("api is not initialized")
|
return fmt.Errorf("api is not initialized")
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package base
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -31,7 +31,7 @@ var AllowedFileTypes = map[string]string{
|
||||||
var MaxSize int64 = 20 * 1024 * 1024
|
var MaxSize int64 = 20 * 1024 * 1024
|
||||||
|
|
||||||
// Upload the file
|
// Upload the file
|
||||||
func (ast *Base) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*assistant.File, error) {
|
func (ast *Local) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*assistant.File, error) {
|
||||||
|
|
||||||
// check file size
|
// check file size
|
||||||
if file.Size > MaxSize {
|
if file.Size > MaxSize {
|
||||||
|
|
@ -69,13 +69,13 @@ func (ast *Base) Upload(ctx context.Context, file *multipart.FileHeader, reader
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ast *Base) id(temp string, ext string) (string, error) {
|
func (ast *Local) id(temp string, ext string) (string, error) {
|
||||||
date := time.Now().Format("20060102")
|
date := time.Now().Format("20060102")
|
||||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
||||||
return fmt.Sprintf("/__assistants/%s/%s/%s%s", ast.ID, date, hash, ext), nil
|
return fmt.Sprintf("/__assistants/%s/%s/%s%s", ast.ID, date, hash, ext), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ast *Base) allowed(contentType string) bool {
|
func (ast *Local) allowed(contentType string) bool {
|
||||||
if _, ok := AllowedFileTypes[contentType]; ok {
|
if _, ok := AllowedFileTypes[contentType]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func (ast *Base) allowed(contentType string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download downloads a file
|
// Download downloads a file
|
||||||
func (ast *Base) Download(ctx context.Context, fileID string) (*assistant.FileResponse, error) {
|
func (ast *Local) Download(ctx context.Context, fileID string) (*assistant.FileResponse, error) {
|
||||||
|
|
||||||
// Get the data filesystem
|
// Get the data filesystem
|
||||||
data, err := fs.Get("data")
|
data, err := fs.Get("data")
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package base
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -8,16 +8,16 @@ import (
|
||||||
"github.com/yaoapp/yao/openai"
|
"github.com/yaoapp/yao/openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Base the base assistant
|
// Local the local assistant
|
||||||
type Base struct {
|
type Local struct {
|
||||||
ID string `json:"assistant_id"`
|
ID string `json:"assistant_id"`
|
||||||
Prompts []assistant.Prompt `json:"prompts,omitempty"`
|
Prompts []assistant.Prompt `json:"prompts,omitempty"`
|
||||||
Connector connector.Connector `json:"-" yaml:"-"`
|
Connector connector.Connector `json:"-" yaml:"-"`
|
||||||
openai *openai.OpenAI
|
openai *openai.OpenAI
|
||||||
}
|
}
|
||||||
|
|
||||||
// New create a new base assistant
|
// New create a new local assistant
|
||||||
func New(connector connector.Connector, prompts []assistant.Prompt, id string) (*Base, error) {
|
func New(connector connector.Connector, prompts []assistant.Prompt, id string) (*Local, error) {
|
||||||
|
|
||||||
setting := connector.Setting()
|
setting := connector.Setting()
|
||||||
api, err := openai.NewOpenAI(setting)
|
api, err := openai.NewOpenAI(setting)
|
||||||
|
|
@ -25,10 +25,10 @@ func New(connector connector.Connector, prompts []assistant.Prompt, id string) (
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Base{Connector: connector, ID: id, Prompts: prompts, openai: api}, nil
|
return &Local{Connector: connector, ID: id, Prompts: prompts, openai: api}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List list all assistants
|
// List list all assistants
|
||||||
func (ast *Base) List(ctx context.Context, param assistant.QueryParam) ([]assistant.Assistant, error) {
|
func (ast *Local) List(ctx context.Context, param assistant.QueryParam) ([]assistant.Assistant, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -30,13 +30,16 @@ type QueryParam struct {
|
||||||
|
|
||||||
// Assistant the assistant
|
// Assistant the assistant
|
||||||
type Assistant struct {
|
type Assistant struct {
|
||||||
ID string `json:"assistant_id"` // Assistant ID
|
ID string `json:"assistant_id"` // Assistant ID
|
||||||
Name string `json:"name,omitempty"` // Assistant Name
|
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||||
Connector string `json:"connector"` // AI Connector
|
Name string `json:"name,omitempty"` // Assistant Name
|
||||||
Description string `json:"description,omitempty"` // Assistant Description
|
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
||||||
Option map[string]interface{} `json:"option,omitempty"` // AI Option
|
Connector string `json:"connector"` // AI Connector
|
||||||
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
|
Description string `json:"description,omitempty"` // Assistant Description
|
||||||
API API `json:"-" yaml:"-"` // Assistant API
|
Option map[string]interface{} `json:"option,omitempty"` // AI Option
|
||||||
|
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
|
||||||
|
Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows
|
||||||
|
API API `json:"-" yaml:"-"` // Assistant API
|
||||||
}
|
}
|
||||||
|
|
||||||
// File the file
|
// File the file
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,59 @@
|
||||||
package conversation
|
package conversation
|
||||||
|
|
||||||
// Mongo conversation
|
// Mongo represents a MongoDB-based conversation storage
|
||||||
type Mongo struct{}
|
type Mongo struct{}
|
||||||
|
|
||||||
// NewMongo create a new conversation
|
// NewMongo creates a new MongoDB conversation storage
|
||||||
func NewMongo() *Mongo {
|
func NewMongo() *Mongo {
|
||||||
return &Mongo{}
|
return &Mongo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateChatTitle update the chat title
|
// GetChats retrieves a list of chats
|
||||||
func (conv *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
|
func (m *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||||
return nil
|
return &ChatGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChats get the chat list
|
// GetChat retrieves a single chat's information
|
||||||
func (conv *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
func (m *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||||
return &ChatGroupResponse{
|
return &ChatInfo{}, nil
|
||||||
Groups: []ChatGroup{},
|
|
||||||
Page: filter.Page,
|
|
||||||
PageSize: filter.PageSize,
|
|
||||||
Total: 0,
|
|
||||||
LastPage: 1,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHistory get the history
|
// GetHistory retrieves chat history
|
||||||
func (conv *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
func (m *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||||
return []map[string]interface{}{}, nil
|
return []map[string]interface{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveHistory save the history
|
// SaveHistory saves chat history
|
||||||
func (conv *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
func (m *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequest get the request
|
// DeleteChat deletes a single chat
|
||||||
func (conv *Mongo) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
func (m *Mongo) DeleteChat(sid string, cid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveRequest save the request
|
|
||||||
func (conv *Mongo) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChat get the chat info and its history
|
// DeleteAllChats deletes all chats
|
||||||
func (conv *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
|
func (m *Mongo) DeleteAllChats(sid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChat deletes a specific chat and its history
|
|
||||||
func (conv *Mongo) DeleteChat(sid string, cid string) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAllChats deletes all chats and their histories for a user
|
// UpdateChatTitle updates chat title
|
||||||
func (conv *Mongo) DeleteAllChats(sid string) error {
|
func (m *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveAssistant saves assistant information
|
||||||
|
func (m *Mongo) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
||||||
|
return assistant["assistant_id"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAssistant deletes an assistant
|
||||||
|
func (m *Mongo) DeleteAssistant(assistantID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssistants retrieves a list of assistants
|
||||||
|
func (m *Mongo) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
|
||||||
|
return &AssistantResponse{}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,59 @@
|
||||||
package conversation
|
package conversation
|
||||||
|
|
||||||
// Redis conversation
|
// Redis represents a Redis-based conversation storage
|
||||||
type Redis struct{}
|
type Redis struct{}
|
||||||
|
|
||||||
// NewRedis create a new conversation
|
// NewRedis creates a new Redis conversation storage
|
||||||
func NewRedis() *Redis {
|
func NewRedis() *Redis {
|
||||||
return &Redis{}
|
return &Redis{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateChatTitle update the chat title
|
// GetChats retrieves a list of chats
|
||||||
func (conv *Redis) UpdateChatTitle(sid string, cid string, title string) error {
|
func (r *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||||
return nil
|
return &ChatGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChats get the chat list
|
// GetChat retrieves a single chat's information
|
||||||
func (conv *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
func (r *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||||
return &ChatGroupResponse{
|
return &ChatInfo{}, nil
|
||||||
Groups: []ChatGroup{},
|
|
||||||
Page: filter.Page,
|
|
||||||
PageSize: filter.PageSize,
|
|
||||||
Total: 0,
|
|
||||||
LastPage: 1,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHistory get the history
|
// GetHistory retrieves chat history
|
||||||
func (conv *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
func (r *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||||
return []map[string]interface{}{}, nil
|
return []map[string]interface{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveHistory save the history
|
// SaveHistory saves chat history
|
||||||
func (conv *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
func (r *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequest get the request
|
// DeleteChat deletes a single chat
|
||||||
func (conv *Redis) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
func (r *Redis) DeleteChat(sid string, cid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveRequest save the request
|
|
||||||
func (conv *Redis) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChat get the chat info and its history
|
// DeleteAllChats deletes all chats
|
||||||
func (conv *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
|
func (r *Redis) DeleteAllChats(sid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChat deletes a specific chat and its history
|
|
||||||
func (conv *Redis) DeleteChat(sid string, cid string) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAllChats deletes all chats and their histories for a user
|
// UpdateChatTitle updates chat title
|
||||||
func (conv *Redis) DeleteAllChats(sid string) error {
|
func (r *Redis) UpdateChatTitle(sid string, cid string, title string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveAssistant saves assistant information
|
||||||
|
func (r *Redis) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
||||||
|
return assistant["assistant_id"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAssistant deletes an assistant
|
||||||
|
func (r *Redis) DeleteAssistant(assistantID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssistants retrieves a list of assistants
|
||||||
|
func (r *Redis) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
|
||||||
|
return &AssistantResponse{}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,133 @@
|
||||||
package conversation
|
package conversation
|
||||||
|
|
||||||
// Setting the conversation config
|
// Setting represents the conversation configuration structure
|
||||||
|
// Used to configure basic conversation parameters including connector, user field, table name, etc.
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Connector string `json:"connector,omitempty"`
|
Connector string `json:"connector,omitempty"` // Name of the connector used to specify data storage method
|
||||||
UserField string `json:"user_field,omitempty"` // the user id field name, default is user_id
|
UserField string `json:"user_field,omitempty"` // User ID field name, defaults to "user_id"
|
||||||
Table string `json:"table,omitempty"`
|
Table string `json:"table,omitempty"` // Database table name
|
||||||
MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"`
|
MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"` // Maximum storage size limit
|
||||||
TTL int `json:"ttl,omitempty" yaml:"ttl,omitempty"`
|
TTL int `json:"ttl,omitempty" yaml:"ttl,omitempty"` // Time To Live in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatInfo represents the chat information and its history
|
// ChatInfo represents the chat information structure
|
||||||
|
// Contains basic information and history for a single chat
|
||||||
type ChatInfo struct {
|
type ChatInfo struct {
|
||||||
Chat map[string]interface{} `json:"chat"`
|
Chat map[string]interface{} `json:"chat"` // Basic chat information
|
||||||
History []map[string]interface{} `json:"history"`
|
History []map[string]interface{} `json:"history"` // Chat history records
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatFilter represents the filter parameters for GetChats
|
// ChatFilter represents the chat filter structure
|
||||||
|
// Used for filtering and pagination when retrieving chat lists
|
||||||
type ChatFilter struct {
|
type ChatFilter struct {
|
||||||
Keywords string `json:"keywords,omitempty"`
|
Keywords string `json:"keywords,omitempty"` // Keyword search
|
||||||
Page int `json:"page,omitempty"` // 页码,从1开始
|
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||||
PageSize int `json:"pagesize,omitempty"` // 每页数量
|
PageSize int `json:"pagesize,omitempty"` // Number of items per page
|
||||||
Order string `json:"order,omitempty"` // desc/asc
|
Order string `json:"order,omitempty"` // Sort order: desc/asc
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatGroup represents a group of chats by date
|
// ChatGroup represents the chat group structure
|
||||||
|
// Groups chats by date
|
||||||
type ChatGroup struct {
|
type ChatGroup struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"` // Group label (typically a date)
|
||||||
Chats []map[string]interface{} `json:"chats"`
|
Chats []map[string]interface{} `json:"chats"` // List of chats in this group
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatGroupResponse represents paginated chat groups
|
// ChatGroupResponse represents the paginated chat group response
|
||||||
|
// Contains paginated chat group information
|
||||||
type ChatGroupResponse struct {
|
type ChatGroupResponse struct {
|
||||||
Groups []ChatGroup `json:"groups"`
|
Groups []ChatGroup `json:"groups"` // List of chat groups
|
||||||
Page int `json:"page"` // 当前页码
|
Page int `json:"page"` // Current page number
|
||||||
PageSize int `json:"pagesize"` // 每页数量
|
PageSize int `json:"pagesize"` // Items per page
|
||||||
Total int64 `json:"total"` // 总记录数
|
Total int64 `json:"total"` // Total number of records
|
||||||
LastPage int `json:"last_page"` // 最后一页页码
|
LastPage int `json:"last_page"` // Last page number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversation the store interface
|
// AssistantFilter represents the assistant filter structure
|
||||||
type Conversation interface {
|
// Used for filtering and pagination when retrieving assistant lists
|
||||||
GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error)
|
type AssistantFilter struct {
|
||||||
GetChat(sid string, cid string) (*ChatInfo, error)
|
Tags []string `json:"tags,omitempty"` // Filter by tags
|
||||||
GetHistory(sid string, cid string) ([]map[string]interface{}, error)
|
Keywords string `json:"keywords,omitempty"` // Search in name and description
|
||||||
SaveHistory(sid string, messages []map[string]interface{}, cid string) error
|
Connector string `json:"connector,omitempty"` // Filter by connector
|
||||||
GetRequest(sid string, rid string) ([]map[string]interface{}, error)
|
AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID
|
||||||
SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error
|
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||||
DeleteChat(sid string, cid string) error
|
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||||
DeleteAllChats(sid string) error
|
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||||
UpdateChatTitle(sid string, cid string, title string) error
|
PageSize int `json:"pagesize,omitempty"` // Items per page
|
||||||
|
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssistantResponse represents the assistant response structure
|
||||||
|
// Used for returning paginated assistant lists
|
||||||
|
type AssistantResponse struct {
|
||||||
|
Data []map[string]interface{} `json:"data"` // The paginated data
|
||||||
|
Page int `json:"page"` // Current page number
|
||||||
|
PageSize int `json:"pagesize"` // Number of items per page
|
||||||
|
PageCnt int `json:"pagecnt"` // Total number of pages
|
||||||
|
Next int `json:"next"` // Next page number
|
||||||
|
Prev int `json:"prev"` // Previous page number
|
||||||
|
Total int64 `json:"total"` // Total number of items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversation defines the conversation storage interface
|
||||||
|
// Provides basic operations required for conversation management
|
||||||
|
type Conversation interface {
|
||||||
|
// GetChats retrieves a list of chats
|
||||||
|
// sid: Session ID
|
||||||
|
// filter: Filter conditions
|
||||||
|
// Returns: Grouped chat list and potential error
|
||||||
|
GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error)
|
||||||
|
|
||||||
|
// GetChat retrieves a single chat's information
|
||||||
|
// sid: Session ID
|
||||||
|
// cid: Chat ID
|
||||||
|
// Returns: Chat information and potential error
|
||||||
|
GetChat(sid string, cid string) (*ChatInfo, error)
|
||||||
|
|
||||||
|
// GetHistory retrieves chat history
|
||||||
|
// sid: Session ID
|
||||||
|
// cid: Chat ID
|
||||||
|
// Returns: History record list and potential error
|
||||||
|
GetHistory(sid string, cid string) ([]map[string]interface{}, error)
|
||||||
|
|
||||||
|
// SaveHistory saves chat history
|
||||||
|
// sid: Session ID
|
||||||
|
// messages: Message list
|
||||||
|
// cid: Chat ID
|
||||||
|
// context: Context information
|
||||||
|
// Returns: Potential error
|
||||||
|
SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error
|
||||||
|
|
||||||
|
// DeleteChat deletes a single chat
|
||||||
|
// sid: Session ID
|
||||||
|
// cid: Chat ID
|
||||||
|
// Returns: Potential error
|
||||||
|
DeleteChat(sid string, cid string) error
|
||||||
|
|
||||||
|
// DeleteAllChats deletes all chats
|
||||||
|
// sid: Session ID
|
||||||
|
// Returns: Potential error
|
||||||
|
DeleteAllChats(sid string) error
|
||||||
|
|
||||||
|
// UpdateChatTitle updates chat title
|
||||||
|
// sid: Session ID
|
||||||
|
// cid: Chat ID
|
||||||
|
// title: New title
|
||||||
|
// Returns: Potential error
|
||||||
|
UpdateChatTitle(sid string, cid string, title string) error
|
||||||
|
|
||||||
|
// SaveAssistant saves assistant information
|
||||||
|
// assistant: Assistant information
|
||||||
|
// Returns: Potential error
|
||||||
|
SaveAssistant(assistant map[string]interface{}) (interface{}, error)
|
||||||
|
|
||||||
|
// DeleteAssistant deletes an assistant
|
||||||
|
// assistantID: Assistant ID
|
||||||
|
// Returns: Potential error
|
||||||
|
DeleteAssistant(assistantID string) error
|
||||||
|
|
||||||
|
// GetAssistants retrieves a list of assistants
|
||||||
|
// filter: Filter conditions
|
||||||
|
// Returns: Paginated assistant list and potential error
|
||||||
|
GetAssistants(filter AssistantFilter) (*AssistantResponse, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,59 @@
|
||||||
package conversation
|
package conversation
|
||||||
|
|
||||||
// Weaviate Database conversation
|
// Weaviate represents a Weaviate-based conversation storage
|
||||||
type Weaviate struct{}
|
type Weaviate struct{}
|
||||||
|
|
||||||
// NewWeaviate create a new conversation
|
// NewWeaviate creates a new Weaviate conversation storage
|
||||||
func NewWeaviate() *Weaviate {
|
func NewWeaviate() *Weaviate {
|
||||||
return &Weaviate{}
|
return &Weaviate{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateChatTitle update the chat title
|
// GetChats retrieves a list of chats
|
||||||
func (conv *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
|
func (w *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||||
return nil
|
return &ChatGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChats get the chat list
|
// GetChat retrieves a single chat's information
|
||||||
func (conv *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
func (w *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||||
return &ChatGroupResponse{
|
return &ChatInfo{}, nil
|
||||||
Groups: []ChatGroup{},
|
|
||||||
Page: filter.Page,
|
|
||||||
PageSize: filter.PageSize,
|
|
||||||
Total: 0,
|
|
||||||
LastPage: 1,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHistory get the history
|
// GetHistory retrieves chat history
|
||||||
func (conv *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
func (w *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||||
return []map[string]interface{}{}, nil
|
return []map[string]interface{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveHistory save the history
|
// SaveHistory saves chat history
|
||||||
func (conv *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
func (w *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequest get the request
|
// DeleteChat deletes a single chat
|
||||||
func (conv *Weaviate) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
func (w *Weaviate) DeleteChat(sid string, cid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveRequest save the request
|
|
||||||
func (conv *Weaviate) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChat get the chat info and its history
|
// DeleteAllChats deletes all chats
|
||||||
func (conv *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
|
func (w *Weaviate) DeleteAllChats(sid string) error {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChat deletes a specific chat and its history
|
|
||||||
func (conv *Weaviate) DeleteChat(sid string, cid string) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAllChats deletes all chats and their histories for a user
|
// UpdateChatTitle updates chat title
|
||||||
func (conv *Weaviate) DeleteAllChats(sid string) error {
|
func (w *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveAssistant saves assistant information
|
||||||
|
func (w *Weaviate) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
||||||
|
return assistant["assistant_id"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAssistant deletes an assistant
|
||||||
|
func (w *Weaviate) DeleteAssistant(assistantID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssistants retrieves a list of assistants
|
||||||
|
func (w *Weaviate) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
|
||||||
|
return &AssistantResponse{}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/gou/session"
|
"github.com/yaoapp/gou/session"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
|
|
@ -15,32 +16,34 @@ import (
|
||||||
"github.com/yaoapp/xun/dbal/schema"
|
"github.com/yaoapp/xun/dbal/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Xun Database conversation
|
// Package conversation provides functionality for managing chat conversations and assistants.
|
||||||
|
|
||||||
|
// Xun implements the Conversation interface using a database backend.
|
||||||
|
// It provides functionality for:
|
||||||
|
// - Managing chat conversations and their message histories
|
||||||
|
// - Organizing chats with pagination and date-based grouping
|
||||||
|
// - Handling chat metadata like titles and creation dates
|
||||||
|
// - Managing AI assistants with their configurations and metadata
|
||||||
|
// - Supporting data expiration through TTL settings
|
||||||
type Xun struct {
|
type Xun struct {
|
||||||
query query.Query
|
query query.Query
|
||||||
schema schema.Schema
|
schema schema.Schema
|
||||||
setting Setting
|
setting Setting
|
||||||
}
|
}
|
||||||
|
|
||||||
type row struct {
|
// Public interface methods:
|
||||||
Role string `json:"role"`
|
//
|
||||||
Name string `json:"name"` // User name
|
// NewXun creates a new conversation instance with the given settings
|
||||||
Content string `json:"content"`
|
// UpdateChatTitle updates the title of a specific chat
|
||||||
Sid string `json:"sid"`
|
// GetChats retrieves a paginated list of chats grouped by date
|
||||||
Rid string `json:"rid"`
|
// GetChat retrieves a specific chat and its message history
|
||||||
Cid string `json:"cid"` // Chat ID from chat history
|
// GetHistory retrieves the message history for a specific chat
|
||||||
ExpiredAt interface{} `json:"expired_at"`
|
// SaveHistory saves new messages to a chat's history
|
||||||
}
|
// DeleteChat deletes a specific chat and its history
|
||||||
|
// DeleteAllChats deletes all chats and their histories for a user
|
||||||
// Public interface methods and constructor remain exported:
|
// SaveAssistant creates or updates an assistant
|
||||||
// - NewXun
|
// DeleteAssistant deletes an assistant by assistant_id
|
||||||
// - UpdateChatTitle
|
// GetAssistants retrieves a paginated list of assistants with filtering
|
||||||
// - GetChats
|
|
||||||
// - GetChat
|
|
||||||
// - GetHistory
|
|
||||||
// - SaveHistory
|
|
||||||
// - GetRequest
|
|
||||||
// - SaveRequest
|
|
||||||
|
|
||||||
// NewXun create a new conversation
|
// NewXun create a new conversation
|
||||||
func NewXun(setting Setting) (*Xun, error) {
|
func NewXun(setting Setting) (*Xun, error) {
|
||||||
|
|
@ -111,6 +114,11 @@ func (conv *Xun) initialize() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize assistant table
|
||||||
|
if err := conv.initAssistantTable(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,11 +134,12 @@ func (conv *Xun) initHistoryTable() error {
|
||||||
err = conv.schema.CreateTable(historyTable, func(table schema.Blueprint) {
|
err = conv.schema.CreateTable(historyTable, func(table schema.Blueprint) {
|
||||||
table.ID("id")
|
table.ID("id")
|
||||||
table.String("sid", 255).Index()
|
table.String("sid", 255).Index()
|
||||||
table.String("rid", 255).Null().Index()
|
|
||||||
table.String("cid", 200).Null().Index()
|
table.String("cid", 200).Null().Index()
|
||||||
|
table.String("uid", 255).Null().Index()
|
||||||
table.String("role", 200).Null().Index()
|
table.String("role", 200).Null().Index()
|
||||||
table.String("name", 200).Null().Index()
|
table.String("name", 200).Null().Index()
|
||||||
table.Text("content").Null()
|
table.Text("content").Null()
|
||||||
|
table.JSON("context").Null()
|
||||||
table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index()
|
table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index()
|
||||||
table.TimestampTz("updated_at").Null().Index()
|
table.TimestampTz("updated_at").Null().Index()
|
||||||
table.TimestampTz("expired_at").Null().Index()
|
table.TimestampTz("expired_at").Null().Index()
|
||||||
|
|
@ -148,7 +157,7 @@ func (conv *Xun) initHistoryTable() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := []string{"id", "sid", "rid", "cid", "role", "name", "content", "created_at", "updated_at", "expired_at"}
|
fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "created_at", "updated_at", "expired_at"}
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
if !tab.HasColumn(field) {
|
if !tab.HasColumn(field) {
|
||||||
return fmt.Errorf("%s is required", field)
|
return fmt.Errorf("%s is required", field)
|
||||||
|
|
@ -198,6 +207,59 @@ func (conv *Xun) initChatTable() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conv *Xun) initAssistantTable() error {
|
||||||
|
assistantTable := conv.getAssistantTable()
|
||||||
|
has, err := conv.schema.HasTable(assistantTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the assistant table
|
||||||
|
if !has {
|
||||||
|
err = conv.schema.CreateTable(assistantTable, func(table schema.Blueprint) {
|
||||||
|
table.ID("id")
|
||||||
|
table.String("assistant_id", 200).Unique().Index()
|
||||||
|
table.String("type", 200).SetDefault("assistant").Index() // default is assistant
|
||||||
|
table.String("name", 200).Null() // assistant name
|
||||||
|
table.String("avatar", 200).Null() // assistant avatar
|
||||||
|
table.String("connector", 200).NotNull() // assistant connector
|
||||||
|
table.Text("description").Null() // assistant description
|
||||||
|
table.JSON("options").Null() // assistant options
|
||||||
|
table.JSON("prompts").Null() // assistant prompts
|
||||||
|
table.JSON("flows").Null() // assistant flows
|
||||||
|
table.JSON("files").Null() // assistant files
|
||||||
|
table.JSON("functions").Null() // assistant functions
|
||||||
|
table.JSON("tags").Null() // assistant tags
|
||||||
|
table.Boolean("readonly").SetDefault(false).Index() // assistant readonly
|
||||||
|
table.JSON("permissions").Null() // assistant permissions
|
||||||
|
table.Boolean("automated").SetDefault(true).Index() // assistant autoable
|
||||||
|
table.Boolean("mentionable").SetDefault(true).Index() // Whether this assistant can appear in @ mention list
|
||||||
|
table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index()
|
||||||
|
table.TimestampTz("updated_at").Null().Index()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Trace("Create the assistant table: %s", assistantTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the table
|
||||||
|
tab, err := conv.schema.GetTable(assistantTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "options", "prompts", "flows", "files", "functions", "tags", "mentionable", "created_at", "updated_at"}
|
||||||
|
for _, field := range fields {
|
||||||
|
if !tab.HasColumn(field) {
|
||||||
|
return fmt.Errorf("%s is required", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (conv *Xun) getUserID(sid string) (string, error) {
|
func (conv *Xun) getUserID(sid string) (string, error) {
|
||||||
field := "user_id"
|
field := "user_id"
|
||||||
if conv.setting.UserField != "" {
|
if conv.setting.UserField != "" {
|
||||||
|
|
@ -217,13 +279,17 @@ func (conv *Xun) getUserID(sid string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conv *Xun) getHistoryTable() string {
|
func (conv *Xun) getHistoryTable() string {
|
||||||
return conv.setting.Table
|
return conv.setting.Table + "_history"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conv *Xun) getChatTable() string {
|
func (conv *Xun) getChatTable() string {
|
||||||
return conv.setting.Table + "_chat"
|
return conv.setting.Table + "_chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conv *Xun) getAssistantTable() string {
|
||||||
|
return conv.setting.Table + "_assistant"
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateChatTitle update the chat title
|
// UpdateChatTitle update the chat title
|
||||||
func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error {
|
func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error {
|
||||||
userID, err := conv.getUserID(sid)
|
userID, err := conv.getUserID(sid)
|
||||||
|
|
@ -380,7 +446,7 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e
|
||||||
}
|
}
|
||||||
|
|
||||||
qb := conv.newQuery().
|
qb := conv.newQuery().
|
||||||
Select("role", "name", "content").
|
Select("role", "name", "content", "context", "uid", "created_at", "updated_at").
|
||||||
Where("sid", userID).
|
Where("sid", userID).
|
||||||
Where("cid", cid).
|
Where("cid", cid).
|
||||||
OrderBy("id", "desc")
|
OrderBy("id", "desc")
|
||||||
|
|
@ -401,18 +467,23 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e
|
||||||
|
|
||||||
res := []map[string]interface{}{}
|
res := []map[string]interface{}{}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
res = append([]map[string]interface{}{{
|
message := map[string]interface{}{
|
||||||
"role": row.Get("role"),
|
"role": row.Get("role"),
|
||||||
"name": row.Get("name"),
|
"name": row.Get("name"),
|
||||||
"content": row.Get("content"),
|
"content": row.Get("content"),
|
||||||
}}, res...)
|
"context": row.Get("context"),
|
||||||
|
"uid": row.Get("uid"),
|
||||||
|
"created_at": row.Get("created_at"),
|
||||||
|
"updated_at": row.Get("updated_at"),
|
||||||
|
}
|
||||||
|
res = append([]map[string]interface{}{message}, res...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveHistory save the history
|
// SaveHistory save the history
|
||||||
func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||||
|
|
||||||
if cid == "" {
|
if cid == "" {
|
||||||
cid = uuid.New().String() // Generate a new UUID if cid is empty
|
cid = uuid.New().String() // Generate a new UUID if cid is empty
|
||||||
|
|
@ -450,24 +521,49 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid
|
||||||
// Save message history
|
// Save message history
|
||||||
defer conv.clean()
|
defer conv.clean()
|
||||||
var expiredAt interface{} = nil
|
var expiredAt interface{} = nil
|
||||||
values := []row{}
|
values := []map[string]interface{}{}
|
||||||
if conv.setting.TTL > 0 {
|
if conv.setting.TTL > 0 {
|
||||||
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
value := row{
|
// Type assertion safety checks
|
||||||
Role: message["role"].(string),
|
role, ok := message["role"].(string)
|
||||||
Name: "",
|
if !ok {
|
||||||
Content: message["content"].(string),
|
return fmt.Errorf("invalid role type in message: %v", message["role"])
|
||||||
Sid: userID,
|
|
||||||
Cid: cid,
|
|
||||||
ExpiredAt: expiredAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if message["name"] != nil {
|
content, ok := message["content"].(string)
|
||||||
value.Name = message["name"].(string)
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid content type in message: %v", message["content"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contextRaw interface{} = nil
|
||||||
|
if context != nil {
|
||||||
|
contextRaw, err = jsoniter.MarshalToString(context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value := map[string]interface{}{
|
||||||
|
"role": role,
|
||||||
|
"name": "",
|
||||||
|
"content": content,
|
||||||
|
"sid": userID,
|
||||||
|
"cid": cid,
|
||||||
|
"uid": userID,
|
||||||
|
"context": contextRaw,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": nil,
|
||||||
|
"expired_at": expiredAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := message["name"].(string); ok {
|
||||||
|
value["name"] = name
|
||||||
|
}
|
||||||
|
|
||||||
values = append(values, value)
|
values = append(values, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,79 +575,6 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequest get the request history
|
|
||||||
func (conv *Xun) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
|
||||||
userID, err := conv.getUserID(sid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
qb := conv.newQuery().
|
|
||||||
Select("role", "name", "content", "sid").
|
|
||||||
Where("rid", rid).
|
|
||||||
Where("sid", userID).
|
|
||||||
OrderBy("id", "desc")
|
|
||||||
|
|
||||||
if conv.setting.TTL > 0 {
|
|
||||||
qb.Where("expired_at", ">", time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := 20
|
|
||||||
if conv.setting.MaxSize > 0 {
|
|
||||||
limit = conv.setting.MaxSize
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := qb.Limit(limit).Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := []map[string]interface{}{}
|
|
||||||
for _, row := range rows {
|
|
||||||
res = append([]map[string]interface{}{{
|
|
||||||
"role": row.Get("role"),
|
|
||||||
"name": row.Get("name"),
|
|
||||||
"content": row.Get("content"),
|
|
||||||
}}, res...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveRequest save the request history
|
|
||||||
func (conv *Xun) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
|
||||||
userID, err := conv.getUserID(sid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer conv.clean()
|
|
||||||
var expiredAt interface{} = nil
|
|
||||||
values := []row{}
|
|
||||||
if conv.setting.TTL > 0 {
|
|
||||||
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, message := range messages {
|
|
||||||
value := row{
|
|
||||||
Role: message["role"].(string),
|
|
||||||
Name: "",
|
|
||||||
Content: message["content"].(string),
|
|
||||||
Sid: userID,
|
|
||||||
Cid: cid,
|
|
||||||
Rid: rid,
|
|
||||||
ExpiredAt: expiredAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
if message["name"] != nil {
|
|
||||||
value.Name = message["name"].(string)
|
|
||||||
}
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conv.newQuery().Insert(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChat get the chat info and its history
|
// GetChat get the chat info and its history
|
||||||
func (conv *Xun) GetChat(sid string, cid string) (*ChatInfo, error) {
|
func (conv *Xun) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||||
userID, err := conv.getUserID(sid)
|
userID, err := conv.getUserID(sid)
|
||||||
|
|
@ -638,3 +661,265 @@ func (conv *Xun) DeleteAllChats(sid string) error {
|
||||||
Delete()
|
Delete()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processJSONField processes a field that should be stored as JSON string
|
||||||
|
func (conv *Xun) processJSONField(field interface{}) (interface{}, error) {
|
||||||
|
if field == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := field.(type) {
|
||||||
|
case string:
|
||||||
|
return v, nil
|
||||||
|
default:
|
||||||
|
jsonStr, err := jsoniter.MarshalToString(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal %v to JSON: %v", field, err)
|
||||||
|
}
|
||||||
|
return jsonStr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJSONFields parses JSON string fields into their corresponding Go types
|
||||||
|
func (conv *Xun) parseJSONFields(data map[string]interface{}, fields []string) {
|
||||||
|
for _, field := range fields {
|
||||||
|
if val := data[field]; val != nil {
|
||||||
|
if strVal, ok := val.(string); ok && strVal != "" {
|
||||||
|
var parsed interface{}
|
||||||
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
||||||
|
data[field] = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAssistant saves assistant information
|
||||||
|
func (conv *Xun) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
||||||
|
// Validate required fields
|
||||||
|
requiredFields := []string{"name", "type", "connector"}
|
||||||
|
for _, field := range requiredFields {
|
||||||
|
if _, ok := assistant[field]; !ok {
|
||||||
|
return nil, fmt.Errorf("field %s is required", field)
|
||||||
|
}
|
||||||
|
if assistant[field] == nil || assistant[field] == "" {
|
||||||
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the assistant map to avoid modifying the original
|
||||||
|
assistantCopy := make(map[string]interface{})
|
||||||
|
for k, v := range assistant {
|
||||||
|
assistantCopy[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process JSON fields
|
||||||
|
jsonFields := []string{"tags", "options", "prompts", "flows", "files", "functions", "permissions"}
|
||||||
|
for _, field := range jsonFields {
|
||||||
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
||||||
|
// If it's a string, try to parse it first
|
||||||
|
if strVal, ok := val.(string); ok && strVal != "" {
|
||||||
|
var parsed interface{}
|
||||||
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
||||||
|
assistantCopy[field] = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate assistant_id if not provided
|
||||||
|
if _, ok := assistantCopy["assistant_id"]; !ok {
|
||||||
|
assistantCopy["assistant_id"] = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if assistant exists
|
||||||
|
exists, err := conv.query.New().
|
||||||
|
Table(conv.getAssistantTable()).
|
||||||
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
||||||
|
Exists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JSON fields to strings for storage
|
||||||
|
for _, field := range jsonFields {
|
||||||
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
||||||
|
jsonStr, err := jsoniter.MarshalToString(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
||||||
|
}
|
||||||
|
assistantCopy[field] = jsonStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or insert
|
||||||
|
if exists {
|
||||||
|
_, err := conv.query.New().
|
||||||
|
Table(conv.getAssistantTable()).
|
||||||
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
||||||
|
Update(assistantCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return assistantCopy["assistant_id"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conv.query.New().
|
||||||
|
Table(conv.getAssistantTable()).
|
||||||
|
Insert(assistantCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return assistantCopy["assistant_id"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAssistant deletes an assistant by assistant_id
|
||||||
|
func (conv *Xun) DeleteAssistant(assistantID string) error {
|
||||||
|
// Check if assistant exists
|
||||||
|
exists, err := conv.query.New().
|
||||||
|
Table(conv.getAssistantTable()).
|
||||||
|
Where("assistant_id", assistantID).
|
||||||
|
Exists()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("assistant %s not found", assistantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conv.query.New().
|
||||||
|
Table(conv.getAssistantTable()).
|
||||||
|
Where("assistant_id", assistantID).
|
||||||
|
Delete()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssistants retrieves assistants with pagination and filtering
|
||||||
|
func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
|
||||||
|
qb := conv.query.New().
|
||||||
|
Table(conv.getAssistantTable())
|
||||||
|
|
||||||
|
// Apply tag filter if provided
|
||||||
|
if filter.Tags != nil && len(filter.Tags) > 0 {
|
||||||
|
qb.Where(func(qb query.Query) {
|
||||||
|
for i, tag := range filter.Tags {
|
||||||
|
// For each tag, we need to match it as part of a JSON array
|
||||||
|
// This will match both single tag arrays ["tag1"] and multi-tag arrays ["tag1","tag2"]
|
||||||
|
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
|
||||||
|
if i == 0 {
|
||||||
|
qb.Where("tags", "like", pattern)
|
||||||
|
} else {
|
||||||
|
qb.OrWhere("tags", "like", pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply keyword filter if provided
|
||||||
|
if filter.Keywords != "" {
|
||||||
|
qb.Where(func(qb query.Query) {
|
||||||
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
||||||
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply connector filter if provided
|
||||||
|
if filter.Connector != "" {
|
||||||
|
qb.Where("connector", filter.Connector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply assistant_id filter if provided
|
||||||
|
if filter.AssistantID != "" {
|
||||||
|
qb.Where("assistant_id", filter.AssistantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply mentionable filter if provided
|
||||||
|
if filter.Mentionable != nil {
|
||||||
|
qb.Where("mentionable", *filter.Mentionable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply automated filter if provided
|
||||||
|
if filter.Automated != nil {
|
||||||
|
qb.Where("automated", *filter.Automated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults for pagination
|
||||||
|
if filter.PageSize <= 0 {
|
||||||
|
filter.PageSize = 20
|
||||||
|
}
|
||||||
|
if filter.Page <= 0 {
|
||||||
|
filter.Page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
total, err := qb.Clone().Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
offset := (filter.Page - 1) * filter.PageSize
|
||||||
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
||||||
|
nextPage := filter.Page + 1
|
||||||
|
if nextPage > totalPages {
|
||||||
|
nextPage = 0
|
||||||
|
}
|
||||||
|
prevPage := filter.Page - 1
|
||||||
|
if prevPage < 1 {
|
||||||
|
prevPage = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply select fields if provided
|
||||||
|
if filter.Select != nil && len(filter.Select) > 0 {
|
||||||
|
selectFields := make([]interface{}, len(filter.Select))
|
||||||
|
for i, field := range filter.Select {
|
||||||
|
selectFields[i] = field
|
||||||
|
}
|
||||||
|
qb.Select(selectFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paginated results
|
||||||
|
rows, err := qb.OrderBy("created_at", "desc").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(filter.PageSize).
|
||||||
|
Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert rows to map slice and parse JSON fields
|
||||||
|
data := make([]map[string]interface{}, len(rows))
|
||||||
|
jsonFields := []string{"tags", "options", "prompts", "flows", "files", "functions", "permissions"}
|
||||||
|
for i, row := range rows {
|
||||||
|
data[i] = row
|
||||||
|
// Only parse JSON fields if they are selected or no select filter is provided
|
||||||
|
if filter.Select == nil || len(filter.Select) == 0 {
|
||||||
|
conv.parseJSONFields(data[i], jsonFields)
|
||||||
|
} else {
|
||||||
|
// Parse only selected JSON fields
|
||||||
|
selectedJSONFields := []string{}
|
||||||
|
for _, field := range jsonFields {
|
||||||
|
for _, selected := range filter.Select {
|
||||||
|
if selected == field {
|
||||||
|
selectedJSONFields = append(selectedJSONFields, field)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(selectedJSONFields) > 0 {
|
||||||
|
conv.parseJSONFields(data[i], selectedJSONFields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AssistantResponse{
|
||||||
|
Data: data,
|
||||||
|
Page: filter.Page,
|
||||||
|
PageSize: filter.PageSize,
|
||||||
|
PageCnt: totalPages,
|
||||||
|
Next: nextPage,
|
||||||
|
Prev: prevPage,
|
||||||
|
Total: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/xun/capsule"
|
"github.com/yaoapp/xun/capsule"
|
||||||
|
|
@ -15,13 +16,28 @@ import (
|
||||||
func TestNewXunDefault(t *testing.T) {
|
func TestNewXunDefault(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
|
||||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to ensure table is created
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
conv, err := NewXun(Setting{
|
conv, err := NewXun(Setting{
|
||||||
Connector: "default",
|
Connector: "default",
|
||||||
Table: "__unit_test_conversation",
|
Table: "__unit_test_conversation",
|
||||||
|
|
@ -32,35 +48,59 @@ func TestNewXunDefault(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
has, err := capsule.Schema().HasTable("__unit_test_conversation")
|
// Check history table
|
||||||
|
has, err := capsule.Schema().HasTable("__unit_test_conversation_history")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, true, has)
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
// validate the table
|
// Check chat table
|
||||||
tab, err := conv.schema.GetTable(conv.setting.Table)
|
has, err = capsule.Schema().HasTable("__unit_test_conversation_chat")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
|
// Check assistant table
|
||||||
|
has, err = capsule.Schema().HasTable("__unit_test_conversation_assistant")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
|
// validate the history table
|
||||||
|
tab, err := conv.schema.GetTable(conv.getHistoryTable())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := []string{"id", "sid", "cid", "rid", "role", "name", "content", "created_at", "updated_at", "expired_at"}
|
fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "created_at", "updated_at", "expired_at"}
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
assert.Equal(t, true, tab.HasColumn(field))
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
}
|
}
|
||||||
|
|
||||||
conv, err = NewXun(Setting{
|
// validate the chat table
|
||||||
Connector: "default",
|
tab, err = conv.schema.GetTable(conv.getChatTable())
|
||||||
Table: "__unit_test_conversation",
|
|
||||||
})
|
|
||||||
|
|
||||||
has, err = capsule.Schema().HasTable("__unit_test_conversation")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, true, has)
|
chatFields := []string{"id", "chat_id", "title", "sid", "created_at", "updated_at"}
|
||||||
|
for _, field := range chatFields {
|
||||||
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the assistant table
|
||||||
|
tab, err = conv.schema.GetTable(conv.getAssistantTable())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantFields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "options", "prompts", "flows", "files", "functions", "tags", "readonly", "permissions", "automated", "mentionable", "created_at", "updated_at"}
|
||||||
|
for _, field := range assistantFields {
|
||||||
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewXunConnector(t *testing.T) {
|
func TestNewXunConnector(t *testing.T) {
|
||||||
|
|
@ -77,9 +117,17 @@ func TestNewXunConnector(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer sch.DropTableIfExists("__unit_test_conversation")
|
defer sch.DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer sch.DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
defer sch.DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
|
||||||
|
sch.DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
sch.DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
sch.DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
|
||||||
|
// Add a small delay to ensure table is created
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
sch.DropTableIfExists("__unit_test_conversation")
|
|
||||||
conv, err := NewXun(Setting{
|
conv, err := NewXun(Setting{
|
||||||
Connector: "mysql",
|
Connector: "mysql",
|
||||||
Table: "__unit_test_conversation",
|
Table: "__unit_test_conversation",
|
||||||
|
|
@ -90,44 +138,73 @@ func TestNewXunConnector(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
has, err := sch.HasTable("__unit_test_conversation")
|
// Check history table
|
||||||
|
has, err := sch.HasTable("__unit_test_conversation_history")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, true, has)
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
// validate the table
|
// Check chat table
|
||||||
tab, err := conv.schema.GetTable(conv.setting.Table)
|
has, err = sch.HasTable("__unit_test_conversation_chat")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
|
// Check assistant table
|
||||||
|
has, err = sch.HasTable("__unit_test_conversation_assistant")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, true, has)
|
||||||
|
|
||||||
|
// validate the history table
|
||||||
|
tab, err := conv.schema.GetTable(conv.getHistoryTable())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := []string{"id", "sid", "cid", "rid", "role", "name", "content", "created_at", "updated_at", "expired_at"}
|
fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "created_at", "updated_at", "expired_at"}
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
assert.Equal(t, true, tab.HasColumn(field))
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
}
|
}
|
||||||
|
|
||||||
conv, err = NewXun(Setting{
|
// validate the chat table
|
||||||
Connector: "default",
|
tab, err = conv.schema.GetTable(conv.getChatTable())
|
||||||
Table: "__unit_test_conversation",
|
|
||||||
})
|
|
||||||
|
|
||||||
has, err = sch.HasTable("__unit_test_conversation")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, true, has)
|
chatFields := []string{"id", "chat_id", "title", "sid", "created_at", "updated_at"}
|
||||||
|
for _, field := range chatFields {
|
||||||
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the assistant table
|
||||||
|
tab, err = conv.schema.GetTable(conv.getAssistantTable())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantFields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "options", "prompts", "flows", "files", "functions", "tags", "readonly", "permissions", "automated", "mentionable", "created_at", "updated_at"}
|
||||||
|
for _, field := range assistantFields {
|
||||||
|
assert.Equal(t, true, tab.HasColumn(field))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestXunSaveAndGetHistory(t *testing.T) {
|
func TestXunSaveAndGetHistory(t *testing.T) {
|
||||||
|
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
|
||||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +220,7 @@ func TestXunSaveAndGetHistory(t *testing.T) {
|
||||||
err = conv.SaveHistory("123456", []map[string]interface{}{
|
err = conv.SaveHistory("123456", []map[string]interface{}{
|
||||||
{"role": "user", "name": "user1", "content": "hello"},
|
{"role": "user", "name": "user1", "content": "hello"},
|
||||||
{"role": "assistant", "name": "user1", "content": "Hello there, how"},
|
{"role": "assistant", "name": "user1", "content": "Hello there, how"},
|
||||||
}, cid)
|
}, cid, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// get the history
|
// get the history
|
||||||
|
|
@ -154,44 +231,18 @@ func TestXunSaveAndGetHistory(t *testing.T) {
|
||||||
assert.Equal(t, 2, len(data))
|
assert.Equal(t, 2, len(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestXunSaveAndGetRequest(t *testing.T) {
|
|
||||||
|
|
||||||
test.Prepare(t, config.Conf)
|
|
||||||
defer test.Clean()
|
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
|
||||||
|
|
||||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conv, err := NewXun(Setting{
|
|
||||||
Connector: "default",
|
|
||||||
Table: "__unit_test_conversation",
|
|
||||||
TTL: 3600,
|
|
||||||
})
|
|
||||||
|
|
||||||
// save the history
|
|
||||||
err = conv.SaveRequest("123456", "912836", "test.command", []map[string]interface{}{
|
|
||||||
{"role": "user", "name": "user1", "content": "hello"},
|
|
||||||
{"role": "assistant", "name": "user1", "content": "Hello there, how"},
|
|
||||||
})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
// get the history
|
|
||||||
data, err := conv.GetRequest("123456", "912836")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, 2, len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
|
||||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +260,7 @@ func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
||||||
{"role": "user", "name": "user1", "content": "hello"},
|
{"role": "user", "name": "user1", "content": "hello"},
|
||||||
{"role": "assistant", "name": "assistant1", "content": "Hi! How can I help you?"},
|
{"role": "assistant", "name": "assistant1", "content": "Hi! How can I help you?"},
|
||||||
}
|
}
|
||||||
err = conv.SaveHistory(sid, messages, cid)
|
err = conv.SaveHistory(sid, messages, cid, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// get the history for specific cid
|
// get the history for specific cid
|
||||||
|
|
@ -224,7 +275,7 @@ func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
||||||
moreMessages := []map[string]interface{}{
|
moreMessages := []map[string]interface{}{
|
||||||
{"role": "user", "name": "user1", "content": "another message"},
|
{"role": "user", "name": "user1", "content": "another message"},
|
||||||
}
|
}
|
||||||
err = conv.SaveHistory(sid, moreMessages, anotherCID)
|
err = conv.SaveHistory(sid, moreMessages, anotherCID, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// get history for the first cid - should still be 2 messages
|
// get history for the first cid - should still be 2 messages
|
||||||
|
|
@ -252,11 +303,11 @@ func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
||||||
func TestXunGetChats(t *testing.T) {
|
func TestXunGetChats(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
|
||||||
// Drop both tables before test
|
// Drop both tables before test
|
||||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +345,7 @@ func TestXunGetChats(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then save the history
|
// Then save the history
|
||||||
err = conv.SaveHistory(sid, messages, chatID)
|
err = conv.SaveHistory(sid, messages, chatID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -325,7 +376,7 @@ func TestXunGetChats(t *testing.T) {
|
||||||
func TestXunDeleteChat(t *testing.T) {
|
func TestXunDeleteChat(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
|
||||||
conv, err := NewXun(Setting{
|
conv, err := NewXun(Setting{
|
||||||
|
|
@ -344,7 +395,7 @@ func TestXunDeleteChat(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the chat and history
|
// Save the chat and history
|
||||||
err = conv.SaveHistory(sid, messages, cid)
|
err = conv.SaveHistory(sid, messages, cid, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
// Verify chat exists
|
// Verify chat exists
|
||||||
|
|
@ -365,7 +416,7 @@ func TestXunDeleteChat(t *testing.T) {
|
||||||
func TestXunDeleteAllChats(t *testing.T) {
|
func TestXunDeleteAllChats(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
|
||||||
conv, err := NewXun(Setting{
|
conv, err := NewXun(Setting{
|
||||||
|
|
@ -385,7 +436,7 @@ func TestXunDeleteAllChats(t *testing.T) {
|
||||||
// Save multiple chats
|
// Save multiple chats
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
cid := fmt.Sprintf("test_chat_%d", i)
|
cid := fmt.Sprintf("test_chat_%d", i)
|
||||||
err = conv.SaveHistory(sid, messages, cid)
|
err = conv.SaveHistory(sid, messages, cid, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,3 +454,423 @@ func TestXunDeleteAllChats(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, int64(0), response.Total)
|
assert.Equal(t, int64(0), response.Total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXunAssistantCRUD(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
|
||||||
|
// Drop assistant table before test
|
||||||
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to ensure table is created
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
conv, err := NewXun(Setting{
|
||||||
|
Connector: "default",
|
||||||
|
Table: "__unit_test_conversation",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test creating a new assistant with different JSON field formats
|
||||||
|
// Test case 1: JSON fields as strings
|
||||||
|
tagsJSON := `["tag1", "tag2", "tag3"]`
|
||||||
|
optionsJSON := `{"model": "gpt-4"}`
|
||||||
|
assistant := map[string]interface{}{
|
||||||
|
"name": "Test Assistant",
|
||||||
|
"type": "assistant",
|
||||||
|
"avatar": "https://example.com/avatar.png",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description",
|
||||||
|
"tags": tagsJSON,
|
||||||
|
"options": optionsJSON,
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SaveAssistant (Create) with string JSON
|
||||||
|
v, err := conv.SaveAssistant(assistant)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assistantID := v.(string)
|
||||||
|
assert.NotEmpty(t, assistantID)
|
||||||
|
|
||||||
|
// Test case 2: JSON fields as native types
|
||||||
|
assistant2 := map[string]interface{}{
|
||||||
|
"name": "Test Assistant 2",
|
||||||
|
"type": "assistant",
|
||||||
|
"avatar": "https://example.com/avatar2.png",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description 2",
|
||||||
|
"tags": []string{"tag1", "tag2", "tag3"},
|
||||||
|
"options": map[string]interface{}{"model": "gpt-4"},
|
||||||
|
"prompts": []string{"prompt1", "prompt2"},
|
||||||
|
"flows": []string{"flow1", "flow2"},
|
||||||
|
"files": []string{"file1", "file2"},
|
||||||
|
"functions": []map[string]interface{}{{"name": "func1"}, {"name": "func2"}},
|
||||||
|
"permissions": map[string]interface{}{"read": true, "write": true},
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SaveAssistant (Create) with native types
|
||||||
|
v, err = conv.SaveAssistant(assistant2)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assistant2ID := v.(string)
|
||||||
|
assert.NotEmpty(t, assistant2ID)
|
||||||
|
|
||||||
|
// Test case 3: Test with nil JSON fields
|
||||||
|
assistant3 := map[string]interface{}{
|
||||||
|
"name": "Test Assistant 3",
|
||||||
|
"type": "assistant",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description 3",
|
||||||
|
"tags": nil,
|
||||||
|
"options": nil,
|
||||||
|
"prompts": nil,
|
||||||
|
"flows": nil,
|
||||||
|
"files": nil,
|
||||||
|
"functions": nil,
|
||||||
|
"permissions": nil,
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SaveAssistant (Create) with nil fields
|
||||||
|
v, err = conv.SaveAssistant(assistant3)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assistant3ID := v.(string)
|
||||||
|
assert.NotEmpty(t, assistant3ID)
|
||||||
|
|
||||||
|
// Test GetAssistants to verify JSON fields are properly stored
|
||||||
|
resp, err := conv.GetAssistants(AssistantFilter{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 3, len(resp.Data))
|
||||||
|
|
||||||
|
// Verify first assistant (string JSON)
|
||||||
|
found := false
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
if item["assistant_id"].(string) == assistantID {
|
||||||
|
found = true
|
||||||
|
// Now we expect parsed JSON values instead of JSON strings
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
// Verify second assistant (native types converted to JSON)
|
||||||
|
found = false
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
if item["assistant_id"].(string) == assistant2ID {
|
||||||
|
found = true
|
||||||
|
// Now we expect parsed JSON values directly
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
|
||||||
|
|
||||||
|
// Verify other JSON fields
|
||||||
|
assert.Equal(t, []interface{}{"prompt1", "prompt2"}, item["prompts"])
|
||||||
|
assert.Equal(t, []interface{}{"flow1", "flow2"}, item["flows"])
|
||||||
|
assert.Equal(t, []interface{}{"file1", "file2"}, item["files"])
|
||||||
|
assert.Equal(t,
|
||||||
|
[]interface{}{
|
||||||
|
map[string]interface{}{"name": "func1"},
|
||||||
|
map[string]interface{}{"name": "func2"},
|
||||||
|
},
|
||||||
|
item["functions"])
|
||||||
|
assert.Equal(t,
|
||||||
|
map[string]interface{}{
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
},
|
||||||
|
item["permissions"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
// Verify third assistant (nil fields)
|
||||||
|
found = false
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
if item["assistant_id"].(string) == assistant3ID {
|
||||||
|
found = true
|
||||||
|
assert.Nil(t, item["tags"])
|
||||||
|
assert.Nil(t, item["options"])
|
||||||
|
assert.Nil(t, item["prompts"])
|
||||||
|
assert.Nil(t, item["flows"])
|
||||||
|
assert.Nil(t, item["files"])
|
||||||
|
assert.Nil(t, item["functions"])
|
||||||
|
assert.Nil(t, item["permissions"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
// Test updating with mixed JSON formats
|
||||||
|
assistant2["assistant_id"] = assistant2ID
|
||||||
|
_, err = conv.SaveAssistant(assistant2)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
if item["assistant_id"].(string) == assistant2ID {
|
||||||
|
// Now we expect parsed JSON values
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DeleteAssistant
|
||||||
|
err = conv.DeleteAssistant(assistantID)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = conv.DeleteAssistant(assistant2ID)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = conv.DeleteAssistant(assistant3ID)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(resp.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXunAssistantPagination(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat")
|
||||||
|
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
|
||||||
|
// Drop assistant table before test
|
||||||
|
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to ensure table is created
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
conv, err := NewXun(Setting{
|
||||||
|
Connector: "default",
|
||||||
|
Table: "__unit_test_conversation",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multiple assistants for pagination testing
|
||||||
|
mentionable := true
|
||||||
|
automated := true
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
tagsJSON, err := jsoniter.MarshalToString([]string{fmt.Sprintf("tag%d", i%5)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternate mentionable and automated flags
|
||||||
|
if i%2 == 0 {
|
||||||
|
mentionable = !mentionable
|
||||||
|
}
|
||||||
|
if i%3 == 0 {
|
||||||
|
automated = !automated
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant := map[string]interface{}{
|
||||||
|
"name": fmt.Sprintf("Assistant %d", i),
|
||||||
|
"type": "assistant",
|
||||||
|
"connector": fmt.Sprintf("connector%d", i%3),
|
||||||
|
"description": fmt.Sprintf("Description %d", i),
|
||||||
|
"tags": tagsJSON,
|
||||||
|
"mentionable": mentionable,
|
||||||
|
"automated": automated,
|
||||||
|
}
|
||||||
|
_, err = conv.SaveAssistant(assistant)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test first page
|
||||||
|
resp, err := conv.GetAssistants(AssistantFilter{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 10, len(resp.Data))
|
||||||
|
assert.Equal(t, int64(25), resp.Total)
|
||||||
|
assert.Equal(t, 3, resp.PageCnt)
|
||||||
|
assert.Equal(t, 2, resp.Next)
|
||||||
|
assert.Equal(t, 0, resp.Prev)
|
||||||
|
|
||||||
|
// Test second page
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Page: 2,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 10, len(resp.Data))
|
||||||
|
assert.Equal(t, 3, resp.Next)
|
||||||
|
assert.Equal(t, 1, resp.Prev)
|
||||||
|
|
||||||
|
// Test last page
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Page: 3,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 5, len(resp.Data))
|
||||||
|
assert.Equal(t, 0, resp.Next)
|
||||||
|
assert.Equal(t, 2, resp.Prev)
|
||||||
|
|
||||||
|
// Test filtering with tags
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Tags: []string{"tag0"},
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 5, len(resp.Data))
|
||||||
|
|
||||||
|
// Test filtering with keywords
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Keywords: "Assistant 1",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(resp.Data), 0)
|
||||||
|
|
||||||
|
// Test filtering with connector
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Connector: "connector0",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(resp.Data), 0)
|
||||||
|
|
||||||
|
// Test filtering with mentionable
|
||||||
|
mentionableTrue := true
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Mentionable: &mentionableTrue,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(resp.Data), 0)
|
||||||
|
|
||||||
|
// Test filtering with automated
|
||||||
|
automatedTrue := true
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Automated: &automatedTrue,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(resp.Data), 0)
|
||||||
|
|
||||||
|
// Test filtering by assistant_id
|
||||||
|
// First get an assistant_id from previous results
|
||||||
|
firstAssistantID := resp.Data[0]["assistant_id"].(string)
|
||||||
|
|
||||||
|
// Test exact match with assistant_id
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
AssistantID: firstAssistantID,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(resp.Data))
|
||||||
|
assert.Equal(t, firstAssistantID, resp.Data[0]["assistant_id"])
|
||||||
|
|
||||||
|
// Test assistant_id with other filters
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
AssistantID: firstAssistantID,
|
||||||
|
Select: []string{"name", "assistant_id", "description"},
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(resp.Data))
|
||||||
|
assert.Equal(t, firstAssistantID, resp.Data[0]["assistant_id"])
|
||||||
|
// Verify only selected fields are returned
|
||||||
|
assert.Contains(t, resp.Data[0], "name")
|
||||||
|
assert.Contains(t, resp.Data[0], "assistant_id")
|
||||||
|
assert.Contains(t, resp.Data[0], "description")
|
||||||
|
assert.NotContains(t, resp.Data[0], "tags")
|
||||||
|
assert.NotContains(t, resp.Data[0], "options")
|
||||||
|
|
||||||
|
// Test non-existent assistant_id
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
AssistantID: "non-existent-id",
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, len(resp.Data))
|
||||||
|
|
||||||
|
// Test combined filters
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Tags: []string{"tag0"},
|
||||||
|
Keywords: "Assistant",
|
||||||
|
Connector: "connector0",
|
||||||
|
Mentionable: &mentionableTrue,
|
||||||
|
Automated: &automatedTrue,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
// Test filtering with select fields
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Select: []string{"name", "description", "tags"},
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 10, len(resp.Data))
|
||||||
|
// Verify only selected fields are returned
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
// These fields should exist
|
||||||
|
assert.Contains(t, item, "name")
|
||||||
|
assert.Contains(t, item, "description")
|
||||||
|
assert.Contains(t, item, "tags")
|
||||||
|
// These fields should not exist
|
||||||
|
assert.NotContains(t, item, "options")
|
||||||
|
assert.NotContains(t, item, "prompts")
|
||||||
|
assert.NotContains(t, item, "flows")
|
||||||
|
assert.NotContains(t, item, "files")
|
||||||
|
assert.NotContains(t, item, "functions")
|
||||||
|
assert.NotContains(t, item, "permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test filtering with select fields and other filters combined
|
||||||
|
resp, err = conv.GetAssistants(AssistantFilter{
|
||||||
|
Tags: []string{"tag0"},
|
||||||
|
Keywords: "Assistant",
|
||||||
|
Select: []string{"name", "tags"},
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// Verify only selected fields are returned
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
// These fields should exist
|
||||||
|
assert.Contains(t, item, "name")
|
||||||
|
assert.Contains(t, item, "tags")
|
||||||
|
// These fields should not exist
|
||||||
|
assert.NotContains(t, item, "description")
|
||||||
|
assert.NotContains(t, item, "options")
|
||||||
|
assert.NotContains(t, item, "prompts")
|
||||||
|
assert.NotContains(t, item, "flows")
|
||||||
|
assert.NotContains(t, item, "files")
|
||||||
|
assert.NotContains(t, item, "functions")
|
||||||
|
assert.NotContains(t, item, "permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
74
neo/neo.go
74
neo/neo.go
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
"github.com/yaoapp/yao/neo/assistant"
|
||||||
"github.com/yaoapp/yao/neo/assistant/base"
|
"github.com/yaoapp/yao/neo/assistant/local"
|
||||||
"github.com/yaoapp/yao/neo/assistant/openai"
|
"github.com/yaoapp/yao/neo/assistant/openai"
|
||||||
"github.com/yaoapp/yao/neo/conversation"
|
"github.com/yaoapp/yao/neo/conversation"
|
||||||
"github.com/yaoapp/yao/neo/message"
|
"github.com/yaoapp/yao/neo/message"
|
||||||
|
|
@ -58,20 +58,50 @@ func (neo *DSL) GetMentions(keywords string) ([]Mention, error) {
|
||||||
return neo.HookMention(context.Background(), keywords)
|
return neo.HookMention(context.Background(), keywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateChatTitle generate the chat title
|
// GeneratePrompts generate prompts for the AI assistant
|
||||||
func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context) (string, error) {
|
func (neo *DSL) GeneratePrompts(ctx Context, input string, c *gin.Context, silent ...bool) (string, error) {
|
||||||
|
|
||||||
prompts := `
|
prompts := `
|
||||||
Help me generate a title for the chat
|
Optimize the prompts for the AI assistant
|
||||||
1. The title should be a short and concise description of the chat.
|
1. Optimize prompts based on the user's input
|
||||||
2. The title should be a single sentence.
|
2. The prompts should be clear and specific
|
||||||
3. The title should be in same language as the chat.
|
3. The prompts should be in the same language as the input
|
||||||
4. The title should be no more than 50 characters.
|
4. Keep the prompts concise but comprehensive
|
||||||
|
5. DO NOT ASK USER FOR MORE INFORMATION, JUST GENERATE PROMPTS
|
||||||
|
6. DO NOT ANSWER THE QUESTION, JUST GENERATE PROMPTS
|
||||||
`
|
`
|
||||||
|
isSilent := false
|
||||||
|
if len(silent) > 0 {
|
||||||
|
isSilent = silent[0]
|
||||||
|
}
|
||||||
|
return neo.GenerateWithAI(ctx, input, "prompts", prompts, c, isSilent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateChatTitle generate the chat title
|
||||||
|
func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context, silent ...bool) (string, error) {
|
||||||
|
prompts := `
|
||||||
|
Help me generate a title for the chat
|
||||||
|
1. The title should be a short and concise description of the chat.
|
||||||
|
2. The title should be a single sentence.
|
||||||
|
3. The title should be in same language as the chat.
|
||||||
|
4. The title should be no more than 50 characters.
|
||||||
|
`
|
||||||
|
isSilent := false
|
||||||
|
if len(silent) > 0 {
|
||||||
|
isSilent = silent[0]
|
||||||
|
}
|
||||||
|
return neo.GenerateWithAI(ctx, input, "title", prompts, c, isSilent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateWithAI generate content with AI, type can be "title", "prompts", etc.
|
||||||
|
func (neo *DSL) GenerateWithAI(ctx Context, input string, messageType string, systemPrompt string, c *gin.Context, silent bool) (string, error) {
|
||||||
messages := []map[string]interface{}{
|
messages := []map[string]interface{}{
|
||||||
{"role": "system", "content": prompts},
|
{"role": "system", "content": systemPrompt},
|
||||||
{"role": "user", "content": input},
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": input,
|
||||||
|
"type": messageType,
|
||||||
|
"name": ctx.Sid,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := neo.HookCreate(ctx, messages, c)
|
res, err := neo.HookCreate(ctx, messages, c)
|
||||||
|
|
@ -119,8 +149,21 @@ func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context) (st
|
||||||
// Append content and send message
|
// Append content and send message
|
||||||
content = msg.Append(content)
|
content = msg.Append(content)
|
||||||
|
|
||||||
|
// Only send real-time messages if not in silent mode
|
||||||
|
if !silent && msg.Message != nil && msg.Message.Text != "" {
|
||||||
|
message.New().
|
||||||
|
Map(map[string]interface{}{
|
||||||
|
"text": msg.Message.Text,
|
||||||
|
"done": msg.Message.Done,
|
||||||
|
}).
|
||||||
|
Write(c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
// Complete the stream
|
// Complete the stream
|
||||||
if msg.Message.Done {
|
if msg.Message.Done {
|
||||||
|
if !silent && msg.Message.Text == "" {
|
||||||
|
msg.Write(c.Writer)
|
||||||
|
}
|
||||||
done <- true
|
done <- true
|
||||||
return 0 // break
|
return 0 // break
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +174,9 @@ func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context) (st
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Chat error: %s", err.Error())
|
log.Error("Chat error: %s", err.Error())
|
||||||
message.New().Error(err).Done().Write(c.Writer)
|
if !silent {
|
||||||
|
message.New().Error(err).Done().Write(c.Writer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
done <- true
|
done <- true
|
||||||
|
|
@ -372,9 +417,9 @@ func (neo *DSL) newAssistantByConnector(id string) (assistant.API, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base on the assistant list hook
|
// Base on the assistant list hook
|
||||||
api, err := base.New(conn, neo.Prompts, id)
|
api, err := local.New(conn, neo.Prompts, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Create base assistant error: %s", err.Error())
|
return nil, fmt.Errorf("Create local assistant error: %s", err.Error())
|
||||||
}
|
}
|
||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
@ -453,6 +498,7 @@ func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages
|
||||||
{"role": "assistant", "content": string(content), "name": sid},
|
{"role": "assistant", "content": string(content), "name": sid},
|
||||||
},
|
},
|
||||||
chatID,
|
chatID,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
167
neo/process.go
167
neo/process.go
|
|
@ -1,15 +1,32 @@
|
||||||
package neo
|
package neo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/kun/exception"
|
"github.com/yaoapp/kun/exception"
|
||||||
|
"github.com/yaoapp/yao/neo/conversation"
|
||||||
"github.com/yaoapp/yao/neo/message"
|
"github.com/yaoapp/yao/neo/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetNeo returns the Neo instance
|
||||||
|
func GetNeo() *DSL {
|
||||||
|
if Neo == nil {
|
||||||
|
exception.New("Neo is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
return Neo
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
process.RegisterGroup("neo", map[string]process.Handler{
|
process.RegisterGroup("neo", map[string]process.Handler{
|
||||||
"write": ProcessWrite,
|
"write": ProcessWrite,
|
||||||
|
"assistant.create": processAssistantCreate,
|
||||||
|
"assistant.save": processAssistantSave,
|
||||||
|
"assistant.delete": processAssistantDelete,
|
||||||
|
"assistant.search": processAssistantSearch,
|
||||||
|
"assistant.find": processAssistantFind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,3 +56,151 @@ func ProcessWrite(process *process.Process) interface{} {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processAssistantCreate process the assistant create request
|
||||||
|
func processAssistantCreate(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
data := process.ArgsMap(0)
|
||||||
|
|
||||||
|
neo := GetNeo()
|
||||||
|
if neo.Conversation == nil {
|
||||||
|
exception.New("Neo conversation is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := neo.Conversation.SaveAssistant(data)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("Failed to create assistant: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAssistantSave process the assistant save request
|
||||||
|
func processAssistantSave(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
data := process.ArgsMap(0)
|
||||||
|
|
||||||
|
neo := GetNeo()
|
||||||
|
if neo.Conversation == nil {
|
||||||
|
exception.New("Neo conversation is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := neo.Conversation.SaveAssistant(data)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("Failed to save assistant: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAssistantDelete process the assistant delete request
|
||||||
|
func processAssistantDelete(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
assistantID := process.ArgsString(0)
|
||||||
|
|
||||||
|
neo := GetNeo()
|
||||||
|
if neo.Conversation == nil {
|
||||||
|
exception.New("Neo conversation is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := neo.Conversation.DeleteAssistant(assistantID)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("Failed to delete assistant: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return gin.H{"message": "ok"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAssistantSearch process the assistant search request
|
||||||
|
func processAssistantSearch(process *process.Process) interface{} {
|
||||||
|
params := process.ArgsMap(0)
|
||||||
|
filter := conversation.AssistantFilter{}
|
||||||
|
|
||||||
|
// Parse page and pagesize
|
||||||
|
if page, ok := params["page"]; ok {
|
||||||
|
pageStr := fmt.Sprintf("%v", page)
|
||||||
|
if pageInt, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
filter.Page = pageInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pagesize, ok := params["pagesize"]; ok {
|
||||||
|
pagesizeStr := fmt.Sprintf("%v", pagesize)
|
||||||
|
if pagesizeInt, err := strconv.Atoi(pagesizeStr); err == nil {
|
||||||
|
filter.PageSize = pagesizeInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tags
|
||||||
|
if tags, ok := params["tags"]; ok {
|
||||||
|
switch v := tags.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
filter.Tags = make([]string, len(v))
|
||||||
|
for i, tag := range v {
|
||||||
|
filter.Tags[i] = fmt.Sprintf("%v", tag)
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
filter.Tags = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse keywords
|
||||||
|
if keywords, ok := params["keywords"].(string); ok {
|
||||||
|
filter.Keywords = keywords
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse connector
|
||||||
|
if connector, ok := params["connector"].(string); ok {
|
||||||
|
filter.Connector = connector
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse mentionable
|
||||||
|
if mentionable, ok := params["mentionable"].(bool); ok {
|
||||||
|
filter.Mentionable = &mentionable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse automated
|
||||||
|
if automated, ok := params["automated"].(bool); ok {
|
||||||
|
filter.Automated = &automated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get assistants
|
||||||
|
neo := GetNeo()
|
||||||
|
if neo.Conversation == nil {
|
||||||
|
exception.New("Neo conversation is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := neo.Conversation.GetAssistants(filter)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("get assistants error: %s", 500, err).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// processAssistantFind process the assistant find request
|
||||||
|
func processAssistantFind(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
assistantID := process.ArgsString(0)
|
||||||
|
|
||||||
|
neo := GetNeo()
|
||||||
|
if neo.Conversation == nil {
|
||||||
|
exception.New("Neo conversation is not initialized", 500).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := conversation.AssistantFilter{
|
||||||
|
AssistantID: assistantID,
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := neo.Conversation.GetAssistants(filter)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("Failed to find assistant: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Data) == 0 {
|
||||||
|
exception.New("Assistant not found: %s", 404, assistantID).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Data[0]
|
||||||
|
}
|
||||||
|
|
|
||||||
513
neo/process_test.go
Normal file
513
neo/process_test.go
Normal file
|
|
@ -0,0 +1,513 @@
|
||||||
|
package neo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/kun/any"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepare(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
err := Load(config.Conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the test data before each test
|
||||||
|
p, err := process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"page": 1,
|
||||||
|
"pagesize": 1000, // Use a large page size to get all records
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output, err := p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res := any.Of(output).Map()
|
||||||
|
items := res.Get("data")
|
||||||
|
if items != nil {
|
||||||
|
for _, item := range items.([]map[string]interface{}) {
|
||||||
|
assistantID := item["assistant_id"].(string)
|
||||||
|
p, err = process.Of("neo.assistant.delete", assistantID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify cleanup
|
||||||
|
p, err = process.Of("neo.assistant.search")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res = any.Of(output).Map()
|
||||||
|
total := res.Get("total")
|
||||||
|
if total != nil && any.Of(total).CInt() > 0 {
|
||||||
|
t.Fatalf("Failed to clean up test data, %d records remaining", any.Of(total).CInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
check(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessAssistantCRUD(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create an assistant with string JSON fields
|
||||||
|
tagsJSON := `["tag1", "tag2", "tag3"]`
|
||||||
|
optionsJSON := `{"model": "gpt-4"}`
|
||||||
|
assistant := map[string]interface{}{
|
||||||
|
"name": "Test Assistant",
|
||||||
|
"type": "assistant",
|
||||||
|
"avatar": "https://example.com/avatar.png",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description",
|
||||||
|
"tags": tagsJSON,
|
||||||
|
"options": optionsJSON,
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test processAssistantCreate with string JSON
|
||||||
|
p, err := process.Of("neo.assistant.create", assistant)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID := output
|
||||||
|
assert.NotNil(t, assistantID)
|
||||||
|
|
||||||
|
// Test processAssistantFind
|
||||||
|
p, err = process.Of("neo.assistant.find", assistantID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAssistant := output.(map[string]interface{})
|
||||||
|
assert.Equal(t, assistantID, foundAssistant["assistant_id"])
|
||||||
|
assert.Equal(t, "Test Assistant", foundAssistant["name"])
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, foundAssistant["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, foundAssistant["options"])
|
||||||
|
|
||||||
|
// Test processAssistantFind with non-existent ID
|
||||||
|
p, err = process.Of("neo.assistant.find", "non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Assistant not found")
|
||||||
|
|
||||||
|
// Test with native type JSON fields
|
||||||
|
assistant2 := map[string]interface{}{
|
||||||
|
"name": "Test Assistant 2",
|
||||||
|
"type": "assistant",
|
||||||
|
"avatar": "https://example.com/avatar2.png",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description 2",
|
||||||
|
"tags": []string{"tag1", "tag2", "tag3"},
|
||||||
|
"options": map[string]interface{}{"model": "gpt-4"},
|
||||||
|
"prompts": []string{"prompt1", "prompt2"},
|
||||||
|
"flows": []string{"flow1", "flow2"},
|
||||||
|
"files": []string{"file1", "file2"},
|
||||||
|
"functions": []map[string]interface{}{{"name": "func1"}, {"name": "func2"}},
|
||||||
|
"permissions": map[string]interface{}{"read": true, "write": true},
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test processAssistantCreate with native types
|
||||||
|
p, err = process.Of("neo.assistant.create", assistant2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant2ID := output
|
||||||
|
assert.NotNil(t, assistant2ID)
|
||||||
|
|
||||||
|
// Test with nil JSON fields
|
||||||
|
assistant3 := map[string]interface{}{
|
||||||
|
"name": "Test Assistant 3",
|
||||||
|
"type": "assistant",
|
||||||
|
"connector": "openai",
|
||||||
|
"description": "Test Description 3",
|
||||||
|
"tags": nil,
|
||||||
|
"options": nil,
|
||||||
|
"prompts": nil,
|
||||||
|
"flows": nil,
|
||||||
|
"files": nil,
|
||||||
|
"functions": nil,
|
||||||
|
"permissions": nil,
|
||||||
|
"mentionable": true,
|
||||||
|
"automated": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test processAssistantCreate with nil fields
|
||||||
|
p, err = process.Of("neo.assistant.create", assistant3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant3ID := output
|
||||||
|
assert.NotNil(t, assistant3ID)
|
||||||
|
|
||||||
|
// Test processAssistantSearch to verify all assistants
|
||||||
|
p, err = process.Of("neo.assistant.search")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchRes := any.Of(output).Map()
|
||||||
|
total := searchRes.Get("total")
|
||||||
|
if total == nil {
|
||||||
|
total = int64(0)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(3), total)
|
||||||
|
|
||||||
|
items := searchRes.Get("data")
|
||||||
|
if items == nil {
|
||||||
|
items = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 3, len(items.([]map[string]interface{})))
|
||||||
|
|
||||||
|
// Verify each assistant's JSON fields
|
||||||
|
for _, item := range items.([]map[string]interface{}) {
|
||||||
|
switch item["assistant_id"].(string) {
|
||||||
|
case assistantID:
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
|
||||||
|
case assistant2ID:
|
||||||
|
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
|
||||||
|
assert.Equal(t, []interface{}{"prompt1", "prompt2"}, item["prompts"])
|
||||||
|
assert.Equal(t, []interface{}{"flow1", "flow2"}, item["flows"])
|
||||||
|
assert.Equal(t, []interface{}{"file1", "file2"}, item["files"])
|
||||||
|
assert.Equal(t,
|
||||||
|
[]interface{}{
|
||||||
|
map[string]interface{}{"name": "func1"},
|
||||||
|
map[string]interface{}{"name": "func2"},
|
||||||
|
},
|
||||||
|
item["functions"])
|
||||||
|
assert.Equal(t,
|
||||||
|
map[string]interface{}{
|
||||||
|
"read": true,
|
||||||
|
"write": true,
|
||||||
|
},
|
||||||
|
item["permissions"])
|
||||||
|
case assistant3ID:
|
||||||
|
assert.Nil(t, item["tags"])
|
||||||
|
assert.Nil(t, item["options"])
|
||||||
|
assert.Nil(t, item["prompts"])
|
||||||
|
assert.Nil(t, item["flows"])
|
||||||
|
assert.Nil(t, item["files"])
|
||||||
|
assert.Nil(t, item["functions"])
|
||||||
|
assert.Nil(t, item["permissions"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test updating with mixed JSON formats
|
||||||
|
assistant2["assistant_id"] = assistant2ID
|
||||||
|
assistant2["tags"] = `["tag4", "tag5"]`
|
||||||
|
assistant2["options"] = map[string]interface{}{"model": "gpt-3.5"}
|
||||||
|
p, err = process.Of("neo.assistant.save", assistant2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
savedID := output
|
||||||
|
assert.NotNil(t, savedID)
|
||||||
|
|
||||||
|
// Double check with a new search
|
||||||
|
p, err = process.Of("neo.assistant.search")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchRes = any.Of(output).Map()
|
||||||
|
items = searchRes.Get("data")
|
||||||
|
found := false
|
||||||
|
for _, item := range items.([]map[string]interface{}) {
|
||||||
|
if item["assistant_id"].(string) == assistant2ID {
|
||||||
|
found = true
|
||||||
|
assert.Equal(t, []interface{}{"tag4", "tag5"}, item["tags"])
|
||||||
|
assert.Equal(t, map[string]interface{}{"model": "gpt-3.5"}, item["options"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
// Test processAssistantDelete
|
||||||
|
p, err = process.Of("neo.assistant.delete", assistantID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRes := any.Of(output).Map()
|
||||||
|
assert.Equal(t, "ok", deleteRes.Get("message"))
|
||||||
|
|
||||||
|
// Delete remaining assistants
|
||||||
|
p, err = process.Of("neo.assistant.delete", assistant2ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
p, err = process.Of("neo.assistant.delete", assistant3ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
// Verify all assistants are deleted
|
||||||
|
p, err = process.Of("neo.assistant.search")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchRes = any.Of(output).Map()
|
||||||
|
total = searchRes.Get("total")
|
||||||
|
if total == nil {
|
||||||
|
total = int64(0)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(0), total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessAssistantSearchPagination(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Create multiple assistants for pagination testing
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
assistant := map[string]interface{}{
|
||||||
|
"name": fmt.Sprintf("Assistant %d", i),
|
||||||
|
"type": "assistant",
|
||||||
|
"connector": fmt.Sprintf("connector%d", i%3),
|
||||||
|
"description": fmt.Sprintf("Description %d", i),
|
||||||
|
"tags": []string{fmt.Sprintf("tag%d", i%5)},
|
||||||
|
"mentionable": i%2 == 0,
|
||||||
|
"automated": i%3 == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := process.Of("neo.assistant.create", assistant)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test first page
|
||||||
|
p, err := process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"page": 1,
|
||||||
|
"pagesize": 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := any.Of(output).Map()
|
||||||
|
total := res.Get("total")
|
||||||
|
if total == nil {
|
||||||
|
total = int64(0)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(25), total)
|
||||||
|
|
||||||
|
items := res.Get("data")
|
||||||
|
if items == nil {
|
||||||
|
items = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 10, len(items.([]map[string]interface{})))
|
||||||
|
|
||||||
|
pageCnt := res.Get("pagecnt")
|
||||||
|
if pageCnt == nil {
|
||||||
|
pageCnt = 1
|
||||||
|
}
|
||||||
|
assert.Equal(t, 3, pageCnt)
|
||||||
|
|
||||||
|
// Test second page
|
||||||
|
p, err = process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"page": 2,
|
||||||
|
"pagesize": 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = any.Of(output).Map()
|
||||||
|
items = res.Get("data")
|
||||||
|
if items == nil {
|
||||||
|
items = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 10, len(items.([]map[string]interface{})))
|
||||||
|
|
||||||
|
// Test last page
|
||||||
|
p, err = process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"page": 3,
|
||||||
|
"pagesize": 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = any.Of(output).Map()
|
||||||
|
items = res.Get("data")
|
||||||
|
if items == nil {
|
||||||
|
items = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 5, len(items.([]map[string]interface{})))
|
||||||
|
|
||||||
|
// Test filtering with tags
|
||||||
|
p, err = process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"tags": []string{"tag0"},
|
||||||
|
"page": 1,
|
||||||
|
"pagesize": 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = any.Of(output).Map()
|
||||||
|
items = res.Get("data")
|
||||||
|
if items == nil {
|
||||||
|
items = []map[string]interface{}{}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 5, len(items.([]map[string]interface{})))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessAssistantValidation(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Test missing required fields
|
||||||
|
p, err := process.Of("neo.assistant.create", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// Test invalid assistant ID for delete
|
||||||
|
p, err = process.Of("neo.assistant.delete", "non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// Test invalid assistant ID for find
|
||||||
|
p, err = process.Of("neo.assistant.find", "non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Assistant not found")
|
||||||
|
|
||||||
|
// Test invalid page number
|
||||||
|
p, err = process.Of("neo.assistant.search", map[string]interface{}{
|
||||||
|
"page": -1,
|
||||||
|
"pagesize": 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := p.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
res := any.Of(output).Map()
|
||||||
|
total := res.Get("total")
|
||||||
|
if total == nil {
|
||||||
|
total = int64(0)
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(0), total)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -82,6 +83,50 @@ func Prepare(t *testing.T, cfg config.Config, rootEnv ...string) {
|
||||||
// cfg.DataRoot = filepath.Join(root, "data")
|
// cfg.DataRoot = filepath.Join(root, "data")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
var appData []byte
|
||||||
|
var appFile string
|
||||||
|
|
||||||
|
// Read app setting
|
||||||
|
if has, _ := application.App.Exists("app.yao"); has {
|
||||||
|
appFile = "app.yao"
|
||||||
|
appData, err = application.App.Read("app.yao")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if has, _ := application.App.Exists("app.jsonc"); has {
|
||||||
|
appFile = "app.jsonc"
|
||||||
|
appData, err = application.App.Read("app.jsonc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if has, _ := application.App.Exists("app.json"); has {
|
||||||
|
appFile = "app.json"
|
||||||
|
appData, err = application.App.Read("app.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatal(fmt.Errorf("app.yao or app.jsonc or app.json does not exists"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace $ENV with os.Getenv
|
||||||
|
var envRe = regexp.MustCompile(`\$ENV\.([0-9a-zA-Z_-]+)`)
|
||||||
|
appData = envRe.ReplaceAllFunc(appData, func(s []byte) []byte {
|
||||||
|
key := string(s[5:])
|
||||||
|
val := os.Getenv(key)
|
||||||
|
if val == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return []byte(val)
|
||||||
|
})
|
||||||
|
share.App = share.AppInfo{}
|
||||||
|
err = application.Parse(appFile, appData, &share.App)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
utils.Init()
|
utils.Init()
|
||||||
dbconnect(t, cfg)
|
dbconnect(t, cfg)
|
||||||
load(t, cfg)
|
load(t, cfg)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue