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/google/uuid"
|
||||
"github.com/yaoapp/gou/api"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/helper"
|
||||
"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+"/download", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/mentions", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/generate", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/generate/title", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/generate/prompts", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/dangerous/clear_chats", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/assistants", neo.optionsHandler)
|
||||
router.OPTIONS(path+"/assistants/:id", neo.optionsHandler)
|
||||
|
||||
// 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.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)...)
|
||||
|
||||
// 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)...)
|
||||
|
||||
// Get chat details example:
|
||||
// curl -X GET 'http://localhost:5099/api/__yao/neo/chats/chat_123?token=xxx'
|
||||
router.GET(path+"/chats/:id", append(middlewares, neo.handleChatDetail)...)
|
||||
|
||||
// Update chat example:
|
||||
// curl -X POST 'http://localhost:5099/api/__yao/neo/chats/chat_123' \
|
||||
// -H 'Content-Type: application/json' \
|
||||
// -d '{"title": "New Title", "content": "Chat content for title generation", "token": "xxx"}'
|
||||
router.POST(path+"/chats/:id", append(middlewares, neo.handleChatUpdate)...)
|
||||
|
||||
// Delete chat example:
|
||||
// curl -X DELETE 'http://localhost:5099/api/__yao/neo/chats/chat_123?token=xxx'
|
||||
router.DELETE(path+"/chats/:id", append(middlewares, neo.handleChatDelete)...)
|
||||
|
||||
// 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)...)
|
||||
|
||||
// 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)...)
|
||||
|
||||
// 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)...)
|
||||
|
||||
// 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)...)
|
||||
|
||||
// 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
|
||||
// Clear all chats example:
|
||||
// curl -X DELETE 'http://localhost:5099/api/__yao/neo/dangerous/clear_chats?token=xxx'
|
||||
router.DELETE(path+"/dangerous/clear_chats", append(middlewares, neo.handleChatsDeleteAll)...)
|
||||
|
||||
return nil
|
||||
|
|
@ -383,49 +472,35 @@ func (neo *DSL) handleMentions(c *gin.Context) {
|
|||
|
||||
// Get keywords from query parameter
|
||||
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 {
|
||||
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
||||
c.Done()
|
||||
return
|
||||
}
|
||||
|
||||
// Add test data
|
||||
testMentions := []Mention{
|
||||
{
|
||||
ID: "assistant_1",
|
||||
Name: "Alice AI",
|
||||
Type: "assistant",
|
||||
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice",
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
// Convert assistants to mentions
|
||||
mentions := []Mention{}
|
||||
for _, item := range response.Data {
|
||||
mention := Mention{
|
||||
ID: item["assistant_id"].(string),
|
||||
Name: item["name"].(string),
|
||||
Type: item["type"].(string),
|
||||
Avatar: item["avatar"].(string),
|
||||
}
|
||||
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.Done()
|
||||
}
|
||||
|
|
@ -462,7 +537,7 @@ func (neo *DSL) handleChatUpdate(c *gin.Context) {
|
|||
ctx, cancel := NewContextWithCancel(sid, c.Query("chat_id"), "")
|
||||
defer cancel()
|
||||
|
||||
title, err := neo.GenerateChatTitle(ctx, body.Content, c)
|
||||
title, err := neo.GenerateChatTitle(ctx, body.Content, c, true)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
|
||||
c.Done()
|
||||
|
|
@ -534,3 +609,417 @@ func (neo *DSL) handleChatsDeleteAll(c *gin.Context) {
|
|||
c.JSON(200, gin.H{"message": "ok"})
|
||||
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 (
|
||||
"context"
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
)
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("api is not initialized")
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package base
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -31,7 +31,7 @@ var AllowedFileTypes = map[string]string{
|
|||
var MaxSize int64 = 20 * 1024 * 1024
|
||||
|
||||
// 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
|
||||
if file.Size > MaxSize {
|
||||
|
|
@ -69,13 +69,13 @@ func (ast *Base) Upload(ctx context.Context, file *multipart.FileHeader, reader
|
|||
}, 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")
|
||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ func (ast *Base) allowed(contentType string) bool {
|
|||
}
|
||||
|
||||
// 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
|
||||
data, err := fs.Get("data")
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package base
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -8,16 +8,16 @@ import (
|
|||
"github.com/yaoapp/yao/openai"
|
||||
)
|
||||
|
||||
// Base the base assistant
|
||||
type Base struct {
|
||||
// Local the local assistant
|
||||
type Local struct {
|
||||
ID string `json:"assistant_id"`
|
||||
Prompts []assistant.Prompt `json:"prompts,omitempty"`
|
||||
Connector connector.Connector `json:"-" yaml:"-"`
|
||||
openai *openai.OpenAI
|
||||
}
|
||||
|
||||
// New create a new base assistant
|
||||
func New(connector connector.Connector, prompts []assistant.Prompt, id string) (*Base, error) {
|
||||
// New create a new local assistant
|
||||
func New(connector connector.Connector, prompts []assistant.Prompt, id string) (*Local, error) {
|
||||
|
||||
setting := connector.Setting()
|
||||
api, err := openai.NewOpenAI(setting)
|
||||
|
|
@ -25,10 +25,10 @@ func New(connector connector.Connector, prompts []assistant.Prompt, id string) (
|
|||
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
|
||||
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
|
||||
}
|
||||
|
|
@ -30,13 +30,16 @@ type QueryParam struct {
|
|||
|
||||
// Assistant the assistant
|
||||
type Assistant struct {
|
||||
ID string `json:"assistant_id"` // Assistant ID
|
||||
Name string `json:"name,omitempty"` // Assistant Name
|
||||
Connector string `json:"connector"` // AI Connector
|
||||
Description string `json:"description,omitempty"` // Assistant Description
|
||||
Option map[string]interface{} `json:"option,omitempty"` // AI Option
|
||||
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
|
||||
API API `json:"-" yaml:"-"` // Assistant API
|
||||
ID string `json:"assistant_id"` // Assistant ID
|
||||
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
|
||||
Name string `json:"name,omitempty"` // Assistant Name
|
||||
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
|
||||
Connector string `json:"connector"` // AI Connector
|
||||
Description string `json:"description,omitempty"` // Assistant Description
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,60 +1,59 @@
|
|||
package conversation
|
||||
|
||||
// Mongo conversation
|
||||
// Mongo represents a MongoDB-based conversation storage
|
||||
type Mongo struct{}
|
||||
|
||||
// NewMongo create a new conversation
|
||||
// NewMongo creates a new MongoDB conversation storage
|
||||
func NewMongo() *Mongo {
|
||||
return &Mongo{}
|
||||
}
|
||||
|
||||
// UpdateChatTitle update the chat title
|
||||
func (conv *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
return nil
|
||||
// GetChats retrieves a list of chats
|
||||
func (m *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{}, nil
|
||||
}
|
||||
|
||||
// GetChats get the chat list
|
||||
func (conv *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{
|
||||
Groups: []ChatGroup{},
|
||||
Page: filter.Page,
|
||||
PageSize: filter.PageSize,
|
||||
Total: 0,
|
||||
LastPage: 1,
|
||||
}, nil
|
||||
// GetChat retrieves a single chat's information
|
||||
func (m *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return &ChatInfo{}, nil
|
||||
}
|
||||
|
||||
// GetHistory get the history
|
||||
func (conv *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
// GetHistory retrieves chat history
|
||||
func (m *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
// SaveHistory save the history
|
||||
func (conv *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
||||
// SaveHistory saves chat history
|
||||
func (m *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRequest get the request
|
||||
func (conv *Mongo) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveRequest save the request
|
||||
func (conv *Mongo) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
||||
// DeleteChat deletes a single chat
|
||||
func (m *Mongo) DeleteChat(sid string, cid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChat get the chat info and its history
|
||||
func (conv *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteChat deletes a specific chat and its history
|
||||
func (conv *Mongo) DeleteChat(sid string, cid string) error {
|
||||
// DeleteAllChats deletes all chats
|
||||
func (m *Mongo) DeleteAllChats(sid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllChats deletes all chats and their histories for a user
|
||||
func (conv *Mongo) DeleteAllChats(sid string) error {
|
||||
// UpdateChatTitle updates chat title
|
||||
func (m *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
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
|
||||
|
||||
// Redis conversation
|
||||
// Redis represents a Redis-based conversation storage
|
||||
type Redis struct{}
|
||||
|
||||
// NewRedis create a new conversation
|
||||
// NewRedis creates a new Redis conversation storage
|
||||
func NewRedis() *Redis {
|
||||
return &Redis{}
|
||||
}
|
||||
|
||||
// UpdateChatTitle update the chat title
|
||||
func (conv *Redis) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
return nil
|
||||
// GetChats retrieves a list of chats
|
||||
func (r *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{}, nil
|
||||
}
|
||||
|
||||
// GetChats get the chat list
|
||||
func (conv *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{
|
||||
Groups: []ChatGroup{},
|
||||
Page: filter.Page,
|
||||
PageSize: filter.PageSize,
|
||||
Total: 0,
|
||||
LastPage: 1,
|
||||
}, nil
|
||||
// GetChat retrieves a single chat's information
|
||||
func (r *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return &ChatInfo{}, nil
|
||||
}
|
||||
|
||||
// GetHistory get the history
|
||||
func (conv *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
// GetHistory retrieves chat history
|
||||
func (r *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
// SaveHistory save the history
|
||||
func (conv *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
||||
// SaveHistory saves chat history
|
||||
func (r *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRequest get the request
|
||||
func (conv *Redis) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveRequest save the request
|
||||
func (conv *Redis) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
||||
// DeleteChat deletes a single chat
|
||||
func (r *Redis) DeleteChat(sid string, cid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChat get the chat info and its history
|
||||
func (conv *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteChat deletes a specific chat and its history
|
||||
func (conv *Redis) DeleteChat(sid string, cid string) error {
|
||||
// DeleteAllChats deletes all chats
|
||||
func (r *Redis) DeleteAllChats(sid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllChats deletes all chats and their histories for a user
|
||||
func (conv *Redis) DeleteAllChats(sid string) error {
|
||||
// UpdateChatTitle updates chat title
|
||||
func (r *Redis) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
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
|
||||
|
||||
// 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 {
|
||||
Connector string `json:"connector,omitempty"`
|
||||
UserField string `json:"user_field,omitempty"` // the user id field name, default is user_id
|
||||
Table string `json:"table,omitempty"`
|
||||
MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"`
|
||||
TTL int `json:"ttl,omitempty" yaml:"ttl,omitempty"`
|
||||
Connector string `json:"connector,omitempty"` // Name of the connector used to specify data storage method
|
||||
UserField string `json:"user_field,omitempty"` // User ID field name, defaults to "user_id"
|
||||
Table string `json:"table,omitempty"` // Database table name
|
||||
MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"` // Maximum storage size limit
|
||||
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 {
|
||||
Chat map[string]interface{} `json:"chat"`
|
||||
History []map[string]interface{} `json:"history"`
|
||||
Chat map[string]interface{} `json:"chat"` // Basic chat information
|
||||
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 {
|
||||
Keywords string `json:"keywords,omitempty"`
|
||||
Page int `json:"page,omitempty"` // 页码,从1开始
|
||||
PageSize int `json:"pagesize,omitempty"` // 每页数量
|
||||
Order string `json:"order,omitempty"` // desc/asc
|
||||
Keywords string `json:"keywords,omitempty"` // Keyword search
|
||||
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||
PageSize int `json:"pagesize,omitempty"` // Number of items per page
|
||||
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 {
|
||||
Label string `json:"label"`
|
||||
Chats []map[string]interface{} `json:"chats"`
|
||||
Label string `json:"label"` // Group label (typically a date)
|
||||
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 {
|
||||
Groups []ChatGroup `json:"groups"`
|
||||
Page int `json:"page"` // 当前页码
|
||||
PageSize int `json:"pagesize"` // 每页数量
|
||||
Total int64 `json:"total"` // 总记录数
|
||||
LastPage int `json:"last_page"` // 最后一页页码
|
||||
Groups []ChatGroup `json:"groups"` // List of chat groups
|
||||
Page int `json:"page"` // Current page number
|
||||
PageSize int `json:"pagesize"` // Items per page
|
||||
Total int64 `json:"total"` // Total number of records
|
||||
LastPage int `json:"last_page"` // Last page number
|
||||
}
|
||||
|
||||
// Conversation the store interface
|
||||
type Conversation interface {
|
||||
GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error)
|
||||
GetChat(sid string, cid string) (*ChatInfo, error)
|
||||
GetHistory(sid string, cid string) ([]map[string]interface{}, error)
|
||||
SaveHistory(sid string, messages []map[string]interface{}, cid string) error
|
||||
GetRequest(sid string, rid string) ([]map[string]interface{}, error)
|
||||
SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error
|
||||
DeleteChat(sid string, cid string) error
|
||||
DeleteAllChats(sid string) error
|
||||
UpdateChatTitle(sid string, cid string, title string) error
|
||||
// AssistantFilter represents the assistant filter structure
|
||||
// Used for filtering and pagination when retrieving assistant lists
|
||||
type AssistantFilter struct {
|
||||
Tags []string `json:"tags,omitempty"` // Filter by tags
|
||||
Keywords string `json:"keywords,omitempty"` // Search in name and description
|
||||
Connector string `json:"connector,omitempty"` // Filter by connector
|
||||
AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID
|
||||
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||
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
|
||||
|
||||
// Weaviate Database conversation
|
||||
// Weaviate represents a Weaviate-based conversation storage
|
||||
type Weaviate struct{}
|
||||
|
||||
// NewWeaviate create a new conversation
|
||||
// NewWeaviate creates a new Weaviate conversation storage
|
||||
func NewWeaviate() *Weaviate {
|
||||
return &Weaviate{}
|
||||
}
|
||||
|
||||
// UpdateChatTitle update the chat title
|
||||
func (conv *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
return nil
|
||||
// GetChats retrieves a list of chats
|
||||
func (w *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{}, nil
|
||||
}
|
||||
|
||||
// GetChats get the chat list
|
||||
func (conv *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
|
||||
return &ChatGroupResponse{
|
||||
Groups: []ChatGroup{},
|
||||
Page: filter.Page,
|
||||
PageSize: filter.PageSize,
|
||||
Total: 0,
|
||||
LastPage: 1,
|
||||
}, nil
|
||||
// GetChat retrieves a single chat's information
|
||||
func (w *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return &ChatInfo{}, nil
|
||||
}
|
||||
|
||||
// GetHistory get the history
|
||||
func (conv *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
// GetHistory retrieves chat history
|
||||
func (w *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
|
||||
return []map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
// SaveHistory save the history
|
||||
func (conv *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string) error {
|
||||
// SaveHistory saves chat history
|
||||
func (w *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRequest get the request
|
||||
func (conv *Weaviate) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SaveRequest save the request
|
||||
func (conv *Weaviate) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
|
||||
// DeleteChat deletes a single chat
|
||||
func (w *Weaviate) DeleteChat(sid string, cid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChat get the chat info and its history
|
||||
func (conv *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteChat deletes a specific chat and its history
|
||||
func (conv *Weaviate) DeleteChat(sid string, cid string) error {
|
||||
// DeleteAllChats deletes all chats
|
||||
func (w *Weaviate) DeleteAllChats(sid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAllChats deletes all chats and their histories for a user
|
||||
func (conv *Weaviate) DeleteAllChats(sid string) error {
|
||||
// UpdateChatTitle updates chat title
|
||||
func (w *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
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"
|
||||
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/gou/session"
|
||||
"github.com/yaoapp/kun/log"
|
||||
|
|
@ -15,32 +16,34 @@ import (
|
|||
"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 {
|
||||
query query.Query
|
||||
schema schema.Schema
|
||||
setting Setting
|
||||
}
|
||||
|
||||
type row struct {
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"` // User name
|
||||
Content string `json:"content"`
|
||||
Sid string `json:"sid"`
|
||||
Rid string `json:"rid"`
|
||||
Cid string `json:"cid"` // Chat ID from chat history
|
||||
ExpiredAt interface{} `json:"expired_at"`
|
||||
}
|
||||
|
||||
// Public interface methods and constructor remain exported:
|
||||
// - NewXun
|
||||
// - UpdateChatTitle
|
||||
// - GetChats
|
||||
// - GetChat
|
||||
// - GetHistory
|
||||
// - SaveHistory
|
||||
// - GetRequest
|
||||
// - SaveRequest
|
||||
// Public interface methods:
|
||||
//
|
||||
// NewXun creates a new conversation instance with the given settings
|
||||
// UpdateChatTitle updates the title of a specific chat
|
||||
// GetChats retrieves a paginated list of chats grouped by date
|
||||
// GetChat retrieves a specific chat and its message history
|
||||
// GetHistory retrieves the message history for a specific chat
|
||||
// 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
|
||||
// SaveAssistant creates or updates an assistant
|
||||
// DeleteAssistant deletes an assistant by assistant_id
|
||||
// GetAssistants retrieves a paginated list of assistants with filtering
|
||||
|
||||
// NewXun create a new conversation
|
||||
func NewXun(setting Setting) (*Xun, error) {
|
||||
|
|
@ -111,6 +114,11 @@ func (conv *Xun) initialize() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Initialize assistant table
|
||||
if err := conv.initAssistantTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -126,11 +134,12 @@ func (conv *Xun) initHistoryTable() error {
|
|||
err = conv.schema.CreateTable(historyTable, func(table schema.Blueprint) {
|
||||
table.ID("id")
|
||||
table.String("sid", 255).Index()
|
||||
table.String("rid", 255).Null().Index()
|
||||
table.String("cid", 200).Null().Index()
|
||||
table.String("uid", 255).Null().Index()
|
||||
table.String("role", 200).Null().Index()
|
||||
table.String("name", 200).Null().Index()
|
||||
table.Text("content").Null()
|
||||
table.JSON("context").Null()
|
||||
table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index()
|
||||
table.TimestampTz("updated_at").Null().Index()
|
||||
table.TimestampTz("expired_at").Null().Index()
|
||||
|
|
@ -148,7 +157,7 @@ func (conv *Xun) initHistoryTable() error {
|
|||
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 {
|
||||
if !tab.HasColumn(field) {
|
||||
return fmt.Errorf("%s is required", field)
|
||||
|
|
@ -198,6 +207,59 @@ func (conv *Xun) initChatTable() error {
|
|||
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) {
|
||||
field := "user_id"
|
||||
if conv.setting.UserField != "" {
|
||||
|
|
@ -217,13 +279,17 @@ func (conv *Xun) getUserID(sid string) (string, error) {
|
|||
}
|
||||
|
||||
func (conv *Xun) getHistoryTable() string {
|
||||
return conv.setting.Table
|
||||
return conv.setting.Table + "_history"
|
||||
}
|
||||
|
||||
func (conv *Xun) getChatTable() string {
|
||||
return conv.setting.Table + "_chat"
|
||||
}
|
||||
|
||||
func (conv *Xun) getAssistantTable() string {
|
||||
return conv.setting.Table + "_assistant"
|
||||
}
|
||||
|
||||
// UpdateChatTitle update the chat title
|
||||
func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error {
|
||||
userID, err := conv.getUserID(sid)
|
||||
|
|
@ -380,7 +446,7 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e
|
|||
}
|
||||
|
||||
qb := conv.newQuery().
|
||||
Select("role", "name", "content").
|
||||
Select("role", "name", "content", "context", "uid", "created_at", "updated_at").
|
||||
Where("sid", userID).
|
||||
Where("cid", cid).
|
||||
OrderBy("id", "desc")
|
||||
|
|
@ -401,18 +467,23 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e
|
|||
|
||||
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...)
|
||||
message := map[string]interface{}{
|
||||
"role": row.Get("role"),
|
||||
"name": row.Get("name"),
|
||||
"content": row.Get("content"),
|
||||
"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
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
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
|
||||
defer conv.clean()
|
||||
var expiredAt interface{} = nil
|
||||
values := []row{}
|
||||
values := []map[string]interface{}{}
|
||||
if conv.setting.TTL > 0 {
|
||||
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, message := range messages {
|
||||
value := row{
|
||||
Role: message["role"].(string),
|
||||
Name: "",
|
||||
Content: message["content"].(string),
|
||||
Sid: userID,
|
||||
Cid: cid,
|
||||
ExpiredAt: expiredAt,
|
||||
// Type assertion safety checks
|
||||
role, ok := message["role"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid role type in message: %v", message["role"])
|
||||
}
|
||||
|
||||
if message["name"] != nil {
|
||||
value.Name = message["name"].(string)
|
||||
content, ok := message["content"].(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)
|
||||
}
|
||||
|
||||
|
|
@ -479,79 +575,6 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid
|
|||
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
|
||||
func (conv *Xun) GetChat(sid string, cid string) (*ChatInfo, error) {
|
||||
userID, err := conv.getUserID(sid)
|
||||
|
|
@ -638,3 +661,265 @@ func (conv *Xun) DeleteAllChats(sid string) error {
|
|||
Delete()
|
||||
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"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/xun/capsule"
|
||||
|
|
@ -15,13 +16,28 @@ import (
|
|||
func TestNewXunDefault(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
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 {
|
||||
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{
|
||||
Connector: "default",
|
||||
Table: "__unit_test_conversation",
|
||||
|
|
@ -32,35 +48,59 @@ func TestNewXunDefault(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
has, err := capsule.Schema().HasTable("__unit_test_conversation")
|
||||
// Check history table
|
||||
has, err := capsule.Schema().HasTable("__unit_test_conversation_history")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, true, has)
|
||||
|
||||
// validate the table
|
||||
tab, err := conv.schema.GetTable(conv.setting.Table)
|
||||
// Check chat 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 {
|
||||
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 {
|
||||
assert.Equal(t, true, tab.HasColumn(field))
|
||||
}
|
||||
|
||||
conv, err = NewXun(Setting{
|
||||
Connector: "default",
|
||||
Table: "__unit_test_conversation",
|
||||
})
|
||||
|
||||
has, err = capsule.Schema().HasTable("__unit_test_conversation")
|
||||
// validate the chat table
|
||||
tab, err = conv.schema.GetTable(conv.getChatTable())
|
||||
if err != nil {
|
||||
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) {
|
||||
|
|
@ -77,9 +117,17 @@ func TestNewXunConnector(t *testing.T) {
|
|||
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{
|
||||
Connector: "mysql",
|
||||
Table: "__unit_test_conversation",
|
||||
|
|
@ -90,44 +138,73 @@ func TestNewXunConnector(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
has, err := sch.HasTable("__unit_test_conversation")
|
||||
// Check history table
|
||||
has, err := sch.HasTable("__unit_test_conversation_history")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, true, has)
|
||||
|
||||
// validate the table
|
||||
tab, err := conv.schema.GetTable(conv.setting.Table)
|
||||
// Check chat 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 {
|
||||
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 {
|
||||
assert.Equal(t, true, tab.HasColumn(field))
|
||||
}
|
||||
|
||||
conv, err = NewXun(Setting{
|
||||
Connector: "default",
|
||||
Table: "__unit_test_conversation",
|
||||
})
|
||||
|
||||
has, err = sch.HasTable("__unit_test_conversation")
|
||||
// validate the chat table
|
||||
tab, err = conv.schema.GetTable(conv.getChatTable())
|
||||
if err != nil {
|
||||
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) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -143,7 +220,7 @@ func TestXunSaveAndGetHistory(t *testing.T) {
|
|||
err = conv.SaveHistory("123456", []map[string]interface{}{
|
||||
{"role": "user", "name": "user1", "content": "hello"},
|
||||
{"role": "assistant", "name": "user1", "content": "Hello there, how"},
|
||||
}, cid)
|
||||
}, cid, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// get the history
|
||||
|
|
@ -154,44 +231,18 @@ func TestXunSaveAndGetHistory(t *testing.T) {
|
|||
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) {
|
||||
test.Prepare(t, config.Conf)
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -209,7 +260,7 @@ func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
|||
{"role": "user", "name": "user1", "content": "hello"},
|
||||
{"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)
|
||||
|
||||
// get the history for specific cid
|
||||
|
|
@ -224,7 +275,7 @@ func TestXunSaveAndGetHistoryWithCID(t *testing.T) {
|
|||
moreMessages := []map[string]interface{}{
|
||||
{"role": "user", "name": "user1", "content": "another message"},
|
||||
}
|
||||
err = conv.SaveHistory(sid, moreMessages, anotherCID)
|
||||
err = conv.SaveHistory(sid, moreMessages, anotherCID, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// 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) {
|
||||
test.Prepare(t, config.Conf)
|
||||
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")
|
||||
|
||||
// Drop both tables before test
|
||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation")
|
||||
err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -294,7 +345,7 @@ func TestXunGetChats(t *testing.T) {
|
|||
}
|
||||
|
||||
// Then save the history
|
||||
err = conv.SaveHistory(sid, messages, chatID)
|
||||
err = conv.SaveHistory(sid, messages, chatID, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -325,7 +376,7 @@ func TestXunGetChats(t *testing.T) {
|
|||
func TestXunDeleteChat(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
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")
|
||||
|
||||
conv, err := NewXun(Setting{
|
||||
|
|
@ -344,7 +395,7 @@ func TestXunDeleteChat(t *testing.T) {
|
|||
}
|
||||
|
||||
// Save the chat and history
|
||||
err = conv.SaveHistory(sid, messages, cid)
|
||||
err = conv.SaveHistory(sid, messages, cid, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Verify chat exists
|
||||
|
|
@ -365,7 +416,7 @@ func TestXunDeleteChat(t *testing.T) {
|
|||
func TestXunDeleteAllChats(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
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")
|
||||
|
||||
conv, err := NewXun(Setting{
|
||||
|
|
@ -385,7 +436,7 @@ func TestXunDeleteAllChats(t *testing.T) {
|
|||
// Save multiple chats
|
||||
for i := 0; i < 3; 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)
|
||||
}
|
||||
|
||||
|
|
@ -403,3 +454,423 @@ func TestXunDeleteAllChats(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
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/kun/log"
|
||||
"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/conversation"
|
||||
"github.com/yaoapp/yao/neo/message"
|
||||
|
|
@ -58,20 +58,50 @@ func (neo *DSL) GetMentions(keywords string) ([]Mention, error) {
|
|||
return neo.HookMention(context.Background(), keywords)
|
||||
}
|
||||
|
||||
// GenerateChatTitle generate the chat title
|
||||
func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context) (string, error) {
|
||||
|
||||
// GeneratePrompts generate prompts for the AI assistant
|
||||
func (neo *DSL) GeneratePrompts(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.
|
||||
Optimize the prompts for the AI assistant
|
||||
1. Optimize prompts based on the user's input
|
||||
2. The prompts should be clear and specific
|
||||
3. The prompts should be in the same language as the input
|
||||
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{}{
|
||||
{"role": "system", "content": prompts},
|
||||
{"role": "user", "content": input},
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": input,
|
||||
"type": messageType,
|
||||
"name": ctx.Sid,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
if msg.Message.Done {
|
||||
if !silent && msg.Message.Text == "" {
|
||||
msg.Write(c.Writer)
|
||||
}
|
||||
done <- true
|
||||
return 0 // break
|
||||
}
|
||||
|
|
@ -131,7 +174,9 @@ func (neo *DSL) GenerateChatTitle(ctx Context, input string, c *gin.Context) (st
|
|||
|
||||
if err != nil {
|
||||
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
|
||||
|
|
@ -372,9 +417,9 @@ func (neo *DSL) newAssistantByConnector(id string) (assistant.API, error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
|
@ -453,6 +498,7 @@ func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages
|
|||
{"role": "assistant", "content": string(content), "name": sid},
|
||||
},
|
||||
chatID,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
167
neo/process.go
167
neo/process.go
|
|
@ -1,15 +1,32 @@
|
|||
package neo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/exception"
|
||||
"github.com/yaoapp/yao/neo/conversation"
|
||||
"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() {
|
||||
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
|
||||
}
|
||||
|
||||
// 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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -82,6 +83,50 @@ func Prepare(t *testing.T, cfg config.Config, rootEnv ...string) {
|
|||
// 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()
|
||||
dbconnect(t, cfg)
|
||||
load(t, cfg)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue