From 1c502bfea8e4fba66f18ce499ed6df310d1b977d Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 10 Nov 2025 17:30:53 +0800 Subject: [PATCH] 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. --- agent/api.go | 58 ++-- agent/assistant/api.go | 6 +- agent/assistant/call.go | 344 +-------------------- agent/assistant/object.go | 16 +- agent/context/context.go | 242 ++++----------- agent/context/context_test.go | 288 ++++++++++++++++++ agent/context/gin.go | 81 +++++ agent/context/types.go | 123 ++++++++ agent/context/utils.go | 54 ++++ agent/message/message.go | 45 --- agent/message/types.go | 48 +++ openapi/chat/README.md | 553 ---------------------------------- openapi/chat/chat.go | 96 ++---- openapi/chat/completions.go | 104 +++++++ openapi/chat/types.go | 1 + openapi/llm/llm.go | 5 +- openapi/mcp/mcp.go | 5 +- openapi/models.go | 42 +++ openapi/oauth/guard.go | 53 +++- openapi/openapi.go | 6 + openapi/types.go | 14 + 21 files changed, 934 insertions(+), 1250 deletions(-) create mode 100644 agent/context/context_test.go create mode 100644 agent/context/gin.go create mode 100644 agent/context/types.go create mode 100644 agent/context/utils.go create mode 100644 agent/message/types.go delete mode 100644 openapi/chat/README.md create mode 100644 openapi/chat/completions.go create mode 100644 openapi/chat/types.go create mode 100644 openapi/models.go diff --git a/agent/api.go b/agent/api.go index 371f3909..1962608a 100644 --- a/agent/api.go +++ b/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 diff --git a/agent/assistant/api.go b/agent/assistant/api.go index fb83e321..5f7f5882 100644 --- a/agent/assistant/api.go +++ b/agent/assistant/api.go @@ -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) diff --git a/agent/assistant/call.go b/agent/assistant/call.go index 5e0a00cc..1c737c5c 100644 --- a/agent/assistant/call.go +++ b/agent/assistant/call.go @@ -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 -// } diff --git a/agent/assistant/object.go b/agent/assistant/object.go index a1994a84..4ad34bbb 100644 --- a/agent/assistant/object.go +++ b/agent/assistant/object.go @@ -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()) } diff --git a/agent/context/context.go b/agent/context/context.go index 62a3d9cf..e9cb15bc 100644 --- a/agent/context/context.go +++ b/agent/context/context.go @@ -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()) +} diff --git a/agent/context/context_test.go b/agent/context/context_test.go new file mode 100644 index 00000000..27200ef0 --- /dev/null +++ b/agent/context/context_test.go @@ -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) + }) + } +} diff --git a/agent/context/gin.go b/agent/context/gin.go new file mode 100644 index 00000000..8159e3a9 --- /dev/null +++ b/agent/context/gin.go @@ -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" + } +} diff --git a/agent/context/types.go b/agent/context/types.go new file mode 100644 index 00000000..0dca6750 --- /dev/null +++ b/agent/context/types.go @@ -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) +} diff --git a/agent/context/utils.go b/agent/context/utils.go new file mode 100644 index 00000000..7b7c2621 --- /dev/null +++ b/agent/context/utils.go @@ -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 + } +} + diff --git a/agent/message/message.go b/agent/message/message.go index 8183adf9..0ea72992 100644 --- a/agent/message/message.go +++ b/agent/message/message.go @@ -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{}{}} diff --git a/agent/message/types.go b/agent/message/types.go new file mode 100644 index 00000000..c0849e0f --- /dev/null +++ b/agent/message/types.go @@ -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"` +} diff --git a/openapi/chat/README.md b/openapi/chat/README.md deleted file mode 100644 index bf8e2d79..00000000 --- a/openapi/chat/README.md +++ /dev/null @@ -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. diff --git a/openapi/chat/chat.go b/openapi/chat/chat.go index 183857c9..69bc53f7 100644 --- a/openapi/chat/chat.go +++ b/openapi/chat/chat.go @@ -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"}) } diff --git a/openapi/chat/completions.go b/openapi/chat/completions.go new file mode 100644 index 00000000..25947d27 --- /dev/null +++ b/openapi/chat/completions.go @@ -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) {} diff --git a/openapi/chat/types.go b/openapi/chat/types.go new file mode 100644 index 00000000..5c2cd9a8 --- /dev/null +++ b/openapi/chat/types.go @@ -0,0 +1 @@ +package chat diff --git a/openapi/llm/llm.go b/openapi/llm/llm.go index 075c75a7..14b55eff 100644 --- a/openapi/llm/llm.go +++ b/openapi/llm/llm.go @@ -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) diff --git a/openapi/mcp/mcp.go b/openapi/mcp/mcp.go index de2037c5..a73e5ef2 100644 --- a/openapi/mcp/mcp.go +++ b/openapi/mcp/mcp.go @@ -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) diff --git a/openapi/models.go b/openapi/models.go new file mode 100644 index 00000000..f8718e84 --- /dev/null +++ b/openapi/models.go @@ -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"}) +} diff --git a/openapi/oauth/guard.go b/openapi/oauth/guard.go index b7ab4c18..7230e798 100644 --- a/openapi/oauth/guard.go +++ b/openapi/oauth/guard.go @@ -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) diff --git a/openapi/openapi.go b/openapi/openapi.go index 20967f0a..f1160190 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -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) diff --git a/openapi/types.go b/openapi/types.go index b77542d2..a30f1c63 100644 --- a/openapi/types.go +++ b/openapi/types.go @@ -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"` +}