feat(api): add endpoint to create group of dedicated agents (#79)
Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
d689bb4331
commit
c1ac7b675a
@@ -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 {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,18 +22,18 @@ import (
|
|||||||
|
|
||||||
type AgentPool struct {
|
type AgentPool struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
file string
|
file string
|
||||||
pooldir string
|
pooldir string
|
||||||
pool AgentPoolData
|
pool AgentPoolData
|
||||||
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
|
||||||
timeout string
|
timeout string
|
||||||
conversationLogs string
|
conversationLogs string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
@@ -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
10
main.go
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
70
webui/app.go
70
webui/app.go
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user