feat(api): add endpoint to create group of dedicated agents (#79)

Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2025-03-22 18:44:22 +01:00
committed by GitHub
parent d689bb4331
commit c1ac7b675a
7 changed files with 136 additions and 57 deletions

View File

@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
guidance = "Generate a random character for roleplaying." guidance = "Generate a random character for roleplaying."
} }
err := llm.GenerateTypedJSON(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character) err := llm.GenerateTypedJSON(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character) //err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
a.Character = a.options.character a.Character = a.options.character
if err != nil { if err != nil {

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"sync" "sync"
"time" "time"
@@ -27,7 +28,7 @@ type AgentPool struct {
agents map[string]*Agent agents map[string]*Agent
managers map[string]sse.Manager managers map[string]sse.Manager
agentStatus map[string]*Status agentStatus map[string]*Status
apiURL, defaultModel, defaultMultimodalModel, localRAGAPI, localRAGKey, apiKey string apiURL, defaultModel, defaultMultimodalModel, imageModel, localRAGAPI, localRAGKey, apiKey string
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []Action availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []Action
connectors func(*AgentConfig) []Connector connectors func(*AgentConfig) []Connector
promptBlocks func(*AgentConfig) []PromptBlock promptBlocks func(*AgentConfig) []PromptBlock
@@ -66,7 +67,7 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
} }
func NewAgentPool( func NewAgentPool(
defaultModel, defaultMultimodalModel, apiURL, apiKey, directory string, defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory string,
LocalRAGAPI string, LocalRAGAPI string,
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []agent.Action, availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []agent.Action,
connectors func(*AgentConfig) []Connector, connectors func(*AgentConfig) []Connector,
@@ -92,6 +93,7 @@ func NewAgentPool(
apiURL: apiURL, apiURL: apiURL,
defaultModel: defaultModel, defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel, defaultMultimodalModel: defaultMultimodalModel,
imageModel: imageModel,
localRAGAPI: LocalRAGAPI, localRAGAPI: LocalRAGAPI,
apiKey: apiKey, apiKey: apiKey,
agents: make(map[string]*Agent), agents: make(map[string]*Agent),
@@ -116,6 +118,7 @@ func NewAgentPool(
pooldir: directory, pooldir: directory,
defaultModel: defaultModel, defaultModel: defaultModel,
defaultMultimodalModel: defaultMultimodalModel, defaultMultimodalModel: defaultMultimodalModel,
imageModel: imageModel,
apiKey: apiKey, apiKey: apiKey,
agents: make(map[string]*Agent), agents: make(map[string]*Agent),
managers: make(map[string]sse.Manager), managers: make(map[string]sse.Manager),
@@ -130,12 +133,18 @@ func NewAgentPool(
}, nil }, nil
} }
func replaceInvalidChars(s string) string {
s = strings.ReplaceAll(s, "/", "_")
return strings.ReplaceAll(s, " ", "_")
}
// CreateAgent adds a new agent to the pool // CreateAgent adds a new agent to the pool
// and starts it. // and starts it.
// It also saves the state to the file. // It also saves the state to the file.
func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error { func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
a.Lock() a.Lock()
defer a.Unlock() defer a.Unlock()
name = replaceInvalidChars(name)
if _, ok := a.pool[name]; ok { if _, ok := a.pool[name]; ok {
return fmt.Errorf("agent %s already exists", name) return fmt.Errorf("agent %s already exists", name)
} }

10
main.go
View File

@@ -20,6 +20,7 @@ var stateDir = os.Getenv("LOCALAGENT_STATE_DIR")
var localRAG = os.Getenv("LOCALAGENT_LOCALRAG_URL") var localRAG = os.Getenv("LOCALAGENT_LOCALRAG_URL")
var withLogs = os.Getenv("LOCALAGENT_ENABLE_CONVERSATIONS_LOGGING") == "true" var withLogs = os.Getenv("LOCALAGENT_ENABLE_CONVERSATIONS_LOGGING") == "true"
var apiKeysEnv = os.Getenv("LOCALAGENT_API_KEYS") var apiKeysEnv = os.Getenv("LOCALAGENT_API_KEYS")
var imageModel = os.Getenv("LOCALAGENT_IMAGE_MODEL")
func init() { func init() {
if testModel == "" { if testModel == "" {
@@ -54,6 +55,7 @@ func main() {
pool, err := state.NewAgentPool( pool, err := state.NewAgentPool(
testModel, testModel,
multimodalModel, multimodalModel,
imageModel,
apiURL, apiURL,
apiKey, apiKey,
stateDir, stateDir,
@@ -69,7 +71,13 @@ func main() {
} }
// Create the application // Create the application
app := webui.NewApp(webui.WithPool(pool), webui.WithApiKeys(apiKeys...)) app := webui.NewApp(
webui.WithPool(pool),
webui.WithApiKeys(apiKeys...),
webui.WithLLMAPIUrl(apiURL),
webui.WithLLMAPIKey(apiKey),
webui.WithLLMModel(testModel),
)
// Start the agents // Start the agents
if err := pool.StartAll(); err != nil { if err := pool.StartAll(); err != nil {

View File

@@ -5,55 +5,19 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema" "github.com/sashabaranov/go-openai/jsonschema"
) )
// generateAnswer generates an answer for the given text using the OpenAI API func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
func GenerateJSON(ctx context.Context, client *openai.Client, model, text string, i interface{}) error { toolName := "json"
req := openai.ChatCompletionRequest{
ResponseFormat: &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject},
Model: model,
Messages: []openai.ChatCompletionMessage{
{
Role: "user",
Content: text,
},
},
}
resp, err := client.CreateChatCompletion(ctx, req)
if err != nil {
return fmt.Errorf("failed to generate answer: %v", err)
}
if len(resp.Choices) == 0 {
return fmt.Errorf("no response from OpenAI API")
}
err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), i)
if err != nil {
return err
}
return nil
}
func GenerateJSONFromStruct(ctx context.Context, client *openai.Client, guidance, model string, i interface{}) error {
// TODO: use functions?
exampleJSON, err := json.Marshal(i)
if err != nil {
return err
}
return GenerateJSON(ctx, client, model, "Generate a character as JSON data. "+guidance+". This is the JSON fields that should contain: "+string(exampleJSON), i)
}
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst interface{}) error {
decision := openai.ChatCompletionRequest{ decision := openai.ChatCompletionRequest{
Model: model, Model: model,
Messages: []openai.ChatCompletionMessage{ Messages: []openai.ChatCompletionMessage{
{ {
Role: "user", Role: "user",
Content: "Generate a character as JSON data. " + guidance, Content: guidance,
}, },
}, },
Tools: []openai.Tool{ Tools: []openai.Tool{
@@ -61,12 +25,15 @@ func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, mod
Type: openai.ToolTypeFunction, Type: openai.ToolTypeFunction,
Function: openai.FunctionDefinition{ Function: openai.FunctionDefinition{
Name: "identity", Name: toolName,
Parameters: i, Parameters: i,
}, },
}, },
}, },
ToolChoice: "identity", ToolChoice: openai.ToolChoice{
Type: openai.ToolTypeFunction,
Function: openai.ToolFunction{Name: toolName},
},
} }
resp, err := client.CreateChatCompletion(ctx, decision) resp, err := client.CreateChatCompletion(ctx, decision)
@@ -84,5 +51,7 @@ func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, mod
return fmt.Errorf("no tool calls: %d", len(msg.ToolCalls)) return fmt.Errorf("no tool calls: %d", len(msg.ToolCalls))
} }
xlog.Debug("JSON generated", "Arguments", msg.ToolCalls[0].Function.Arguments)
return json.Unmarshal([]byte(msg.ToolCalls[0].Function.Arguments), dst) return json.Unmarshal([]byte(msg.ToolCalls[0].Function.Arguments), dst)
} }

View File

@@ -9,9 +9,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/mudler/LocalAgent/pkg/llm"
"github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/pkg/xlog"
"github.com/mudler/LocalAgent/services" "github.com/mudler/LocalAgent/services"
"github.com/mudler/LocalAgent/webui/types" "github.com/mudler/LocalAgent/webui/types"
"github.com/sashabaranov/go-openai/jsonschema"
"github.com/mudler/LocalAgent/core/action" "github.com/mudler/LocalAgent/core/action"
"github.com/mudler/LocalAgent/core/agent" "github.com/mudler/LocalAgent/core/agent"
@@ -396,3 +398,71 @@ func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error {
return c.JSON(response) return c.JSON(response)
} }
} }
type AgentRole struct {
Name string `json:"name"`
Description string `json:"description"`
SystemPrompt string `json:"system_prompt"`
}
func (a *App) CreateGroup(pool *state.AgentPool) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
var request struct {
Descript string `json:"description"`
}
if err := c.BodyParser(&request); err != nil {
return errorJSONMessage(c, err.Error())
}
var results struct {
Agents []AgentRole `json:"agents"`
}
xlog.Debug("Generating group", "description", request.Descript)
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
err := llm.GenerateTypedJSON(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"agents": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Required: []string{"name", "description", "system_prompt"},
Properties: map[string]jsonschema.Definition{
"name": {
Type: jsonschema.String,
Description: "The name of the agent",
},
"description": {
Type: jsonschema.String,
Description: "The description of the agent",
},
"system_prompt": {
Type: jsonschema.String,
Description: "The system prompt for the agent",
},
},
},
},
},
}, &results)
if err != nil {
return errorJSONMessage(c, err.Error())
}
for _, agent := range results.Agents {
xlog.Info("Creating agent", "name", agent.Name, "description", agent.Description)
config := state.AgentConfig{
Name: agent.Name,
Description: agent.Description,
SystemPrompt: agent.SystemPrompt,
}
if err := pool.CreateAgent(agent.Name, &config); err != nil {
return errorJSONMessage(c, err.Error())
}
}
return c.JSON(results)
}
}

View File

@@ -6,6 +6,9 @@ type Config struct {
DefaultChunkSize int DefaultChunkSize int
Pool *state.AgentPool Pool *state.AgentPool
ApiKeys []string ApiKeys []string
LLMAPIURL string
LLMAPIKey string
LLMModel string
} }
type Option func(*Config) type Option func(*Config)
@@ -16,6 +19,24 @@ func WithDefaultChunkSize(size int) Option {
} }
} }
func WithLLMModel(model string) Option {
return func(c *Config) {
c.LLMModel = model
}
}
func WithLLMAPIUrl(url string) Option {
return func(c *Config) {
c.LLMAPIURL = url
}
}
func WithLLMAPIKey(key string) Option {
return func(c *Config) {
c.LLMAPIKey = key
}
}
func WithPool(pool *state.AgentPool) Option { func WithPool(pool *state.AgentPool) Option {
return func(c *Config) { return func(c *Config) {
c.Pool = pool c.Pool = pool

View File

@@ -141,6 +141,8 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
webapp.Post("/action/:name/run", app.ExecuteAction(pool)) webapp.Post("/action/:name/run", app.ExecuteAction(pool))
webapp.Get("/actions", app.ListActions()) webapp.Get("/actions", app.ListActions())
webapp.Post("/api/agent/group/create", app.CreateGroup(pool))
webapp.Post("/settings/import", app.ImportAgent(pool)) webapp.Post("/settings/import", app.ImportAgent(pool))
webapp.Get("/settings/export/:name", app.ExportAgent(pool)) webapp.Get("/settings/export/:name", app.ExportAgent(pool))
} }