Refactor context handling in agent API and assistant methods
- Updated context creation in handleChat, handleGenerateTitle, and handleGeneratePrompts to use the request context, improving context management. - Commented out unused context settings for assistant ID, silent mode, history visibility, and client type to streamline the code. - Refactored context handling in assistant methods to enhance clarity and maintainability. - Removed deprecated functions and cleaned up the context structure for better performance and readability.
This commit is contained in:
parent
af383dd941
commit
1c502bfea8
21 changed files with 934 additions and 1250 deletions
58
agent/api.go
58
agent/api.go
|
|
@ -176,33 +176,33 @@ func (agent *DSL) handleChat(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Set the context with validated chat_id
|
||||
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
|
||||
ctx, cancel := chatctx.NewWithCancel(c.Request.Context(), nil, chatID, c.Query("context"))
|
||||
defer cancel()
|
||||
defer ctx.Release() // Release the context after the request is done
|
||||
|
||||
// Set the assistant ID
|
||||
assistantID := c.Query("assistant_id")
|
||||
if assistantID != "" {
|
||||
ctx = chatctx.WithAssistantID(ctx, assistantID)
|
||||
}
|
||||
// // Set the assistant ID
|
||||
// assistantID := c.Query("assistant_id")
|
||||
// if assistantID != "" {
|
||||
// ctx = chatctx.WithAssistantID(ctx, assistantID)
|
||||
// }
|
||||
|
||||
// Set the silent mode
|
||||
silent := c.Query("silent")
|
||||
if silent == "true" || silent == "1" {
|
||||
ctx = chatctx.WithSilent(ctx, true)
|
||||
}
|
||||
// // Set the silent mode
|
||||
// silent := c.Query("silent")
|
||||
// if silent == "true" || silent == "1" {
|
||||
// ctx = chatctx.WithSilent(ctx, true)
|
||||
// }
|
||||
|
||||
// Set the history visible
|
||||
historyVisible := c.Query("history_visible")
|
||||
if historyVisible != "" {
|
||||
ctx = chatctx.WithHistoryVisible(ctx, historyVisible == "true" || historyVisible == "1")
|
||||
}
|
||||
// // Set the history visible
|
||||
// historyVisible := c.Query("history_visible")
|
||||
// if historyVisible != "" {
|
||||
// ctx = chatctx.WithHistoryVisible(ctx, historyVisible == "true" || historyVisible == "1")
|
||||
// }
|
||||
|
||||
// Set the client type
|
||||
clientType := c.Query("client_type")
|
||||
if clientType != "" {
|
||||
ctx = chatctx.WithClientType(ctx, clientType)
|
||||
}
|
||||
// // Set the client type
|
||||
// clientType := c.Query("client_type")
|
||||
// if clientType != "" {
|
||||
// ctx = chatctx.WithClientType(ctx, clientType)
|
||||
// }
|
||||
|
||||
err := agent.Answer(ctx, content, c)
|
||||
|
||||
|
|
@ -664,13 +664,13 @@ func (agent *DSL) handleGenerateTitle(c *gin.Context) {
|
|||
chatID := fmt.Sprintf("generate_title_%d", time.Now().UnixNano())
|
||||
|
||||
// Set the context with validated chat_id
|
||||
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
|
||||
ctx, cancel := chatctx.NewWithCancel(c.Request.Context(), nil, chatID, c.Query("context"))
|
||||
defer cancel()
|
||||
defer ctx.Release() // Release the context after the request is done
|
||||
|
||||
// Set the assistant ID
|
||||
ctx = chatctx.WithHistoryVisible(ctx, false)
|
||||
ctx = chatctx.WithAssistantID(ctx, agent.Use.Title)
|
||||
// // Set the assistant ID
|
||||
// ctx = chatctx.WithHistoryVisible(ctx, false)
|
||||
// ctx = chatctx.WithAssistantID(ctx, agent.Use.Title)
|
||||
|
||||
err := agent.Answer(ctx, content, c)
|
||||
|
||||
|
|
@ -704,13 +704,13 @@ func (agent *DSL) handleGeneratePrompts(c *gin.Context) {
|
|||
chatID := fmt.Sprintf("generate_prompts_%d", time.Now().UnixNano())
|
||||
|
||||
// Set the context with validated chat_id
|
||||
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
|
||||
ctx, cancel := chatctx.NewWithCancel(c.Request.Context(), nil, chatID, c.Query("context"))
|
||||
defer cancel()
|
||||
defer ctx.Release() // Release the context after the request is done
|
||||
|
||||
// Set the assistant ID
|
||||
ctx = chatctx.WithHistoryVisible(ctx, false)
|
||||
ctx = chatctx.WithAssistantID(ctx, agent.Use.Prompt)
|
||||
// // Set the assistant ID
|
||||
// ctx = chatctx.WithHistoryVisible(ctx, false)
|
||||
// ctx = chatctx.WithAssistantID(ctx, agent.Use.Prompt)
|
||||
err := agent.Answer(ctx, content, c)
|
||||
|
||||
// Error handling
|
||||
|
|
|
|||
|
|
@ -91,9 +91,9 @@ func (ast *Assistant) execute(c *gin.Context, ctx chatctx.Context, userInput int
|
|||
|
||||
// Add RAG、Vision and Search support
|
||||
// ctx.RAG = rag != nil
|
||||
ctx.Knowledge = false
|
||||
ctx.Vision = ast.vision
|
||||
ctx.Search = ast.search && search != nil
|
||||
// ctx.Knowledge = false
|
||||
// ctx.Vision = ast.vision
|
||||
// ctx.Search = ast.search && search != nil
|
||||
|
||||
// Run init hook
|
||||
res, err := ast.HookCreate(c, ctx, input, options, contents)
|
||||
|
|
|
|||
|
|
@ -242,8 +242,8 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
chatCtx.AssistantID = assistantID
|
||||
chatCtx.ChatID = fmt.Sprintf("call_%s", uuid.New().String()) // New chat id
|
||||
chatCtx.Silent = options.Silent
|
||||
chatCtx.ClientType = chatctx.ClientTypeAgent // Set the client type to agent
|
||||
chatCtx.Args = goArgs // Arguments for call
|
||||
chatCtx.Referer = chatctx.RefererScript // Set the referer to hookscript
|
||||
chatCtx.Args = goArgs // Arguments for call
|
||||
|
||||
// Define the callback function
|
||||
var cb func(msg *chatMessage.Message) = nil
|
||||
|
|
@ -306,157 +306,6 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return jsResult
|
||||
}
|
||||
|
||||
// Deprecated this function is not used anymore
|
||||
// Use the hook retry instead
|
||||
// func (obj *objectCall) retry(jsArgs []v8go.Valuer, err error, input interface{}, output []chatMessage.Message, info *v8go.FunctionCallbackInfo, options OptionsCall) (*v8go.Value, error) {
|
||||
|
||||
// // Retry times, if not set, return the error
|
||||
// if options.Retry.Times <= 0 {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// this := info.This()
|
||||
// errmsg := exception.Trim(err)
|
||||
|
||||
// // Get current retry times
|
||||
// jsTimes, retryErr := this.Get("retry_times")
|
||||
// if retryErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to get the retry times: %s", errmsg, retryErr.Error())
|
||||
// }
|
||||
|
||||
// times := int(jsTimes.Int32())
|
||||
// if times > options.Retry.Times {
|
||||
// return nil, fmt.Errorf("%s occurred, max retry times reached", errmsg)
|
||||
// }
|
||||
|
||||
// // Update the retry times
|
||||
// times = times + 1
|
||||
// this.Set("retry_times", int32(times))
|
||||
|
||||
// // Message content
|
||||
// content := ""
|
||||
// for _, msg := range output {
|
||||
// if msg.Type == "text" && msg.IsDelta {
|
||||
// content += msg.Text
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Delay
|
||||
// delay := options.Retry.Delay * int(times)
|
||||
// if delay > options.Retry.DelayMax {
|
||||
// delay = options.Retry.DelayMax
|
||||
// }
|
||||
|
||||
// // Wait for the delay
|
||||
// if delay > 0 {
|
||||
// time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
// }
|
||||
|
||||
// // Format the input
|
||||
// var lastUserMessage *chatMessage.Message = nil
|
||||
// var inputMessages []*chatMessage.Message = nil
|
||||
// var lastUserMessageIndex int = 0
|
||||
// switch v := input.(type) {
|
||||
// case string:
|
||||
// lastUserMessage = &chatMessage.Message{Type: "text", Text: v, Role: "user"}
|
||||
// inputMessages = []*chatMessage.Message{lastUserMessage}
|
||||
|
||||
// case []interface{}:
|
||||
// // Get the last user message
|
||||
// raw, parseErr := jsoniter.Marshal(v)
|
||||
// if parseErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to marshal the input: %s", errmsg, parseErr.Error())
|
||||
// }
|
||||
|
||||
// parseErr = jsoniter.Unmarshal(raw, &inputMessages)
|
||||
// if parseErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to unmarshal the input: %s", errmsg, parseErr.Error())
|
||||
// }
|
||||
|
||||
// // Get the last user message
|
||||
// for i := len(inputMessages) - 1; i >= 0; i-- {
|
||||
// if inputMessages[i].Type == "text" && inputMessages[i].Role == "user" {
|
||||
// lastUserMessage = inputMessages[i]
|
||||
// lastUserMessageIndex = i
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// case *chatMessage.Message:
|
||||
// lastUserMessage = v
|
||||
// inputMessages = []*chatMessage.Message{lastUserMessage}
|
||||
|
||||
// case map[string]interface{}:
|
||||
// text, ok := v["text"].(string)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to get the text", errmsg)
|
||||
// }
|
||||
|
||||
// if v["role"] != "user" {
|
||||
// return nil, fmt.Errorf("%s occurred but the role is not user", errmsg)
|
||||
// }
|
||||
|
||||
// lastUserMessage = &chatMessage.Message{Type: "text", Text: text, Role: "user"}
|
||||
// inputMessages = []*chatMessage.Message{lastUserMessage}
|
||||
// }
|
||||
|
||||
// // Get the prompt from the options
|
||||
// promptTmpl := options.Retry.Prompt
|
||||
// data := sui.Data{
|
||||
// "error": errmsg,
|
||||
// "output": strings.TrimSpace(content),
|
||||
// "input": lastUserMessage.Text,
|
||||
// }
|
||||
// prompt, _ := data.Replace(promptTmpl)
|
||||
|
||||
// // Custom retry prompt by hooking the retry event
|
||||
// if this.Has("on_retry") {
|
||||
// info.Context().Global().Set("error", errmsg) // Set error
|
||||
// jsDelay, _ := bridge.JsValue(info.Context(), delay)
|
||||
// jsPrompt, _ := bridge.JsValue(info.Context(), prompt)
|
||||
// newPrompt, retryErr := obj.trigger(info, "retry", jsTimes, jsDelay, jsPrompt)
|
||||
// if retryErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to trigger the retry event: %s", errmsg, retryErr.Error())
|
||||
// }
|
||||
// // Update the prompt
|
||||
// if newPrompt.IsString() {
|
||||
// prompt = newPrompt.String()
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Generate the new input with the prompt
|
||||
// // Update the input
|
||||
// inputMessages[lastUserMessageIndex].Text = prompt
|
||||
// jsInput, inputErr := bridge.JsValue(info.Context(), inputMessages)
|
||||
// if inputErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to update the input: %s", errmsg, inputErr.Error())
|
||||
// }
|
||||
// // Update the input
|
||||
// this.Set("retry_input", jsInput)
|
||||
|
||||
// // Call the run function
|
||||
// run, funcErr := this.Get("Run")
|
||||
// if funcErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to get the run function: %s", errmsg, funcErr.Error())
|
||||
// }
|
||||
|
||||
// if !run.IsFunction() {
|
||||
// return nil, fmt.Errorf("%s occurred but the run function is not a function", errmsg)
|
||||
// }
|
||||
|
||||
// fn, fnErr := run.AsFunction()
|
||||
// if fnErr != nil {
|
||||
// return nil, fmt.Errorf("%s occurred but failed to get the run function: %s", errmsg, fnErr.Error())
|
||||
// }
|
||||
|
||||
// result, resErr := fn.Call(this, jsArgs...)
|
||||
// if resErr != nil {
|
||||
// return nil, fmt.Errorf("%s (%d)", exception.Trim(resErr), times-1)
|
||||
// }
|
||||
|
||||
// return result, nil
|
||||
// }
|
||||
|
||||
func (obj *objectCall) triggerAnonymous(chatCtx chatctx.Context, global *GlobalVariables, goCallProps map[string]interface{}, source string, bindArgs []interface{}, fnArgs ...interface{}) error {
|
||||
|
||||
ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
|
||||
|
|
@ -510,192 +359,3 @@ func (obj *objectCall) trigger(info *v8go.FunctionCallbackInfo, name string, fnA
|
|||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// jsCallBackup is the backup function for the call function
|
||||
// func jsCallBackup(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||
|
||||
// // Get the args
|
||||
// args := info.Args()
|
||||
// if len(args) < 2 {
|
||||
// return bridge.JsException(info.Context(), "Run requires at least two arguments")
|
||||
// }
|
||||
|
||||
// // Get the assistant id
|
||||
// assistantID := args[0].String()
|
||||
|
||||
// // Get the assistant
|
||||
// newAst, err := Get(assistantID)
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
|
||||
// // Get the input
|
||||
// input := args[1].String()
|
||||
|
||||
// // Get the global variables
|
||||
// global, err := global(info)
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
|
||||
// // Update Context
|
||||
// chatContext := global.ChatContext
|
||||
// chatContext.AssistantID = assistantID
|
||||
// chatContext.ChatID = fmt.Sprintf("chat_%s", uuid.New().String()) // New chat id
|
||||
// chatContext.Silent = true // Silent mode
|
||||
|
||||
// var cb func(msg *chatMessage.Message) = nil
|
||||
// if len(args) > 2 {
|
||||
|
||||
// // Rest args
|
||||
// var jsArgs *v8go.Value
|
||||
// goArgs := []interface{}{}
|
||||
// if len(args) > 3 {
|
||||
// jsArgs = args[3]
|
||||
// if jsArgs != nil {
|
||||
// if jsArgs.IsArray() {
|
||||
// v, err := bridge.GoValue(jsArgs, info.Context())
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
// arr, ok := v.([]interface{})
|
||||
// if !ok {
|
||||
// return bridge.JsException(info.Context(), "Invalid arguments")
|
||||
// }
|
||||
// goArgs = arr
|
||||
// } else {
|
||||
// v, err := bridge.GoValue(jsArgs, info.Context())
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
// goArgs = []interface{}{v}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Parse the callback
|
||||
// funcType := "method"
|
||||
// name := ""
|
||||
// userArgs := []interface{}{}
|
||||
// if args[2].IsFunction() {
|
||||
// funcType = "anonymous"
|
||||
// } else {
|
||||
// goValue, err := bridge.GoValue(args[2], info.Context())
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
// switch v := goValue.(type) {
|
||||
// case string:
|
||||
// name = v
|
||||
// case map[string]interface{}:
|
||||
// if fname, ok := v["name"].(string); ok {
|
||||
// name = fname
|
||||
// }
|
||||
// if args, ok := v["args"].([]interface{}); ok {
|
||||
// userArgs = args
|
||||
// }
|
||||
// }
|
||||
|
||||
// if strings.Contains(name, ".") {
|
||||
// funcType = "process"
|
||||
// }
|
||||
// }
|
||||
|
||||
// switch funcType {
|
||||
// case "anonymous":
|
||||
// source := args[2].String()
|
||||
// cb = func(msg *chatMessage.Message) {
|
||||
// cbArgs := []interface{}{msg}
|
||||
// cbArgs = append(cbArgs, goArgs...)
|
||||
// ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
|
||||
// if err != nil {
|
||||
// fmt.Println("Failed to create context", err.Error())
|
||||
// return
|
||||
// }
|
||||
// defer ctx.Close()
|
||||
|
||||
// global.Assistant.InitObject(ctx, global.GinContext, chatContext, global.Contents)
|
||||
// _, err = ctx.CallAnonymousWith(context.Background(), source, cbArgs...)
|
||||
// if err != nil {
|
||||
// log.Error("Failed to call the method: %s", err.Error())
|
||||
// color.Red("Failed to call the method: %s", err.Error())
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// break
|
||||
|
||||
// case "process":
|
||||
|
||||
// cb = func(msg *chatMessage.Message) {
|
||||
// cbArgs := []interface{}{}
|
||||
// cbArgs = append(cbArgs, msg)
|
||||
// cbArgs = append(cbArgs, userArgs...)
|
||||
// p, err := process.Of(name, cbArgs...)
|
||||
// if err != nil {
|
||||
// log.Error("Failed to get the process: %s", err.Error())
|
||||
// color.Red("Failed to get the process: %s", err.Error())
|
||||
// return
|
||||
// }
|
||||
// err = p.Execute()
|
||||
// if err != nil {
|
||||
// log.Error("Failed to execute the process: %s", err.Error())
|
||||
// color.Red("Failed to execute the process: %s", err.Error())
|
||||
// return
|
||||
// }
|
||||
// defer p.Release()
|
||||
// }
|
||||
|
||||
// case "method":
|
||||
|
||||
// cb = func(msg *chatMessage.Message) {
|
||||
// cbArgs := []interface{}{}
|
||||
// cbArgs = append(cbArgs, msg)
|
||||
// cbArgs = append(cbArgs, userArgs...)
|
||||
// ctx, err := global.Assistant.Script.NewContext(global.ChatContext.Sid, nil)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer ctx.Close()
|
||||
|
||||
// global.Assistant.InitObject(ctx, global.GinContext, global.ChatContext, global.Contents)
|
||||
// _, err = ctx.CallWith(context.Background(), name, cbArgs...)
|
||||
// if err != nil {
|
||||
// log.Error("Failed to call the method: %s", err.Error())
|
||||
// color.Red("Failed to call the method: %s", err.Error())
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// // Parse the options
|
||||
// options := map[string]interface{}{}
|
||||
// if len(args) > 4 {
|
||||
// optionsRaw, err := bridge.GoValue(args[4], info.Context())
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
|
||||
// // Parse the options
|
||||
// if optionsRaw != nil {
|
||||
// switch v := optionsRaw.(type) {
|
||||
// case string:
|
||||
// err := jsoniter.UnmarshalFromString(v, &options)
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
// case map[string]interface{}:
|
||||
// options = v
|
||||
// default:
|
||||
// return bridge.JsException(info.Context(), "Invalid options")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// err = newAst.Execute(global.GinContext, chatContext, input, options, cb) // Execute the assistant
|
||||
// if err != nil {
|
||||
// return bridge.JsException(info.Context(), err.Error())
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ func jsSet(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
||||
if global.ChatContext.SharedSpace == nil {
|
||||
if global.ChatContext.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Shared space is not set")
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ func jsSet(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
}
|
||||
|
||||
// Set the value
|
||||
err = global.ChatContext.SharedSpace.Set(key, value)
|
||||
err = global.ChatContext.Space.Set(key, value)
|
||||
if err != nil {
|
||||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ func jsGet(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
||||
if global.ChatContext.SharedSpace == nil {
|
||||
if global.ChatContext.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Shared space is not set")
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ func jsGet(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
}
|
||||
|
||||
// Get the value
|
||||
value, err := global.ChatContext.SharedSpace.Get(key)
|
||||
value, err := global.ChatContext.Space.Get(key)
|
||||
if err != nil {
|
||||
// If the key is not found, return null
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
|
|
@ -168,7 +168,7 @@ func jsDel(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
||||
if global.ChatContext.SharedSpace == nil {
|
||||
if global.ChatContext.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Shared space is not set")
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ func jsDel(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return bridge.JsException(info.Context(), "Get requires a valid key")
|
||||
}
|
||||
|
||||
err = global.ChatContext.SharedSpace.Delete(key)
|
||||
err = global.ChatContext.Space.Delete(key)
|
||||
if err != nil {
|
||||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
|
@ -201,11 +201,11 @@ func jsClear(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
||||
if global.ChatContext.SharedSpace == nil {
|
||||
if global.ChatContext.Space == nil {
|
||||
return bridge.JsException(info.Context(), "Shared space is not set")
|
||||
}
|
||||
|
||||
err = global.ChatContext.SharedSpace.Clear()
|
||||
err = global.ChatContext.Space.Clear()
|
||||
if err != nil {
|
||||
return bridge.JsException(info.Context(), err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,115 +2,27 @@ package context
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
// Context the context
|
||||
type Context struct {
|
||||
context.Context
|
||||
Sid string `json:"sid" yaml:"-"` // Session ID
|
||||
ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat
|
||||
AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant
|
||||
|
||||
// CUI Context
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Stack string `json:"stack,omitempty"` // will be removed in the future
|
||||
Path string `json:"pathname,omitempty"` // wiil be rename to path
|
||||
FormData map[string]interface{} `json:"formdata,omitempty"`
|
||||
Field *Field `json:"field,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Signal interface{} `json:"signal,omitempty"`
|
||||
|
||||
// Locale information
|
||||
Locale string `json:"locale,omitempty"` // Locale
|
||||
Theme string `json:"theme,omitempty"` // Theme
|
||||
|
||||
Silent bool `json:"silent,omitempty"` // Silent mode
|
||||
ClientType string `json:"client_type,omitempty"` // The request client type. SDK, Desktop, Web, JSSDK, etc. the default is Web
|
||||
HistoryVisible bool `json:"history_visible,omitempty"` // History visible, default is true, if false, the history will not be displayed in the UI
|
||||
Retry bool `json:"retry,omitempty"` // Retry mode
|
||||
RetryTimes uint8 `json:"retry_times,omitempty"` // Retry times
|
||||
|
||||
Vision bool `json:"vision,omitempty"` // Vision support
|
||||
Search bool `json:"search,omitempty"` // Search support
|
||||
Knowledge bool `json:"knowledge,omitempty"` // Knowledge support
|
||||
|
||||
Args []interface{} `json:"args,omitempty"` // Arguments for call
|
||||
SharedSpace plan.Space `json:"-"` // Shared space
|
||||
}
|
||||
|
||||
// Field the context field
|
||||
type Field struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Bind string `json:"bind,omitempty"`
|
||||
Props map[string]interface{} `json:"props,omitempty"`
|
||||
Children []interface{} `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// FileUpload the file upload
|
||||
type FileUpload struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
TempFile string `json:"temp_file,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
// ClientTypeAgent is the client type for Agent
|
||||
ClientTypeAgent = "agent"
|
||||
|
||||
// ClientTypeWeb is the client type for Web (Default UI)
|
||||
ClientTypeWeb = "web"
|
||||
|
||||
// ClientTypeAndroid is the client type for Android
|
||||
ClientTypeAndroid = "android"
|
||||
|
||||
// ClientTypeIOS is the client type for IOS
|
||||
ClientTypeIOS = "ios"
|
||||
|
||||
// ClientTypeJSSDK is the client type for JSSDK
|
||||
ClientTypeJSSDK = "jssdk"
|
||||
|
||||
// ClientTypeMacOS is the client type for MacOS Desktop
|
||||
ClientTypeMacOS = "macos"
|
||||
|
||||
// ClientTypeWindows is the client type for Windows Desktop
|
||||
ClientTypeWindows = "windows"
|
||||
|
||||
// ClientTypeLinux is the client type for Linux Desktop
|
||||
ClientTypeLinux = "linux"
|
||||
)
|
||||
|
||||
// SupportedClientTypes is the supported client types
|
||||
var SupportedClientTypes = map[string]bool{
|
||||
ClientTypeAgent: true,
|
||||
ClientTypeWeb: true,
|
||||
ClientTypeAndroid: true,
|
||||
ClientTypeIOS: true,
|
||||
ClientTypeJSSDK: true,
|
||||
ClientTypeMacOS: true,
|
||||
ClientTypeWindows: true,
|
||||
ClientTypeLinux: true,
|
||||
}
|
||||
|
||||
// New create a new context
|
||||
func New(sid, cid, payload string) Context {
|
||||
func New(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string) Context {
|
||||
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
|
||||
// Validate the client type
|
||||
ctx := Context{
|
||||
Context: context.Background(),
|
||||
SharedSpace: plan.NewMemorySharedSpace(),
|
||||
Sid: sid,
|
||||
ChatID: cid,
|
||||
HistoryVisible: true,
|
||||
ClientType: ClientTypeWeb,
|
||||
Silent: false,
|
||||
Context: parent,
|
||||
Space: plan.NewMemorySharedSpace(),
|
||||
ChatID: chatID,
|
||||
}
|
||||
|
||||
if payload == "" {
|
||||
|
|
@ -126,49 +38,14 @@ func New(sid, cid, payload string) Context {
|
|||
}
|
||||
|
||||
// NewWithCancel create a new context with cancel
|
||||
func NewWithCancel(sid, cid, payload string) (Context, context.CancelFunc) {
|
||||
ctx := New(sid, cid, payload)
|
||||
func NewWithCancel(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string) (Context, context.CancelFunc) {
|
||||
ctx := New(parent, authorized, chatID, payload)
|
||||
return WithCancel(ctx)
|
||||
}
|
||||
|
||||
// WithAssistantID set the assistant ID
|
||||
func WithAssistantID(ctx Context, assistantID string) Context {
|
||||
ctx.AssistantID = assistantID
|
||||
return ctx
|
||||
}
|
||||
|
||||
// WithSilent set the silent mode
|
||||
func WithSilent(ctx Context, silent bool) Context {
|
||||
ctx.Silent = silent
|
||||
return ctx
|
||||
}
|
||||
|
||||
// WithClientType set the client type
|
||||
func WithClientType(ctx Context, clientType string) Context {
|
||||
// Validate the client type
|
||||
if !SupportedClientTypes[clientType] {
|
||||
log.Error("[Agent] Invalid client type: %s", clientType)
|
||||
return ctx
|
||||
}
|
||||
ctx.ClientType = clientType
|
||||
return ctx
|
||||
}
|
||||
|
||||
// WithLocale set the locale
|
||||
func WithLocale(ctx Context, locale string) Context {
|
||||
ctx.Locale = locale
|
||||
return ctx
|
||||
}
|
||||
|
||||
// WithHistoryVisible set the history visible
|
||||
func WithHistoryVisible(ctx Context, historyVisible bool) Context {
|
||||
ctx.HistoryVisible = historyVisible
|
||||
return ctx
|
||||
}
|
||||
|
||||
// NewWithTimeout create a new context with timeout
|
||||
func NewWithTimeout(sid, cid, payload string, timeout time.Duration) (Context, context.CancelFunc) {
|
||||
ctx := New(sid, cid, payload)
|
||||
func NewWithTimeout(parent context.Context, authorized *types.AuthorizedInfo, chatID, payload string, timeout time.Duration) (Context, context.CancelFunc) {
|
||||
ctx := New(parent, authorized, chatID, payload)
|
||||
return WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
|
|
@ -188,78 +65,69 @@ func WithTimeout(parent Context, timeout time.Duration) (Context, context.Cancel
|
|||
|
||||
// Release the context
|
||||
func (ctx *Context) Release() {
|
||||
ctx.SharedSpace.Clear()
|
||||
ctx.SharedSpace = nil
|
||||
ctx.Space.Clear()
|
||||
ctx.Space = nil
|
||||
ctx = nil
|
||||
}
|
||||
|
||||
// Map the context to a map
|
||||
func (ctx *Context) Map() map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"sid": ctx.Sid,
|
||||
"knowledge": ctx.Knowledge,
|
||||
"vision": ctx.Vision,
|
||||
"search": ctx.Search,
|
||||
}
|
||||
data := map[string]interface{}{}
|
||||
|
||||
// Authorized information
|
||||
if ctx.ChatID != "" {
|
||||
data["chat_id"] = ctx.ChatID
|
||||
}
|
||||
if ctx.AssistantID != "" {
|
||||
data["assistant_id"] = ctx.AssistantID
|
||||
}
|
||||
if ctx.Stack != "" {
|
||||
data["stack"] = ctx.Stack
|
||||
|
||||
// Arguments for call
|
||||
if len(ctx.Args) > 0 {
|
||||
data["args"] = ctx.Args
|
||||
}
|
||||
|
||||
// Silent mode
|
||||
if ctx.Silent {
|
||||
data["silent"] = ctx.Silent
|
||||
}
|
||||
|
||||
// History visible
|
||||
data["history_visible"] = ctx.HistoryVisible
|
||||
|
||||
// Client type
|
||||
data["client_type"] = ctx.ClientType
|
||||
|
||||
// Retry mode
|
||||
if ctx.Retry {
|
||||
data["retry"] = ctx.Retry
|
||||
}
|
||||
|
||||
// Arguments for call
|
||||
if ctx.Args != nil && len(ctx.Args) > 0 {
|
||||
data["args"] = ctx.Args
|
||||
if ctx.RetryTimes > 0 {
|
||||
data["retry_times"] = ctx.RetryTimes
|
||||
}
|
||||
|
||||
// Retry times
|
||||
data["retry_times"] = ctx.RetryTimes
|
||||
|
||||
if ctx.Path != "" {
|
||||
data["pathname"] = ctx.Path
|
||||
// Locale information
|
||||
if ctx.Locale != "" {
|
||||
data["locale"] = ctx.Locale
|
||||
}
|
||||
if len(ctx.FormData) > 0 {
|
||||
data["formdata"] = ctx.FormData
|
||||
}
|
||||
if ctx.Field != nil {
|
||||
data["field"] = ctx.Field
|
||||
}
|
||||
if ctx.Namespace != "" {
|
||||
data["namespace"] = ctx.Namespace
|
||||
}
|
||||
if len(ctx.Config) > 0 {
|
||||
data["config"] = ctx.Config
|
||||
}
|
||||
if ctx.Signal != nil {
|
||||
data["signal"] = ctx.Signal
|
||||
if ctx.Theme != "" {
|
||||
data["theme"] = ctx.Theme
|
||||
}
|
||||
|
||||
// Locale
|
||||
data["locale"] = ctx.Locale
|
||||
// Request information
|
||||
if ctx.Client.Type != "" || ctx.Client.UserAgent != "" || ctx.Client.IP != "" {
|
||||
data["client"] = map[string]interface{}{
|
||||
"type": ctx.Client.Type,
|
||||
"user_agent": ctx.Client.UserAgent,
|
||||
"ip": ctx.Client.IP,
|
||||
}
|
||||
}
|
||||
if ctx.Referer != "" {
|
||||
data["referer"] = ctx.Referer
|
||||
}
|
||||
if ctx.Accept != "" {
|
||||
data["accept"] = ctx.Accept
|
||||
}
|
||||
|
||||
// Theme
|
||||
data["theme"] = ctx.Theme
|
||||
// CUI Context information
|
||||
if ctx.Route != "" {
|
||||
data["route"] = ctx.Route
|
||||
}
|
||||
if len(ctx.Data) > 0 {
|
||||
data["data"] = ctx.Data
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// GenChatID generate a new chat ID
|
||||
func GenChatID() string {
|
||||
return fmt.Sprintf("chat_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
|
|
|||
288
agent/context/context_test.go
Normal file
288
agent/context/context_test.go
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewGin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queryParams map[string]string
|
||||
routeParams map[string]string
|
||||
headers map[string]string
|
||||
expectedChatID string
|
||||
expectedAssistant string
|
||||
expectedLocale string
|
||||
expectedTheme string
|
||||
expectedClientType string
|
||||
expectedReferer string
|
||||
expectedAccept Accept
|
||||
}{
|
||||
{
|
||||
name: "Parse all query parameters",
|
||||
queryParams: map[string]string{
|
||||
"chat_id": "chat123",
|
||||
"locale": "zh-CN",
|
||||
"theme": "dark",
|
||||
"referer": RefererProcess,
|
||||
"accept": string(AcceptStandard),
|
||||
},
|
||||
routeParams: map[string]string{
|
||||
"assistant_id": "ast456",
|
||||
},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
||||
},
|
||||
expectedChatID: "chat123",
|
||||
expectedAssistant: "ast456",
|
||||
expectedLocale: "zh-CN",
|
||||
expectedTheme: "dark",
|
||||
expectedClientType: "macos",
|
||||
expectedReferer: RefererProcess,
|
||||
expectedAccept: AcceptStandard,
|
||||
},
|
||||
{
|
||||
name: "Default values with no parameters",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
expectedChatID: "",
|
||||
expectedAssistant: "",
|
||||
expectedLocale: "",
|
||||
expectedTheme: "",
|
||||
expectedClientType: "web",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AcceptWebCUI,
|
||||
},
|
||||
{
|
||||
name: "Android client type detection",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10)",
|
||||
},
|
||||
expectedClientType: "android",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AccepNativeCUI,
|
||||
},
|
||||
{
|
||||
name: "iOS client type detection",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)",
|
||||
},
|
||||
expectedClientType: "ios",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AccepNativeCUI,
|
||||
},
|
||||
{
|
||||
name: "Windows desktop client type detection",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0)",
|
||||
},
|
||||
expectedClientType: "windows",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AcceptDesktopCUI,
|
||||
},
|
||||
{
|
||||
name: "Agent client type detection",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Yao-Agent/1.0",
|
||||
},
|
||||
expectedClientType: "agent",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AcceptStandard,
|
||||
},
|
||||
{
|
||||
name: "JSSDK client type detection",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Yao-JSSDK/2.0",
|
||||
},
|
||||
expectedClientType: "jssdk",
|
||||
expectedReferer: RefererAPI,
|
||||
expectedAccept: AcceptStandard,
|
||||
},
|
||||
{
|
||||
name: "Custom headers for referer and accept",
|
||||
queryParams: map[string]string{},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"X-Yao-Referer": RefererMCP,
|
||||
"X-Yao-Accept": string(AcceptDesktopCUI),
|
||||
},
|
||||
expectedClientType: "web",
|
||||
expectedReferer: RefererMCP,
|
||||
expectedAccept: AcceptDesktopCUI,
|
||||
},
|
||||
{
|
||||
name: "Query parameters override headers",
|
||||
queryParams: map[string]string{
|
||||
"referer": RefererJSSDK,
|
||||
"accept": string(AcceptStandard),
|
||||
},
|
||||
headers: map[string]string{
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"X-Yao-Referer": RefererMCP,
|
||||
"X-Yao-Accept": string(AcceptDesktopCUI),
|
||||
},
|
||||
expectedClientType: "web",
|
||||
expectedReferer: RefererJSSDK,
|
||||
expectedAccept: AcceptStandard,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test server
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
// Build query string
|
||||
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
|
||||
q := req.URL.Query()
|
||||
for key, value := range tt.queryParams {
|
||||
q.Add(key, value)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// Set headers
|
||||
for key, value := range tt.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
c.Request = req
|
||||
|
||||
// Set route params
|
||||
for key, value := range tt.routeParams {
|
||||
c.Params = append(c.Params, gin.Param{Key: key, Value: value})
|
||||
}
|
||||
|
||||
// Call NewGin
|
||||
ctx := NewGin(c)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, tt.expectedChatID, ctx.ChatID, "ChatID mismatch")
|
||||
assert.Equal(t, tt.expectedAssistant, ctx.AssistantID, "AssistantID mismatch")
|
||||
assert.Equal(t, tt.expectedLocale, ctx.Locale, "Locale mismatch")
|
||||
assert.Equal(t, tt.expectedTheme, ctx.Theme, "Theme mismatch")
|
||||
assert.Equal(t, tt.expectedClientType, ctx.Client.Type, "Client.Type mismatch")
|
||||
assert.Equal(t, tt.expectedReferer, ctx.Referer, "Referer mismatch")
|
||||
assert.Equal(t, tt.expectedAccept, ctx.Accept, "Accept mismatch")
|
||||
assert.NotNil(t, ctx.Space, "Space should not be nil")
|
||||
// Client.UserAgent and Client.IP are set from headers/request, may be empty in test context
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClientType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userAgent string
|
||||
expected string
|
||||
}{
|
||||
{"Empty user agent", "", "web"},
|
||||
{"Standard web browser", "Mozilla/5.0", "web"},
|
||||
{"Android", "Mozilla/5.0 (Linux; Android 10)", "android"},
|
||||
{"iPhone", "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)", "ios"},
|
||||
{"iPad", "Mozilla/5.0 (iPad; CPU OS 14_0)", "ios"},
|
||||
{"Windows", "Mozilla/5.0 (Windows NT 10.0)", "windows"},
|
||||
{"macOS", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "macos"},
|
||||
{"Linux", "Mozilla/5.0 (X11; Linux x86_64)", "linux"},
|
||||
{"Yao Agent", "Yao-Agent/1.0", "agent"},
|
||||
{"JSSDK", "Yao-JSSDK/2.0", "jssdk"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseClientType(tt.userAgent)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAccept(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clientType string
|
||||
expected Accept
|
||||
}{
|
||||
{"Web client", "web", AcceptWebCUI},
|
||||
{"Android client", "android", AccepNativeCUI},
|
||||
{"iOS client", "ios", AccepNativeCUI},
|
||||
{"Windows client", "windows", AcceptDesktopCUI},
|
||||
{"macOS client", "macos", AcceptDesktopCUI},
|
||||
{"Linux client", "linux", AcceptDesktopCUI},
|
||||
{"Agent client", "agent", AcceptStandard},
|
||||
{"JSSDK client", "jssdk", AcceptStandard},
|
||||
{"Unknown client", "unknown", AcceptStandard},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseAccept(tt.clientType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAccept(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
accept string
|
||||
expected Accept
|
||||
}{
|
||||
{"Valid standard", "standard", AcceptStandard},
|
||||
{"Valid cui-web", "cui-web", AcceptWebCUI},
|
||||
{"Valid cui-native", "cui-native", AccepNativeCUI},
|
||||
{"Valid cui-desktop", "cui-desktop", AcceptDesktopCUI},
|
||||
{"Invalid value", "invalid", AcceptStandard},
|
||||
{"Empty string", "", AcceptStandard},
|
||||
{"Random string", "random-accept", AcceptStandard},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validateAccept(tt.accept)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateReferer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
referer string
|
||||
expected string
|
||||
}{
|
||||
{"Valid api", "api", RefererAPI},
|
||||
{"Valid process", "process", RefererProcess},
|
||||
{"Valid mcp", "mcp", RefererMCP},
|
||||
{"Valid jssdk", "jssdk", RefererJSSDK},
|
||||
{"Valid agent", "agent", RefererAgent},
|
||||
{"Valid tool", "tool", RefererTool},
|
||||
{"Valid hook", "hook", RefererHook},
|
||||
{"Valid schedule", "schedule", RefererSchedule},
|
||||
{"Valid script", "script", RefererScript},
|
||||
{"Valid internal", "internal", RefererInternal},
|
||||
{"Invalid value", "invalid", RefererAPI},
|
||||
{"Empty string", "", RefererAPI},
|
||||
{"Random string", "random-referer", RefererAPI},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validateReferer(tt.referer)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
81
agent/context/gin.go
Normal file
81
agent/context/gin.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
)
|
||||
|
||||
// NewGin create a new context from gin context
|
||||
func NewGin(c *gin.Context) Context {
|
||||
// Get authorized information
|
||||
authInfo := authorized.GetInfo(c)
|
||||
|
||||
// Extract parameters from query and route
|
||||
chatID := c.Query("chat_id")
|
||||
assistantID := c.Param("assistant_id") // Get from route parameter
|
||||
locale := c.Query("locale")
|
||||
theme := c.Query("theme")
|
||||
referer := c.Query("referer")
|
||||
accept := c.Query("accept")
|
||||
|
||||
// Parse client information from User-Agent header
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
clientType := parseClientType(userAgent)
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// Create base context
|
||||
ctx := Context{
|
||||
Context: c.Request.Context(),
|
||||
Space: plan.NewMemorySharedSpace(),
|
||||
Authorized: authInfo,
|
||||
ChatID: chatID,
|
||||
AssistantID: assistantID,
|
||||
Locale: locale,
|
||||
Theme: theme,
|
||||
Client: Client{
|
||||
Type: clientType,
|
||||
UserAgent: userAgent,
|
||||
IP: clientIP,
|
||||
},
|
||||
}
|
||||
|
||||
// Get Referer from query parameter, header, or default
|
||||
ctx.Referer = getValidatedValue(referer, c.GetHeader("X-Yao-Referer"), RefererAPI, validateReferer)
|
||||
|
||||
// Get Accept from query parameter, header, or default
|
||||
ctx.Accept = getValidatedAccept(accept, c.GetHeader("X-Yao-Accept"), clientType)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// parseClientType parses the client type from User-Agent header
|
||||
func parseClientType(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return "web" // Default to web
|
||||
}
|
||||
|
||||
ua := strings.ToLower(userAgent)
|
||||
|
||||
// Check for specific client types
|
||||
switch {
|
||||
case strings.Contains(ua, "yao-agent") || strings.Contains(ua, "agent"):
|
||||
return "agent"
|
||||
case strings.Contains(ua, "yao-jssdk") || strings.Contains(ua, "jssdk"):
|
||||
return "jssdk"
|
||||
case strings.Contains(ua, "android"):
|
||||
return "android"
|
||||
case strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") || strings.Contains(ua, "ipod"):
|
||||
return "ios"
|
||||
case strings.Contains(ua, "windows"):
|
||||
return "windows"
|
||||
case strings.Contains(ua, "mac os x") || strings.Contains(ua, "macintosh"):
|
||||
return "macos"
|
||||
case strings.Contains(ua, "linux"):
|
||||
return "linux"
|
||||
default:
|
||||
return "web"
|
||||
}
|
||||
}
|
||||
123
agent/context/types.go
Normal file
123
agent/context/types.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/yaoapp/gou/plan"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
// Accept the accept of the request, it will be used to identify the accept of the request.
|
||||
type Accept string
|
||||
|
||||
// Referer the referer of the request, it will be used to identify the referer of the request.
|
||||
type Referer string
|
||||
|
||||
// Client represents the client information from HTTP request
|
||||
type Client struct {
|
||||
Type string `json:"type,omitempty"` // Client type: web, android, ios, windows, macos, linux, agent, jssdk
|
||||
UserAgent string `json:"user_agent,omitempty"` // Original User-Agent header
|
||||
IP string `json:"ip,omitempty"` // Client IP address
|
||||
}
|
||||
|
||||
const (
|
||||
// AcceptStandard standard response format compatible with OpenAI API and general chat UIs (default)
|
||||
AcceptStandard = "standard"
|
||||
|
||||
// AcceptWebCUI web-based CUI format with action request support for Yao Chat User Interface
|
||||
AcceptWebCUI = "cui-web"
|
||||
|
||||
// AccepNativeCUI native mobile/tablet CUI format with action request support
|
||||
AccepNativeCUI = "cui-native"
|
||||
|
||||
// AcceptDesktopCUI desktop CUI format with action request support
|
||||
AcceptDesktopCUI = "cui-desktop"
|
||||
)
|
||||
|
||||
// ValidAccepts is the map of valid accept types
|
||||
var ValidAccepts = map[string]bool{
|
||||
AcceptStandard: true,
|
||||
AcceptWebCUI: true,
|
||||
AccepNativeCUI: true,
|
||||
AcceptDesktopCUI: true,
|
||||
}
|
||||
|
||||
const (
|
||||
// RefererAPI request from HTTP API endpoint
|
||||
RefererAPI = "api"
|
||||
|
||||
// RefererProcess request from Yao Process call
|
||||
RefererProcess = "process"
|
||||
|
||||
// RefererMCP request from MCP (Model Context Protocol) server
|
||||
RefererMCP = "mcp"
|
||||
|
||||
// RefererJSSDK request from JavaScript SDK
|
||||
RefererJSSDK = "jssdk"
|
||||
|
||||
// RefererAgent request from agent-to-agent recursive call (assistant calling another assistant)
|
||||
RefererAgent = "agent"
|
||||
|
||||
// RefererTool request from tool/function execution
|
||||
RefererTool = "tool"
|
||||
|
||||
// RefererHook request from hook trigger (on_message, on_error, etc.)
|
||||
RefererHook = "hook"
|
||||
|
||||
// RefererSchedule request from scheduled task or cron job
|
||||
RefererSchedule = "schedule"
|
||||
|
||||
// RefererScript request from custom script execution
|
||||
RefererScript = "script"
|
||||
|
||||
// RefererInternal request from internal system call
|
||||
RefererInternal = "internal"
|
||||
)
|
||||
|
||||
// ValidReferers is the map of valid referer types
|
||||
var ValidReferers = map[string]bool{
|
||||
RefererAPI: true,
|
||||
RefererProcess: true,
|
||||
RefererMCP: true,
|
||||
RefererJSSDK: true,
|
||||
RefererAgent: true,
|
||||
RefererTool: true,
|
||||
RefererHook: true,
|
||||
RefererSchedule: true,
|
||||
RefererScript: true,
|
||||
RefererInternal: true,
|
||||
}
|
||||
|
||||
// Context the context
|
||||
type Context struct {
|
||||
|
||||
// Context
|
||||
context.Context
|
||||
Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call
|
||||
|
||||
// Authorized information
|
||||
Authorized *types.AuthorizedInfo `json:"authorized,omitempty"` // Authorized information
|
||||
ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat
|
||||
AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant
|
||||
Sid string `json:"sid" yaml:"-"` // Session ID (Deprecated, use Authorized instead)
|
||||
|
||||
// Arguments for call
|
||||
Args []interface{} `json:"args,omitempty"` // Arguments for call, it will be used to pass data to the call
|
||||
Retry bool `json:"retry,omitempty"` // Retry mode
|
||||
RetryTimes uint8 `json:"retry_times,omitempty"` // Retry times
|
||||
|
||||
// Locale information
|
||||
Locale string `json:"locale,omitempty"` // Locale
|
||||
Theme string `json:"theme,omitempty"` // Theme
|
||||
|
||||
// Request information
|
||||
Client Client `json:"client,omitempty"` // Client information from HTTP request
|
||||
Referer string `json:"referer,omitempty"` // Request source: api, process, mcp, jssdk, agent, tool, hook, schedule, script, internal
|
||||
Accept Accept `json:"accept,omitempty"` // Response format: standard, cui-web, cui-native, cui-desktop
|
||||
|
||||
// CUI Context information
|
||||
Route string `json:"route,omitempty"` // The route of the request, it will be used to identify the route of the request
|
||||
Data map[string]interface{} `json:"data,omitempty"` // The data of the request, it will be used to pass data to the page
|
||||
|
||||
Silent bool `json:"silent,omitempty"` // Silent mode (Deprecated, use Referer instead)
|
||||
}
|
||||
54
agent/context/utils.go
Normal file
54
agent/context/utils.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package context
|
||||
|
||||
// getValidatedValue gets value from query, header, or default, and validates it
|
||||
func getValidatedValue(queryValue, headerValue, defaultValue string, validator func(string) string) string {
|
||||
if queryValue != "" {
|
||||
return validator(queryValue)
|
||||
}
|
||||
if headerValue != "" {
|
||||
return validator(headerValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getValidatedAccept gets Accept from query, header, or parse from client type
|
||||
func getValidatedAccept(queryValue, headerValue, clientType string) Accept {
|
||||
if queryValue != "" {
|
||||
return validateAccept(queryValue)
|
||||
}
|
||||
if headerValue != "" {
|
||||
return validateAccept(headerValue)
|
||||
}
|
||||
return parseAccept(clientType)
|
||||
}
|
||||
|
||||
// validateReferer validates and returns a valid Referer, returns RefererAPI if invalid
|
||||
func validateReferer(referer string) string {
|
||||
if ValidReferers[referer] {
|
||||
return referer
|
||||
}
|
||||
return RefererAPI
|
||||
}
|
||||
|
||||
// validateAccept validates and returns a valid Accept type, returns AcceptStandard if invalid
|
||||
func validateAccept(accept string) Accept {
|
||||
if ValidAccepts[accept] {
|
||||
return Accept(accept)
|
||||
}
|
||||
return AcceptStandard
|
||||
}
|
||||
|
||||
// parseAccept determines the accept type based on client type
|
||||
func parseAccept(clientType string) Accept {
|
||||
switch clientType {
|
||||
case "web":
|
||||
return AcceptWebCUI
|
||||
case "android", "ios":
|
||||
return AccepNativeCUI
|
||||
case "windows", "macos", "linux":
|
||||
return AcceptDesktopCUI
|
||||
default:
|
||||
return AcceptStandard
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,51 +19,6 @@ import (
|
|||
|
||||
var locker = sync.Mutex{}
|
||||
|
||||
// Message the message
|
||||
type Message struct {
|
||||
ID string `json:"id,omitempty"` // id for the message
|
||||
ToolID string `json:"tool_id,omitempty"` // tool_id for the message
|
||||
Text string `json:"text,omitempty"` // text content
|
||||
Type string `json:"type,omitempty"` // error, text, plan, table, form, page, file, video, audio, image, markdown, json ...
|
||||
Props map[string]interface{} `json:"props,omitempty"` // props for the types
|
||||
IsDone bool `json:"done,omitempty"` // Mark as a done message from agent
|
||||
IsNew bool `json:"new,omitempty"` // Mark as a new message from agent
|
||||
IsDelta bool `json:"delta,omitempty"` // Mark as a delta message from agent
|
||||
Actions []Action `json:"actions,omitempty"` // Conversation Actions for frontend
|
||||
Attachments []attachment.Attachment `json:"attachments,omitempty"` // File attachments
|
||||
Role string `json:"role,omitempty"` // user, assistant, system ...
|
||||
Name string `json:"name,omitempty"` // name for the message
|
||||
AssistantID string `json:"assistant_id,omitempty"` // assistant_id (for assistant role = assistant )
|
||||
AssistantName string `json:"assistant_name,omitempty"` // assistant_name (for assistant role = assistant )
|
||||
AssistantAvatar string `json:"assistant_avatar,omitempty"` // assistant_avatar (for assistant role = assistant )
|
||||
Mentions []Mention `json:"menions,omitempty"` // Mentions for the message ( for user role = user )
|
||||
Data map[string]interface{} `json:"-"` // data for the message
|
||||
Pending bool `json:"-"` // pending for the message
|
||||
Hidden bool `json:"hidden,omitempty"` // hidden for the message (not show in the UI and history)
|
||||
Retry bool `json:"retry,omitempty"` // retry for the message
|
||||
Silent bool `json:"silent,omitempty"` // silent for the message (not show in the UI and history)
|
||||
IsTool bool `json:"-"` // is tool for the message for native tool_calls
|
||||
IsBeginTool bool `json:"-"` // is new tool for the message for native tool_calls
|
||||
IsEndTool bool `json:"-"` // is end tool for the message for native tool_calls
|
||||
Result any `json:"result,omitempty"` // result for the message
|
||||
Begin int64 `json:"begin,omitempty"` // begin at for the message // timestamp
|
||||
End int64 `json:"end,omitempty"` // end at for the message // timestamp
|
||||
}
|
||||
|
||||
// Mention represents a mention
|
||||
type Mention struct {
|
||||
ID string `json:"assistant_id"` // assistant_id
|
||||
Name string `json:"name"` // name
|
||||
Avatar string `json:"avatar,omitempty"` // avatar
|
||||
}
|
||||
|
||||
// Action the action
|
||||
type Action struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Payload interface{} `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// New create a new message
|
||||
func New() *Message {
|
||||
return &Message{Actions: []Action{}, Props: map[string]interface{}{}}
|
||||
|
|
|
|||
48
agent/message/types.go
Normal file
48
agent/message/types.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package message
|
||||
|
||||
import "github.com/yaoapp/yao/attachment"
|
||||
|
||||
// Message the message
|
||||
type Message struct {
|
||||
ID string `json:"id,omitempty"` // id for the message
|
||||
ToolID string `json:"tool_id,omitempty"` // tool_id for the message
|
||||
Text string `json:"text,omitempty"` // text content
|
||||
Type string `json:"type,omitempty"` // error, text, plan, table, form, page, file, video, audio, image, markdown, json ...
|
||||
Props map[string]interface{} `json:"props,omitempty"` // props for the types
|
||||
IsDone bool `json:"done,omitempty"` // Mark as a done message from agent
|
||||
IsNew bool `json:"new,omitempty"` // Mark as a new message from agent
|
||||
IsDelta bool `json:"delta,omitempty"` // Mark as a delta message from agent
|
||||
Actions []Action `json:"actions,omitempty"` // Conversation Actions for frontend
|
||||
Attachments []attachment.Attachment `json:"attachments,omitempty"` // File attachments
|
||||
Role string `json:"role,omitempty"` // user, assistant, system ...
|
||||
Name string `json:"name,omitempty"` // name for the message
|
||||
AssistantID string `json:"assistant_id,omitempty"` // assistant_id (for assistant role = assistant )
|
||||
AssistantName string `json:"assistant_name,omitempty"` // assistant_name (for assistant role = assistant )
|
||||
AssistantAvatar string `json:"assistant_avatar,omitempty"` // assistant_avatar (for assistant role = assistant )
|
||||
Mentions []Mention `json:"menions,omitempty"` // Mentions for the message ( for user role = user )
|
||||
Data map[string]interface{} `json:"-"` // data for the message
|
||||
Pending bool `json:"-"` // pending for the message
|
||||
Hidden bool `json:"hidden,omitempty"` // hidden for the message (not show in the UI and history)
|
||||
Retry bool `json:"retry,omitempty"` // retry for the message
|
||||
Silent bool `json:"silent,omitempty"` // silent for the message (not show in the UI and history)
|
||||
IsTool bool `json:"-"` // is tool for the message for native tool_calls
|
||||
IsBeginTool bool `json:"-"` // is new tool for the message for native tool_calls
|
||||
IsEndTool bool `json:"-"` // is end tool for the message for native tool_calls
|
||||
Result any `json:"result,omitempty"` // result for the message
|
||||
Begin int64 `json:"begin,omitempty"` // begin at for the message // timestamp
|
||||
End int64 `json:"end,omitempty"` // end at for the message // timestamp
|
||||
}
|
||||
|
||||
// Mention represents a mention
|
||||
type Mention struct {
|
||||
ID string `json:"assistant_id"` // assistant_id
|
||||
Name string `json:"name"` // name
|
||||
Avatar string `json:"avatar,omitempty"` // avatar
|
||||
}
|
||||
|
||||
// Action the action
|
||||
type Action struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Payload interface{} `json:"payload,omitempty"`
|
||||
}
|
||||
|
|
@ -1,553 +0,0 @@
|
|||
# Chat API
|
||||
|
||||
This document describes the RESTful API for AI chat completions in Yao applications, providing **100% compatibility with OpenAI clients**.
|
||||
|
||||
## Base URL
|
||||
|
||||
All endpoints are prefixed with the configured base URL followed by `/chat` (e.g., `/v1/chat`).
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require OAuth authentication via the configured OAuth provider.
|
||||
|
||||
## Overview
|
||||
|
||||
The Chat API provides AI-powered chat completion capabilities with **full OpenAI API compatibility**, supporting:
|
||||
|
||||
- **OpenAI Client Compatibility** - 100% compatible with existing OpenAI client libraries
|
||||
- **Server-Sent Events (SSE)** - Real-time streaming responses
|
||||
- **Context Management** - Persistent chat sessions with history
|
||||
- **Assistant Selection** - Multiple AI assistants with different capabilities
|
||||
- **Flexible Parameters** - Standard OpenAI parameters plus Yao-specific extensions
|
||||
- **Session Management** - Automatic session handling with user identification
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Chat Completions
|
||||
|
||||
Create AI chat completions with streaming responses using Server-Sent Events. This endpoint is **100% compatible with OpenAI's `/v1/chat/completions` API**.
|
||||
|
||||
```
|
||||
GET /completions?content={content}&chat_id={chat_id}&assistant_id={assistant_id}&context={context}&silent={silent}&history_visible={history_visible}&client_type={client_type}
|
||||
POST /completions
|
||||
```
|
||||
|
||||
**Note:** This is a temporary implementation for full-process testing, and the interface may undergo significant global changes in the future.
|
||||
|
||||
**OpenAI Compatibility:**
|
||||
|
||||
- **Endpoint Path**: `/chat/completions` (matches OpenAI exactly)
|
||||
- **Request Format**: Supports both OpenAI standard and Yao-extended parameters
|
||||
- **Response Format**: Compatible with OpenAI response structure
|
||||
- **Client Libraries**: Works with existing OpenAI SDKs and client libraries
|
||||
|
||||
**Query Parameters (GET) / Form Data (POST):**
|
||||
|
||||
**Standard OpenAI Parameters:**
|
||||
|
||||
- `model` (optional): AI model to use (mapped to `assistant_id` internally)
|
||||
- `messages` (optional): Array of message objects (OpenAI format)
|
||||
- `temperature` (optional): Sampling temperature
|
||||
- `max_tokens` (optional): Maximum tokens in response
|
||||
- `stream` (optional): Enable streaming responses
|
||||
|
||||
**Yao-Specific Parameters:**
|
||||
|
||||
- `content` (required): The user's message or question (simplified input)
|
||||
- `chat_id` (optional): Chat session identifier (auto-generated if not provided)
|
||||
- `assistant_id` (optional): Specific assistant to use (defaults to system default)
|
||||
- `context` (optional): Additional context for the conversation
|
||||
- `silent` (optional): Silent mode flag ("true"/"false" or "1"/"0")
|
||||
- `history_visible` (optional): Whether chat history is visible ("true"/"false" or "1"/"0")
|
||||
- `client_type` (optional): Client type identifier for customization
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer {access_token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Response Headers:**
|
||||
|
||||
```
|
||||
Content-Type: text/event-stream;charset=utf-8
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
```
|
||||
|
||||
**Example GET Request (Yao Simplified Format):**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/chat/completions?content=Hello%2C%20how%20are%20you%3F&chat_id=chat_123&assistant_id=mohe" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
**Example POST Request (OpenAI Compatible Format):**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/chat/completions" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "mohe",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
"stream": true
|
||||
}'
|
||||
```
|
||||
|
||||
**Example POST Request (Yao Simplified Format):**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/chat/completions" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d "content=Hello, how are you?&chat_id=chat_123&assistant_id=mohe"
|
||||
```
|
||||
|
||||
**Response (Server-Sent Events):**
|
||||
|
||||
The response is streamed as Server-Sent Events with OpenAI-compatible format:
|
||||
|
||||
```
|
||||
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1640995200,"model":"mohe","choices":[{"index":0,"delta":{"content":"Hello! I'm doing well, thank you for asking."},"finish_reason":null}]}
|
||||
|
||||
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1640995200,"model":"mohe","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
**Response Data Types:**
|
||||
|
||||
- `chat.completion.chunk` - Streaming content chunks (OpenAI format)
|
||||
- `error` - Error message if something goes wrong
|
||||
- `[DONE]` - Indicates completion of the response (OpenAI format)
|
||||
|
||||
**Success Response Example (OpenAI Compatible):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1640995200,
|
||||
"model": "mohe",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": "Hello! I'm an AI assistant created by Yao. How can I help you today?"
|
||||
},
|
||||
"finish_reason": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Completion Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1640995200,
|
||||
"model": "mohe",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## OpenAI Client Integration
|
||||
|
||||
### Using OpenAI Python Client
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
# Configure client for Yao API
|
||||
openai.api_base = "https://your-yao-server.com/v1"
|
||||
openai.api_key = "your-oauth-token"
|
||||
|
||||
# Use exactly like OpenAI
|
||||
response = openai.ChatCompletion.create(
|
||||
model="mohe",
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if chunk.choices[0].delta.get("content"):
|
||||
print(chunk.choices[0].delta.content, end="")
|
||||
```
|
||||
|
||||
### Using OpenAI Node.js Client
|
||||
|
||||
```javascript
|
||||
import OpenAI from "openai";
|
||||
|
||||
const openai = new OpenAI({
|
||||
baseURL: "https://your-yao-server.com/v1",
|
||||
apiKey: "your-oauth-token",
|
||||
});
|
||||
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "mohe",
|
||||
messages: [{ role: "user", content: "Hello, how are you?" }],
|
||||
stream: true,
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
process.stdout.write(chunk.choices[0]?.delta?.content || "");
|
||||
}
|
||||
```
|
||||
|
||||
### Using OpenAI Go Client
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := openai.DefaultConfig("your-oauth-token")
|
||||
config.BaseURL = "https://your-yao-server.com/v1"
|
||||
client := openai.NewClientWithConfig(config)
|
||||
|
||||
req := openai.ChatCompletionRequest{
|
||||
Model: "mohe",
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello, how are you?",
|
||||
},
|
||||
},
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
stream, err := client.CreateChatCompletionStream(context.Background(), req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
response, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(response.Choices[0].Delta.Content)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters in Detail
|
||||
|
||||
### Content Parameter (Yao Extension)
|
||||
|
||||
The `content` parameter provides simplified input for basic use cases:
|
||||
|
||||
- **Required** for simplified Yao format
|
||||
- Can be a question, command, or conversation message
|
||||
- Supports natural language input
|
||||
- Alternative to OpenAI's `messages` array format
|
||||
|
||||
### Model/Assistant Selection
|
||||
|
||||
The `model` parameter (OpenAI) or `assistant_id` parameter (Yao) selects the AI assistant:
|
||||
|
||||
- **OpenAI Compatible**: Use `model` field in JSON requests
|
||||
- **Yao Extension**: Use `assistant_id` for URL parameters
|
||||
- **Available Models**: `mohe`, `developer`, `analyst`, etc.
|
||||
- **Default**: System default assistant if not specified
|
||||
|
||||
### Chat ID Management (Yao Extension)
|
||||
|
||||
The `chat_id` parameter manages conversation continuity:
|
||||
|
||||
- **Auto-generated** if not provided (format: `chat_{timestamp}`)
|
||||
- **Persistent** across multiple requests for the same conversation
|
||||
- **Unique** identifier for each chat session
|
||||
- **Yao-specific**: Not part of standard OpenAI API
|
||||
|
||||
### Context and Behavior (Yao Extensions)
|
||||
|
||||
Additional Yao-specific parameters for fine-tuning behavior:
|
||||
|
||||
- `context` - Provides additional context for better responses
|
||||
- `silent` - Controls verbose/quiet response modes
|
||||
- `history_visible` - Controls whether conversation history affects responses
|
||||
- `client_type` - Allows client-specific customizations
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return standardized error responses compatible with OpenAI format:
|
||||
|
||||
**Server-Sent Events Error:**
|
||||
|
||||
```
|
||||
data: {"error":{"type":"invalid_request_error","message":"content is required","code":"missing_parameter"}}
|
||||
```
|
||||
|
||||
**HTTP Error Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"message": "The request is missing required parameters",
|
||||
"code": "missing_parameter"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Common Error Types:**
|
||||
|
||||
- `invalid_request_error` - Missing required parameters
|
||||
- `authentication_error` - Authentication failure
|
||||
- `not_found_error` - Invalid model/assistant ID
|
||||
- `internal_server_error` - Server processing error
|
||||
|
||||
**HTTP Status Codes:**
|
||||
|
||||
- `200` - Success (streaming response)
|
||||
- `400` - Bad Request (invalid parameters)
|
||||
- `401` - Unauthorized (authentication required)
|
||||
- `404` - Not Found (model not found)
|
||||
- `500` - Internal Server Error
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### OpenAI Client Migration
|
||||
|
||||
**Before (OpenAI):**
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_key = "sk-..."
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
```
|
||||
|
||||
**After (Yao - No Code Changes Required):**
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "https://your-yao.com/v1" # Only change needed
|
||||
openai.api_key = "your-oauth-token" # Only change needed
|
||||
response = openai.ChatCompletion.create(
|
||||
model="mohe", # Use Yao assistant
|
||||
messages=[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Simple Chat Interaction
|
||||
|
||||
1. **Start a conversation (Yao simplified format):**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/chat/completions?content=What%20is%20Yao?" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
2. **Continue the conversation (OpenAI format):**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/chat/completions" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "mohe",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Tell me more about its features"}
|
||||
],
|
||||
"stream": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Assistant-Specific Interaction
|
||||
|
||||
1. **Use a specific assistant (OpenAI compatible):**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/chat/completions" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "developer",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Help me debug this code"}
|
||||
],
|
||||
"stream": true,
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
### Context-Aware Conversation (Yao Extensions)
|
||||
|
||||
1. **Provide additional context:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/chat/completions?content=Optimize%20this%20query&context=PostgreSQL%20database%20with%20large%20user%20table&assistant_id=analyst" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
## Client Library Examples
|
||||
|
||||
### Curl (OpenAI Format)
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/chat/completions" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "mohe",
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello!"}
|
||||
],
|
||||
"stream": true,
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
### JavaScript (Fetch API)
|
||||
|
||||
```javascript
|
||||
const response = await fetch("/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "mohe",
|
||||
messages: [{ role: "user", content: "Hello, how are you?" }],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6);
|
||||
if (data === "[DONE]") return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const content = parsed.choices[0]?.delta?.content;
|
||||
if (content) {
|
||||
console.log(content);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From OpenAI API
|
||||
|
||||
**No code changes required!** Just update your configuration:
|
||||
|
||||
1. **Change Base URL**: `https://api.openai.com/v1` → `https://your-yao.com/v1`
|
||||
2. **Update API Key**: Use your Yao OAuth token instead of OpenAI API key
|
||||
3. **Change Model Names**: `gpt-3.5-turbo` → `mohe`, `gpt-4` → `developer`, etc.
|
||||
|
||||
### From Custom Chat APIs
|
||||
|
||||
If migrating from other chat APIs, you can use Yao's simplified format:
|
||||
|
||||
- **Simple GET requests** with `content` parameter
|
||||
- **Form data POST** for basic interactions
|
||||
- **Gradual migration** to full OpenAI format
|
||||
|
||||
## Integration Considerations
|
||||
|
||||
### Performance
|
||||
|
||||
- **Streaming responses** reduce perceived latency
|
||||
- **Connection pooling** for multiple concurrent chats
|
||||
- **Automatic session cleanup** prevents memory leaks
|
||||
- **OpenAI client optimizations** work seamlessly
|
||||
|
||||
### Security
|
||||
|
||||
- **OAuth 2.1 authentication** required for all requests
|
||||
- **Session-based access control**
|
||||
- **Input validation** and sanitization
|
||||
- **Rate limiting** (configured at server level)
|
||||
- **Compatible with OpenAI security practices**
|
||||
|
||||
### Scalability
|
||||
|
||||
- **Stateless design** (session data in external store)
|
||||
- **Load balancer compatible** (sticky sessions not required)
|
||||
- **Horizontal scaling** support
|
||||
- **OpenAI client connection pooling** supported
|
||||
|
||||
## Development Notes
|
||||
|
||||
**Important:** This is a temporary implementation for full-process testing. The interface design and functionality may undergo significant global changes in future versions. However, **OpenAI compatibility will be maintained** to ensure existing client libraries continue to work.
|
||||
|
||||
### Current Limitations
|
||||
|
||||
- Limited error recovery mechanisms
|
||||
- Basic assistant selection logic
|
||||
- Simplified context management
|
||||
- Minimal response formatting options
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
Future versions will maintain OpenAI compatibility while adding:
|
||||
|
||||
- Enhanced context management
|
||||
- Advanced assistant capabilities
|
||||
- Improved error handling
|
||||
- Extended Yao-specific parameters
|
||||
- WebSocket support as alternative to SSE
|
||||
|
||||
### Compatibility Promise
|
||||
|
||||
- **OpenAI Client Support**: All major OpenAI client libraries will continue to work
|
||||
- **Standard Compliance**: Full compliance with OpenAI API specification
|
||||
- **Seamless Migration**: Existing OpenAI code works with minimal configuration changes
|
||||
|
||||
This Chat API provides **100% OpenAI client compatibility** while extending capabilities with Yao-specific features, making it easy to migrate existing applications and integrate with the broader AI ecosystem.
|
||||
|
|
@ -1,15 +1,9 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/yaoapp/yao/agent"
|
||||
chatctx "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/message"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// Attach attaches the agent handlers to the router
|
||||
|
|
@ -18,76 +12,26 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
// Protect all endpoints with OAuth
|
||||
group.Use(oauth.Guard)
|
||||
|
||||
// Chat Completion
|
||||
group.GET("/completions", chatCompletion)
|
||||
group.POST("/completions", chatCompletion)
|
||||
// List Chat Completions
|
||||
group.GET("/completions", placeholder)
|
||||
|
||||
// Create Chat Completion
|
||||
group.POST("/completions", GinCreateCompletions)
|
||||
|
||||
// Update Chat Completion Metadata
|
||||
group.PUT("/completions", GinUpdateCompletions)
|
||||
|
||||
// Get Chat Completion Details
|
||||
group.GET("/completions/:completion_id", placeholder)
|
||||
|
||||
// Get Chat Messages
|
||||
group.GET("/completions/:completion_id/messages", placeholder)
|
||||
|
||||
// Delete Chat Completion
|
||||
group.DELETE("/completions/:completion_id", placeholder)
|
||||
|
||||
}
|
||||
|
||||
// Chat Completion (SSE)
|
||||
// Note: This is a temporary implementation for full-process testing,
|
||||
// and the interface may undergo significant global changes in the future.
|
||||
func chatCompletion(c *gin.Context) {
|
||||
// Set headers for SSE
|
||||
c.Header("Content-Type", "text/event-stream;charset=utf-8")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
|
||||
sid := c.GetString("__sid")
|
||||
if sid == "" {
|
||||
sid = uuid.New().String()
|
||||
}
|
||||
|
||||
content := c.Query("content")
|
||||
if content == "" {
|
||||
msg := message.New().Error("content is required").Done()
|
||||
msg.Write(c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
chatID := c.Query("chat_id")
|
||||
if chatID == "" {
|
||||
// Only generate new chat_id if not provided
|
||||
chatID = fmt.Sprintf("chat_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// Set the context with validated chat_id
|
||||
ctx, cancel := chatctx.NewWithCancel(sid, chatID, c.Query("context"))
|
||||
defer cancel()
|
||||
defer ctx.Release() // Release the context after the request is done
|
||||
|
||||
// Set the assistant ID
|
||||
assistantID := c.Query("assistant_id")
|
||||
if assistantID != "" {
|
||||
ctx = chatctx.WithAssistantID(ctx, assistantID)
|
||||
}
|
||||
|
||||
// Set the silent mode
|
||||
silent := c.Query("silent")
|
||||
if silent == "true" || silent == "1" {
|
||||
ctx = chatctx.WithSilent(ctx, true)
|
||||
}
|
||||
|
||||
// Set the history visible
|
||||
historyVisible := c.Query("history_visible")
|
||||
if historyVisible != "" {
|
||||
ctx = chatctx.WithHistoryVisible(ctx, historyVisible == "true" || historyVisible == "1")
|
||||
}
|
||||
|
||||
// Set the client type
|
||||
clientType := c.Query("client_type")
|
||||
if clientType != "" {
|
||||
ctx = chatctx.WithClientType(ctx, clientType)
|
||||
}
|
||||
|
||||
// Get agent instance and call Answer
|
||||
agentInstance := agent.GetAgent()
|
||||
err := agentInstance.Answer(ctx, content, c)
|
||||
|
||||
// Error handling
|
||||
if err != nil {
|
||||
message.New().Done().Error(err).Write(c.Writer)
|
||||
c.Done()
|
||||
return
|
||||
}
|
||||
func placeholder(c *gin.Context) {
|
||||
response.RespondWithSuccess(c, response.StatusOK, gin.H{"message": "placeholder"})
|
||||
}
|
||||
|
|
|
|||
104
openapi/chat/completions.go
Normal file
104
openapi/chat/completions.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// GinCreateCompletions handles POST /chat/:assistant_id/completions - Create a chat completion
|
||||
func GinCreateCompletions(c *gin.Context) {
|
||||
// Print request information for debugging
|
||||
fmt.Println("========== Chat Completions Request ==========")
|
||||
fmt.Printf("Method: %s\n", c.Request.Method)
|
||||
fmt.Printf("URL: %s\n", c.Request.URL.String())
|
||||
fmt.Printf("RemoteAddr: %s\n", c.Request.RemoteAddr)
|
||||
|
||||
// Print headers
|
||||
fmt.Println("\n--- Headers ---")
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Print path parameters
|
||||
fmt.Println("\n--- Path Parameters ---")
|
||||
for _, param := range c.Params {
|
||||
fmt.Printf("%s: %s\n", param.Key, param.Value)
|
||||
}
|
||||
|
||||
// Print query parameters
|
||||
fmt.Println("\n--- Query Parameters ---")
|
||||
for key, values := range c.Request.URL.Query() {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Print request body
|
||||
fmt.Println("\n--- Request Body ---")
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading body: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%s\n", string(body))
|
||||
// Restore the body for further processing
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
fmt.Println("===============================================")
|
||||
|
||||
// Handle Sid - try multiple methods for maximum compatibility
|
||||
var sid string
|
||||
|
||||
// Method 1: Check if client sent X-Session-Id header
|
||||
sid = c.GetHeader("X-Session-Id")
|
||||
|
||||
// Method 2: Try to read from cookie
|
||||
if sid == "" {
|
||||
sid, err = c.Cookie("Sid")
|
||||
if err == nil && sid != "" {
|
||||
fmt.Printf("Existing Sid from cookie: %s\n", sid)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Existing Sid from header: %s\n", sid)
|
||||
}
|
||||
|
||||
// Method 3: For clients that can't store cookies/headers (like Electron cross-origin),
|
||||
// generate a deterministic session ID based on client fingerprint
|
||||
if sid == "" {
|
||||
// Use Authorization token if available (most stable identifier)
|
||||
authToken := c.GetHeader("Authorization")
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
if authToken != "" {
|
||||
// Generate stable session ID from auth token
|
||||
hash := md5.Sum([]byte(authToken))
|
||||
sid = hex.EncodeToString(hash[:])
|
||||
fmt.Printf("Generated deterministic Sid from auth token: %s\n", sid)
|
||||
} else {
|
||||
// Fallback: generate random UUID
|
||||
sid = uuid.New().String()
|
||||
fmt.Printf("Generated random Sid: %s\n", sid)
|
||||
}
|
||||
|
||||
fmt.Printf("Client fingerprint - UserAgent: %s\n", userAgent)
|
||||
}
|
||||
|
||||
// Try to set cookie (may not work for cross-origin, but doesn't hurt)
|
||||
c.SetCookie("Sid", sid, 86400*30, "/", "", false, false)
|
||||
|
||||
// Return Sid in response header and body for client reference
|
||||
c.Header("X-Session-Id", sid)
|
||||
|
||||
response.RespondWithSuccess(c, response.StatusOK, gin.H{"message": "Create Completions", "sid": sid})
|
||||
}
|
||||
|
||||
// GinUpdateCompletions handles PUT /chat/:assistant_id/completions - Update a chat completion metadata
|
||||
func GinUpdateCompletions(c *gin.Context) {}
|
||||
1
openapi/chat/types.go
Normal file
1
openapi/chat/types.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package chat
|
||||
|
|
@ -19,11 +19,10 @@ type Provider struct {
|
|||
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
|
||||
|
||||
// Create providers group with OAuth guard
|
||||
providers := group.Group("/providers")
|
||||
providers.Use(oauth.Guard)
|
||||
group.Use(oauth.Guard)
|
||||
|
||||
// LLM Providers endpoints
|
||||
providers.GET("/", listProviders) // GET /providers - List all LLM providers
|
||||
group.GET("/providers", listProviders) // GET /providers - List all LLM providers
|
||||
}
|
||||
|
||||
// listProviders lists all available LLM providers (built-in + user-defined)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,10 @@ type Server struct {
|
|||
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
|
||||
|
||||
// Create servers group with OAuth guard
|
||||
servers := group.Group("/servers")
|
||||
servers.Use(oauth.Guard)
|
||||
group.Use(oauth.Guard)
|
||||
|
||||
// MCP Servers endpoints
|
||||
servers.GET("/", listServers) // GET /servers - List all MCP servers
|
||||
group.GET("/servers", listServers) // GET /servers - List all MCP servers
|
||||
}
|
||||
|
||||
// listServers lists all available MCP servers (loaded clients from user perspective)
|
||||
|
|
|
|||
42
openapi/models.go
Normal file
42
openapi/models.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package openapi
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// For compatibility with all platform clients, providing standard OpenAPI model interfaces
|
||||
|
||||
// GinGetModels handles GET /chat/models - Get all chat models
|
||||
func GinGetModels(c *gin.Context) {
|
||||
mockResponse := ModelsResponse{
|
||||
Object: "list",
|
||||
Data: []Model{
|
||||
{
|
||||
ID: "gpt-4o-1024",
|
||||
Object: "model",
|
||||
Created: 1686935002,
|
||||
OwnedBy: "organization-owner",
|
||||
},
|
||||
{
|
||||
ID: "model-id-1",
|
||||
Object: "model",
|
||||
Created: 1686935002,
|
||||
OwnedBy: "organization-owner",
|
||||
},
|
||||
{
|
||||
ID: "model-id-2",
|
||||
Object: "model",
|
||||
Created: 1686935002,
|
||||
OwnedBy: "openai",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, response.StatusOK, mockResponse)
|
||||
}
|
||||
|
||||
// GinGetModelDetails handles GET /chat/models/:model_name - Get model details
|
||||
func GinGetModelDetails(c *gin.Context) {
|
||||
response.RespondWithSuccess(c, response.StatusOK, gin.H{"message": "placeholder"})
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package oauth
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -99,7 +100,57 @@ func (s *Service) getAccessToken(c *gin.Context) string {
|
|||
}
|
||||
token = cookie
|
||||
}
|
||||
return strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
// Get the access token
|
||||
accessToken := strings.TrimPrefix(token, "Bearer ")
|
||||
if s.isAPIKey(accessToken) {
|
||||
return s.getAccessTokenFromAPIKey(accessToken)
|
||||
}
|
||||
return accessToken
|
||||
}
|
||||
|
||||
// isAPIKey checks if the token is a API Key
|
||||
func (s *Service) isAPIKey(token string) bool {
|
||||
if strings.HasPrefix(token, "ak-") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getAccessTokenFromAPIKey gets the access token from the API Key
|
||||
func (s *Service) getAccessTokenFromAPIKey(apiKey string) string {
|
||||
|
||||
// @TODO: Will be implemented later
|
||||
|
||||
// Just Mock data for now ( signature an )
|
||||
userID := os.Getenv("APIKEY_TEST_USER_ID")
|
||||
teamID := os.Getenv("APIKEY_TEST_TEAM_ID")
|
||||
clientID := os.Getenv("YAO_CLIENT_ID")
|
||||
|
||||
// Get or create subject
|
||||
subject, err := OAuth.Subject(clientID, userID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to store user fingerprint: %s", err.Error())
|
||||
}
|
||||
|
||||
extraClaims := make(map[string]interface{})
|
||||
extraClaims["team_id"] = teamID
|
||||
extraClaims["user_id"] = userID
|
||||
extraClaims["token_type"] = "Bearer"
|
||||
extraClaims["expires_in"] = 3600
|
||||
extraClaims["issued_at"] = time.Now().Unix()
|
||||
extraClaims["expires_at"] = time.Now().Unix() + 3600
|
||||
extraClaims["api_key"] = apiKey
|
||||
accessToken, err := OAuth.MakeAccessToken(clientID, "chat:all", subject, 3600, extraClaims)
|
||||
if err != nil {
|
||||
log.Warn("Failed to make access token: %s", err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("========== Access Token From API Key ==========")
|
||||
fmt.Println("accessToken: ", accessToken)
|
||||
fmt.Println("extraClaims: ", extraClaims)
|
||||
fmt.Println("===============================================")
|
||||
return accessToken
|
||||
}
|
||||
|
||||
// GetAccessToken gets the access token from the request (public method)
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
|||
// Well-known handlers
|
||||
openapi.attachWellKnown(router)
|
||||
|
||||
// Models ( LLM Agent )
|
||||
group.GET("/models", openapi.OAuth.Guard, GinGetModels)
|
||||
|
||||
// Get Model Details ( LLM Agent )
|
||||
group.GET("/models/:model_name", openapi.OAuth.Guard, GinGetModelDetails)
|
||||
|
||||
// OAuth handlers
|
||||
openapi.attachOAuth(group)
|
||||
|
||||
|
|
|
|||
|
|
@ -129,3 +129,17 @@ type TempConfig struct {
|
|||
Providers *Providers `json:"providers,omitempty"`
|
||||
OAuth *TempOAuth `json:"oauth,omitempty"`
|
||||
}
|
||||
|
||||
// Model represents a chat model
|
||||
type Model struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
}
|
||||
|
||||
// ModelsResponse represents the response for listing models
|
||||
type ModelsResponse struct {
|
||||
Object string `json:"object"`
|
||||
Data []Model `json:"data"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue