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:
Max 2025-11-10 17:30:53 +08:00
parent af383dd941
commit 1c502bfea8
21 changed files with 934 additions and 1250 deletions

View file

@ -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

View file

@ -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)

View file

@ -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
// }

View file

@ -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())
}

View file

@ -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())
}

View 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
View 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
View 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
View 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
}
}

View file

@ -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
View 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"`
}

View file

@ -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.

View file

@ -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
View 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
View file

@ -0,0 +1 @@
package chat

View file

@ -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)

View file

@ -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
View 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"})
}

View file

@ -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)

View file

@ -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)

View file

@ -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"`
}