reordering

This commit is contained in:
Ettore Di Giacinto
2025-02-25 22:18:08 +01:00
parent d73fd545b2
commit 296734ba3b
46 changed files with 84 additions and 85 deletions

125
core/action/custom.go Normal file
View File

@@ -0,0 +1,125 @@
package action
import (
"context"
"fmt"
"strings"
"github.com/mudler/local-agent-framework/pkg/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error) {
a := &CustomAction{
config: config,
goPkgPath: goPkgPath,
}
if err := a.initializeInterpreter(); err != nil {
return nil, err
}
if err := a.callInit(); err != nil {
xlog.Error("Error calling custom action init", "error", err)
}
return a, nil
}
type CustomAction struct {
config map[string]string
goPkgPath string
i *interp.Interpreter
}
func (a *CustomAction) callInit() error {
if a.i == nil {
return nil
}
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
if err != nil {
return err
}
run := v.Interface().(func() error)
return run()
}
func (a *CustomAction) initializeInterpreter() error {
if _, exists := a.config["code"]; exists && a.i == nil {
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
i := interp.New(interp.Options{
GoPath: a.goPkgPath,
Unrestricted: unsafe,
})
if err := i.Use(stdlib.Symbols); err != nil {
return err
}
if _, exists := a.config["name"]; !exists {
a.config["name"] = "custom"
}
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
if err != nil {
return err
}
a.i = i
}
return nil
}
func (a *CustomAction) Run(ctx context.Context, params ActionParams) (string, error) {
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
if err != nil {
return "", err
}
run := v.Interface().(func(map[string]interface{}) (string, error))
return run(params)
}
func (a *CustomAction) Definition() ActionDefinition {
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
if err != nil {
xlog.Error("Error getting custom action definition", "error", err)
return ActionDefinition{}
}
properties := v.Interface().(func() map[string][]string)
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
if err != nil {
xlog.Error("Error getting custom action definition", "error", err)
return ActionDefinition{}
}
requiredFields := v.Interface().(func() []string)
prop := map[string]jsonschema.Definition{}
for k, v := range properties() {
if len(v) != 2 {
xlog.Error("Invalid property definition", "property", k)
continue
}
prop[k] = jsonschema.Definition{
Type: jsonschema.DataType(v[0]),
Description: v[1],
}
}
return ActionDefinition{
Name: ActionDefinitionName(a.config["name"]),
Description: a.config["description"],
Properties: prop,
Required: requiredFields(),
}
}

View File

@@ -0,0 +1,86 @@
package action_test
import (
"context"
. "github.com/mudler/local-agent-framework/core/action"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai/jsonschema"
)
var _ = Describe("Agent custom action", func() {
Context("custom action", func() {
It("initializes correctly", func() {
testCode := `
import (
"encoding/json"
)
type Params struct {
Foo string
}
func Run(config map[string]interface{}) (string, error) {
p := Params{}
b, err := json.Marshal(config)
if err != nil {
return "", err
}
if err := json.Unmarshal(b, &p); err != nil {
return "", err
}
return p.Foo, nil
}
func Definition() map[string][]string {
return map[string][]string{
"foo": []string{
"string",
"The foo value",
},
}
}
func RequiredFields() []string {
return []string{"foo"}
}
`
customAction, err := NewCustom(
map[string]string{
"code": testCode,
"name": "test",
"description": "A test action",
},
"",
)
Expect(err).ToNot(HaveOccurred())
definition := customAction.Definition()
Expect(definition).To(Equal(ActionDefinition{
Properties: map[string]jsonschema.Definition{
"foo": {
Type: jsonschema.String,
Description: "The foo value",
},
},
Required: []string{"foo"},
Name: "test",
Description: "A test action",
}))
runResult, err := customAction.Run(context.Background(), ActionParams{
"Foo": "bar",
})
Expect(err).ToNot(HaveOccurred())
Expect(runResult).To(Equal("bar"))
})
})
})

81
core/action/definition.go Normal file
View File

@@ -0,0 +1,81 @@
package action
import (
"context"
"encoding/json"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
type ActionContext struct {
context.Context
cancelFunc context.CancelFunc
}
func (ac *ActionContext) Cancel() {
if ac.cancelFunc == nil {
ac.cancelFunc()
}
}
func NewContext(ctx context.Context, cancel context.CancelFunc) *ActionContext {
return &ActionContext{
Context: ctx,
cancelFunc: cancel,
}
}
type ActionParams map[string]interface{}
func (ap ActionParams) Read(s string) error {
err := json.Unmarshal([]byte(s), &ap)
return err
}
func (ap ActionParams) String() string {
b, _ := json.Marshal(ap)
return string(b)
}
func (ap ActionParams) Unmarshal(v interface{}) error {
b, err := json.Marshal(ap)
if err != nil {
return err
}
if err := json.Unmarshal(b, v); err != nil {
return err
}
return nil
}
//type ActionDefinition openai.FunctionDefinition
type ActionDefinition struct {
Properties map[string]jsonschema.Definition
Required []string
Name ActionDefinitionName
Description string
}
type ActionDefinitionName string
func (a ActionDefinitionName) Is(name string) bool {
return string(a) == name
}
func (a ActionDefinitionName) String() string {
return string(a)
}
func (a ActionDefinition) ToFunctionDefinition() openai.FunctionDefinition {
return openai.FunctionDefinition{
Name: a.Name.String(),
Description: a.Description,
Parameters: jsonschema.Definition{
Type: jsonschema.Object,
Properties: a.Properties,
Required: a.Required,
},
}
}

45
core/action/intention.go Normal file
View File

@@ -0,0 +1,45 @@
package action
import (
"context"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewIntention creates a new intention action
// The inention action is special as it tries to identify
// a tool to use and a reasoning over to use it
func NewIntention(s ...string) *IntentAction {
return &IntentAction{tools: s}
}
type IntentAction struct {
tools []string
}
type IntentResponse struct {
Tool string `json:"tool"`
Reasoning string `json:"reasoning"`
}
func (a *IntentAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *IntentAction) Definition() ActionDefinition {
return ActionDefinition{
Name: "pick_tool",
Description: "Pick a tool",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on why you want to call this tool.",
},
"tool": {
Type: jsonschema.String,
Description: "The tool you want to use",
Enum: a.tools,
},
},
Required: []string{"tool", "reasoning"},
}
}

View File

@@ -0,0 +1,37 @@
package action
import (
"context"
"github.com/sashabaranov/go-openai/jsonschema"
)
const ConversationActionName = "new_conversation"
func NewConversation() *ConversationAction {
return &ConversationAction{}
}
type ConversationAction struct{}
type ConversationActionResponse struct {
Message string `json:"message"`
}
func (a *ConversationAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *ConversationAction) Definition() ActionDefinition {
return ActionDefinition{
Name: ConversationActionName,
Description: "Use this tool to initiate a new conversation or to notify something.",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to start the conversation",
},
},
Required: []string{"message"},
}
}

24
core/action/noreply.go Normal file
View File

@@ -0,0 +1,24 @@
package action
import "context"
// StopActionName is the name of the action
// used by the LLM to stop any further action
const StopActionName = "stop"
func NewStop() *StopAction {
return &StopAction{}
}
type StopAction struct{}
func (a *StopAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *StopAction) Definition() ActionDefinition {
return ActionDefinition{
Name: StopActionName,
Description: "Use this tool to stop any further action and stop the conversation. You must use this when it looks like there is a conclusion to the conversation or the topic diverged too much from the original conversation. For instance if the user offer his help and you already replied with a message, you can use this tool to stop the conversation.",
}
}

53
core/action/plan.go Normal file
View File

@@ -0,0 +1,53 @@
package action
import (
"context"
"github.com/sashabaranov/go-openai/jsonschema"
)
// PlanActionName is the name of the plan action
// used by the LLM to schedule more actions
const PlanActionName = "plan"
func NewPlan() *PlanAction {
return &PlanAction{}
}
type PlanAction struct{}
type PlanResult struct {
Subtasks []PlanSubtask `json:"subtasks"`
}
type PlanSubtask struct {
Action string `json:"action"`
Reasoning string `json:"reasoning"`
}
func (a *PlanAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *PlanAction) Definition() ActionDefinition {
return ActionDefinition{
Name: PlanActionName,
Description: "The assistant for solving complex tasks that involves calling more functions in sequence, replies with the action.",
Properties: map[string]jsonschema.Definition{
"subtasks": {
Type: jsonschema.Array,
Description: "The message to reply with",
Properties: map[string]jsonschema.Definition{
"action": {
Type: jsonschema.String,
Description: "The action to call",
},
"reasoning": {
Type: jsonschema.String,
Description: "The reasoning for calling this action",
},
},
},
},
Required: []string{"subtasks"},
}
}

38
core/action/reasoning.go Normal file
View File

@@ -0,0 +1,38 @@
package action
import (
"context"
"github.com/sashabaranov/go-openai/jsonschema"
)
// NewReasoning creates a new reasoning action
// The reasoning action is special as it tries to force the LLM
// to think about what to do next
func NewReasoning() *ReasoningAction {
return &ReasoningAction{}
}
type ReasoningAction struct{}
type ReasoningResponse struct {
Reasoning string `json:"reasoning"`
}
func (a *ReasoningAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *ReasoningAction) Definition() ActionDefinition {
return ActionDefinition{
Name: "pick_action",
Description: "try to understand what's the best thing to do and pick an action with a reasoning",
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
Description: "A detailed reasoning on what would you do in this situation.",
},
},
Required: []string{"reasoning"},
}
}

40
core/action/reply.go Normal file
View File

@@ -0,0 +1,40 @@
package action
import (
"context"
"github.com/sashabaranov/go-openai/jsonschema"
)
// ReplyActionName is the name of the reply action
// used by the LLM to reply to the user without
// any additional processing
const ReplyActionName = "reply"
func NewReply() *ReplyAction {
return &ReplyAction{}
}
type ReplyAction struct{}
type ReplyResponse struct {
Message string `json:"message"`
}
func (a *ReplyAction) Run(context.Context, ActionParams) (string, error) {
return "no-op", nil
}
func (a *ReplyAction) Definition() ActionDefinition {
return ActionDefinition{
Name: ReplyActionName,
Description: "Use this tool to reply to the user once we have all the informations we need.",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to reply with",
},
},
Required: []string{"message"},
}
}

93
core/action/state.go Normal file
View File

@@ -0,0 +1,93 @@
package action
import (
"context"
"fmt"
"github.com/sashabaranov/go-openai/jsonschema"
)
const StateActionName = "update_state"
func NewState() *StateAction {
return &StateAction{}
}
type StateAction struct{}
// State is the structure
// that is used to keep track of the current state
// and the Agent's short memory that it can update
// Besides a long term memory that is accessible by the agent (With vector database),
// And a context memory (that is always powered by a vector database),
// this memory is the shorter one that the LLM keeps across conversation and across its
// reasoning process's and life time.
// TODO: A special action is then used to let the LLM itself update its memory
// periodically during self-processing, and the same action is ALSO exposed
// during the conversation to let the user put for example, a new goal to the agent.
type StateResult struct {
NowDoing string `json:"doing_now"`
DoingNext string `json:"doing_next"`
DoneHistory []string `json:"done_history"`
Memories []string `json:"memories"`
Goal string `json:"goal"`
}
func (a *StateAction) Run(context.Context, ActionParams) (string, error) {
return "internal state has been updated", nil
}
func (a *StateAction) Definition() ActionDefinition {
return ActionDefinition{
Name: StateActionName,
Description: "update the agent state (short memory) with the current state of the conversation.",
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The current goal of the agent.",
},
"doing_next": {
Type: jsonschema.String,
Description: "The next action the agent will do.",
},
"done_history": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
Description: "A list of actions that the agent has done.",
},
"now_doing": {
Type: jsonschema.String,
Description: "The current action the agent is doing.",
},
"memories": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
Description: "A list of memories to keep between conversations.",
},
},
}
}
const fmtT = `=====================
NowDoing: %s
DoingNext: %s
Your current goal is: %s
You have done: %+v
You have a short memory with: %+v
=====================
`
func (c StateResult) String() string {
return fmt.Sprintf(
fmtT,
c.NowDoing,
c.DoingNext,
c.Goal,
c.DoneHistory,
c.Memories,
)
}