Merge pull request #1295 from trheyi/main

Refactor Assistant methods and context handling for improved function…
This commit is contained in:
Max 2025-11-13 11:44:23 +08:00 committed by GitHub
commit a9fced2dd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 298 additions and 42 deletions

View file

@ -1,9 +1,14 @@
package assistant package assistant
import "github.com/yaoapp/yao/agent/context" import (
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/llm"
)
// Stream stream the agent // Stream stream the agent
func (ast *Assistant) Stream(ctx *context.Context, messages []context.Message, handler context.StreamFunc) error { func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Message, handler context.StreamFunc) (*context.Response, error) {
var err error
// Initialize stack and auto-handle completion/failure/restore // Initialize stack and auto-handle completion/failure/restore
_, traceID, done := context.EnterStack(ctx, ast.ID, ctx.Referer) _, traceID, done := context.EnterStack(ctx, ast.ID, ctx.Referer)
@ -11,23 +16,85 @@ func (ast *Assistant) Stream(ctx *context.Context, messages []context.Message, h
_ = traceID // traceID is available for trace logging _ = traceID // traceID is available for trace logging
// Full input messages with chat history
fullMessages, err := ast.WithHistory(ctx, inputMessages)
if err != nil {
return nil, err
}
// Request Create hook ( Optional ) // Request Create hook ( Optional )
var createResponse *context.ResponseHookCreate
if ast.Script != nil {
var err error
createResponse, err = ast.Script.Create(ctx, fullMessages)
if err != nil {
return nil, err
}
}
_ = createResponse // createResponse is available for further processing
var completionOptions *llm.CompletionOptions // default is nil
// LLM Call Stream ( Optional ) // LLM Call Stream ( Optional )
var completionMessages []context.Message
var completionResponse *context.ResponseCompletion
if ast.Prompts != nil || ast.MCP != nil {
llm, err := llm.New(ast.GetConnector(ctx))
if err != nil {
return nil, err
}
// Build the LLM request
completionMessages, completionOptions, err = ast.BuildLLMRequest(ctx, inputMessages, createResponse)
if err != nil {
return nil, err
}
// Call the LLM Completion Stream
completionResponse, err = llm.Stream(ctx, completionMessages, completionOptions, handler)
if err != nil {
return nil, err
}
}
// Request MCP hook ( Optional )
var mcpResponse *context.ResponseHookMCP
if ast.MCP != nil {
_ = mcpResponse // mcpResponse is available for further processing
// MCP Execution Loop
}
// Request Done hook ( Optional ) // Request Done hook ( Optional )
var doneResponse *context.ResponseHookDone
if ast.Script != nil {
var err error
doneResponse, err = ast.Script.Done(ctx, fullMessages, completionResponse, mcpResponse)
if err != nil {
return nil, err
}
}
return nil _ = doneResponse // doneResponse is available for further processing
return &context.Response{Create: createResponse, Done: doneResponse, Completion: completionResponse}, nil
} }
// Run run the agent // GetConnector get the connector from the context
func (ast *Assistant) Run(ctx *context.Context, messages []context.Message) (*context.Response, error) { func (ast *Assistant) GetConnector(ctx *context.Context) string {
if ctx.Connector != "" {
// Initialize stack and auto-handle completion/failure/restore return ctx.Connector
_, traceID, done := context.EnterStack(ctx, ast.ID, ctx.Referer) }
defer done() return ast.Connector
}
_ = traceID // traceID is available for trace logging
// BuildLLMRequest build the LLM request
return &context.Response{}, nil func (ast *Assistant) BuildLLMRequest(ctx *context.Context, messages []context.Message, createResponse *context.ResponseHookCreate) ([]context.Message, *llm.CompletionOptions, error) {
return messages, nil, nil
}
// WithHistory with the history messages
func (ast *Assistant) WithHistory(ctx *context.Context, messages []context.Message) ([]context.Message, error) {
return messages, nil
} }

View file

@ -0,0 +1,8 @@
package hook
import "github.com/yaoapp/yao/agent/context"
// Create create a new assistant
func (s *Script) Create(ctx *context.Context, messages []context.Message) (*context.ResponseHookCreate, error) {
return &context.ResponseHookCreate{}, nil
}

View file

@ -0,0 +1,10 @@
package hook
import (
"github.com/yaoapp/yao/agent/context"
)
// Done done hook
func (s *Script) Done(ctx *context.Context, inputMessages []context.Message, completionResponse *context.ResponseCompletion, mcpResponse *context.ResponseHookMCP) (*context.ResponseHookDone, error) {
return &context.ResponseHookDone{}, nil
}

View file

@ -0,0 +1,8 @@
package hook
import "github.com/yaoapp/yao/agent/context"
// Failback failback hook
func (s *Script) Failback(ctx *context.Context, inputMessages []context.Message, completionResponse *context.ResponseCompletion) (*context.ResponseHookFailback, error) {
return &context.ResponseHookFailback{}, nil
}

View file

@ -0,0 +1 @@
package hook

View file

@ -0,0 +1,8 @@
package hook
import "github.com/yaoapp/yao/agent/context"
// MCP MCP hook
func (s *Script) MCP(ctx *context.Context, messages []context.Message) (*context.ResponseHookMCP, error) {
return &context.ResponseHookMCP{}, nil
}

View file

@ -0,0 +1,20 @@
package hook
import "github.com/yaoapp/yao/agent/context"
// Execute execute the script
func (s *Script) Execute(ctx *context.Context, method string, args ...interface{}) (interface{}, error) {
if s == nil || s.Script == nil {
return nil, nil
}
scriptCtx, err := s.NewContext(ctx.Sid, nil)
if err != nil {
return nil, err
}
defer scriptCtx.Close()
// The first argument is the context
args = append([]interface{}{ctx}, args...)
return scriptCtx.CallWith(ctx.Context, method, args...)
}

View file

@ -0,0 +1,10 @@
package hook
import (
v8 "github.com/yaoapp/gou/runtime/v8"
)
// Script the script hook align
type Script struct {
*v8.Script
}

View file

@ -1 +0,0 @@
package hooks

View file

@ -13,6 +13,7 @@ import (
"github.com/yaoapp/gou/application" "github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/fs" "github.com/yaoapp/gou/fs"
v8 "github.com/yaoapp/gou/runtime/v8" v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/agent/assistant/hook"
"github.com/yaoapp/yao/agent/i18n" "github.com/yaoapp/yao/agent/i18n"
store "github.com/yaoapp/yao/agent/store/types" store "github.com/yaoapp/yao/agent/store/types"
agentvision "github.com/yaoapp/yao/agent/vision" agentvision "github.com/yaoapp/yao/agent/vision"
@ -106,9 +107,7 @@ func LoadBuiltIn() error {
loaded.Put(assistant) loaded.Put(assistant)
// Remove the built-in assistant from the store // Remove the built-in assistant from the store
if _, ok := deletedBuiltIn[assistant.ID]; ok { delete(deletedBuiltIn, assistant.ID)
delete(deletedBuiltIn, assistant.ID)
}
} }
// Remove deleted built-in assistants // Remove deleted built-in assistants
@ -290,16 +289,16 @@ func LoadPath(path string) (*Assistant, error) {
data["updated_at"] = max(updatedAt, ts) data["updated_at"] = max(updatedAt, ts)
} }
// load tools // load tools, deprecated, use mcp instead
toolsfile := filepath.Join(path, "tools.yao") // toolsfile := filepath.Join(path, "tools.yao")
if has, _ := app.Exists(toolsfile); has { // if has, _ := app.Exists(toolsfile); has {
tools, ts, err := loadTools(toolsfile) // tools, ts, err := loadTools(toolsfile)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
data["tools"] = tools // data["tools"] = tools
updatedAt = max(updatedAt, ts) // updatedAt = max(updatedAt, ts)
} // }
// i18ns // i18ns
locales, err := i18n.GetLocales(path) locales, err := i18n.GetLocales(path)
@ -430,6 +429,9 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
} }
assistant.Tags = tags assistant.Tags = tags
case string:
assistant.Tags = []string{vv}
case interface{}: case interface{}:
raw, err := jsoniter.Marshal(vv) raw, err := jsoniter.Marshal(vv)
if err != nil { if err != nil {
@ -442,8 +444,6 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
} }
assistant.Tags = tags assistant.Tags = tags
case string:
assistant.Tags = []string{vv}
} }
} }
@ -571,9 +571,11 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
assistant.Script = script assistant.Script = &hook.Script{Script: script}
case *v8.Script: case *hook.Script:
assistant.Script = v assistant.Script = v
case *v8.Script:
assistant.Script = &hook.Script{Script: v}
} }
} }
@ -641,7 +643,7 @@ func loadPrompts(file string, root string) (string, int64, error) {
return string(prompts), ts.UnixNano(), nil return string(prompts), ts.UnixNano(), nil
} }
func loadScript(file string, root string) (*v8.Script, int64, error) { func loadScript(file string, root string) (*hook.Script, int64, error) {
app, err := fs.Get("app") app, err := fs.Get("app")
if err != nil { if err != nil {
@ -658,7 +660,7 @@ func loadScript(file string, root string) (*v8.Script, int64, error) {
return nil, 0, err return nil, 0, err
} }
return script, ts.UnixNano(), nil return &hook.Script{Script: script}, ts.UnixNano(), nil
} }
func loadScriptSource(source string, file string) (*v8.Script, error) { func loadScriptSource(source string, file string) (*v8.Script, error) {

View file

@ -5,7 +5,7 @@ import (
"io" "io"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
v8 "github.com/yaoapp/gou/runtime/v8" "github.com/yaoapp/yao/agent/assistant/hook"
chatctx "github.com/yaoapp/yao/agent/context" chatctx "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/message" "github.com/yaoapp/yao/agent/message"
store "github.com/yaoapp/yao/agent/store/types" store "github.com/yaoapp/yao/agent/store/types"
@ -96,16 +96,16 @@ type QueryParam struct {
type Assistant struct { type Assistant struct {
store.AssistantModel store.AssistantModel
Search *SearchOption `json:"search,omitempty" yaml:"search,omitempty"` // Whether this assistant supports search Search *SearchOption `json:"search,omitempty" yaml:"search,omitempty"` // Whether this assistant supports search
Script *v8.Script `json:"-" yaml:"-"` // Assistant Script Script *hook.Script `json:"-" yaml:"-"` // Assistant Script
// Internal // Internal
// =============================== // ===============================
openai *api.OpenAI // OpenAI API openai *api.OpenAI // OpenAI API
search bool // Whether this assistant supports search search bool // Whether this assistant supports search
vision bool // Whether this assistant supports vision vision bool // Whether this assistant supports vision
toolCalls bool // Whether this assistant supports tool_calls // toolCalls bool // Whether this assistant supports tool_calls
initHook bool // Whether this assistant has an init hook initHook bool // Whether this assistant has an init hook
runtimeTools []Tool // Converted tools for business logic (OpenAI format) runtimeTools []Tool // Converted tools for business logic (OpenAI format)
} }
// ConnectorSetting the connector setting // ConnectorSetting the connector setting

23
agent/context/jsapi.go Normal file
View file

@ -0,0 +1,23 @@
package context
import (
"rogchap.com/v8go"
)
// JsValue return the JavaScript value of the context
func (ctx *Context) JsValue(v8ctx *v8go.Context) (*v8go.Value, error) {
return ctx.NewObject(v8ctx)
}
// NewObject Create a new JavaScript object from the context
func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
jsObject.Set("ChatID", ctx.ChatID)
jsObject.Set("AssistantID", ctx.AssistantID)
jsObject.Set("Sid", ctx.Sid)
instance, err := jsObject.NewInstance(v8ctx)
if err != nil {
return nil, err
}
return instance.Value, nil
}

View file

@ -0,0 +1,58 @@
package context
import (
"testing"
"github.com/stretchr/testify/assert"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/gou/runtime/v8/bridge"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
"rogchap.com/v8go"
)
// TestJsValue test the JsValue function
func TestJsValue(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
cxt := &Context{
ChatID: "ChatID-123456",
AssistantID: "AssistantID-1234",
Sid: "Sid-1234",
}
v8.RegisterFunction("testContextJsvalue", testContextJsvalueEmbed)
res, err := v8.Call(v8.CallOptions{}, `
function test(cxt) {
return testContextJsvalue(cxt)
}`, cxt)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
assert.Equal(t, "ChatID-123456", res)
}
func testContextJsvalueEmbed(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, testContextJsvalueFunction)
}
func testContextJsvalueFunction(info *v8go.FunctionCallbackInfo) *v8go.Value {
var args = info.Args()
if len(args) < 1 {
return bridge.JsException(info.Context(), "Missing parameters")
}
ctx, err := args[0].AsObject()
if err != nil {
return bridge.JsException(info.Context(), err)
}
chatID, err := ctx.Get("ChatID")
if err != nil {
return bridge.JsException(info.Context(), err)
}
return chatID
}

View file

@ -131,6 +131,8 @@ type Context struct {
ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat
AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant
Sid string `json:"sid" yaml:"-"` // Session ID (Deprecated, use Authorized instead) Sid string `json:"sid" yaml:"-"` // Session ID (Deprecated, use Authorized instead)
Connector string `json:"connector,omitempty"` // Connector, use to select the connector of the LLM Model, Default is Assistant.Connector
Search *bool `json:"search,omitempty"` // Search mode, default is true
// Arguments for call // Arguments for call
Args []interface{} `json:"args,omitempty"` // Arguments for call, it will be used to pass data to the call Args []interface{} `json:"args,omitempty"` // Arguments for call, it will be used to pass data to the call
@ -181,7 +183,28 @@ type Stack struct {
// Response the response // Response the response
// 100% compatible with the OpenAI API // 100% compatible with the OpenAI API
type Response struct{} type Response struct {
Create *ResponseHookCreate `json:"create,omitempty"`
MCP *ResponseHookMCP `json:"mcp,omitempty"`
Done *ResponseHookDone `json:"done,omitempty"`
Failback *ResponseHookFailback `json:"failback,omitempty"`
Completion *ResponseCompletion `json:"completion,omitempty"`
}
// ResponseHookCreate the response of the create hook
type ResponseHookCreate struct{}
// ResponseHookDone the response of the done hook
type ResponseHookDone struct{}
// ResponseHookMCP the response of the mcp hook
type ResponseHookMCP struct{}
// ResponseHookFailback the response of the failback hook
type ResponseHookFailback struct{}
// ResponseCompletion the response of the completion
type ResponseCompletion struct{}
// Message Structure ( OpenAI Chat Completion Input Message Structure, https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages ) // Message Structure ( OpenAI Chat Completion Input Message Structure, https://platform.openai.com/docs/api-reference/chat/create#chat/create-messages )
// =============================== // ===============================

View file

@ -1,3 +1,4 @@
package jsapi package jsapi
// JSAPI Register the JavaScript API // JSAPI Register the JavaScript API
// Agent API will be registered as a third party object

9
agent/llm/interfaces.go Normal file
View file

@ -0,0 +1,9 @@
package llm
import "github.com/yaoapp/yao/agent/context"
// LLM the LLM interface
type LLM interface {
Stream(ctx *context.Context, messages []context.Message, options *CompletionOptions, handler context.StreamFunc) (*context.ResponseCompletion, error)
Post(ctx *context.Context, messages []context.Message, options *CompletionOptions) (*context.ResponseCompletion, error)
}

View file

@ -1 +1,6 @@
package llm package llm
// New create a new LLM instance
func New(connector string) (LLM, error) {
return nil, nil
}

4
agent/llm/types.go Normal file
View file

@ -0,0 +1,4 @@
package llm
// CompletionOptions the completion request
type CompletionOptions struct{}