try to fixup tests, enable e2e (#53)
* try to fixup tests, enable e2e Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Generate JSON character data with tools Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Rework generation of character Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Simplify text Signed-off-by: mudler <mudler@localai.io> * Relax some test constraints Signed-off-by: mudler <mudler@localai.io> * Fixups * Properly fit schema generation * Swap default model * ci fixups --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
31b5849d02
commit
e32a569796
172
pkg/client/agents.go
Normal file
172
pkg/client/agents.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package localagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AgentConfig represents the configuration for an agent
|
||||
type AgentConfig struct {
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Connectors []string `json:"connectors,omitempty"`
|
||||
PromptBlocks []string `json:"prompt_blocks,omitempty"`
|
||||
InitialPrompt string `json:"initial_prompt,omitempty"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
// AgentStatus represents the status of an agent
|
||||
type AgentStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ListAgents returns a list of all agents
|
||||
func (c *Client) ListAgents() ([]string, error) {
|
||||
resp, err := c.doRequest(http.MethodGet, "/agents", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The response is HTML, so we'll need to parse it properly
|
||||
// For now, we'll just return a placeholder implementation
|
||||
return []string{}, fmt.Errorf("ListAgents not implemented")
|
||||
}
|
||||
|
||||
// GetAgentConfig retrieves the configuration for a specific agent
|
||||
func (c *Client) GetAgentConfig(name string) (*AgentConfig, error) {
|
||||
path := fmt.Sprintf("/api/agent/%s/config", name)
|
||||
resp, err := c.doRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var config AgentConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// CreateAgent creates a new agent with the given configuration
|
||||
func (c *Client) CreateAgent(config *AgentConfig) error {
|
||||
resp, err := c.doRequest(http.MethodPost, "/create", config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to create agent: %v", response)
|
||||
}
|
||||
|
||||
// UpdateAgentConfig updates the configuration for an existing agent
|
||||
func (c *Client) UpdateAgentConfig(name string, config *AgentConfig) error {
|
||||
// Ensure the name in the URL matches the name in the config
|
||||
config.Name = name
|
||||
path := fmt.Sprintf("/api/agent/%s/config", name)
|
||||
|
||||
resp, err := c.doRequest(http.MethodPut, path, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to update agent: %v", response)
|
||||
}
|
||||
|
||||
// DeleteAgent removes an agent
|
||||
func (c *Client) DeleteAgent(name string) error {
|
||||
path := fmt.Sprintf("/delete/%s", name)
|
||||
resp, err := c.doRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to delete agent: %v", response)
|
||||
}
|
||||
|
||||
// PauseAgent pauses an agent
|
||||
func (c *Client) PauseAgent(name string) error {
|
||||
path := fmt.Sprintf("/pause/%s", name)
|
||||
resp, err := c.doRequest(http.MethodPut, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to pause agent: %v", response)
|
||||
}
|
||||
|
||||
// StartAgent starts a paused agent
|
||||
func (c *Client) StartAgent(name string) error {
|
||||
path := fmt.Sprintf("/start/%s", name)
|
||||
resp, err := c.doRequest(http.MethodPut, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"]; ok && status == "ok" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to start agent: %v", response)
|
||||
}
|
||||
|
||||
// ExportAgent exports an agent configuration
|
||||
func (c *Client) ExportAgent(name string) (*AgentConfig, error) {
|
||||
path := fmt.Sprintf("/settings/export/%s", name)
|
||||
resp, err := c.doRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var config AgentConfig
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
65
pkg/client/chat.go
Normal file
65
pkg/client/chat.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package localagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Message represents a chat message
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ChatResponse represents a response from the agent
|
||||
type ChatResponse struct {
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
// SendMessage sends a message to an agent
|
||||
func (c *Client) SendMessage(agentName, message string) error {
|
||||
path := fmt.Sprintf("/chat/%s", agentName)
|
||||
|
||||
msg := Message{
|
||||
Message: message,
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(http.MethodPost, path, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The response is HTML, so it's not easily parseable in this context
|
||||
return nil
|
||||
}
|
||||
|
||||
// Notify sends a notification to an agent
|
||||
func (c *Client) Notify(agentName, message string) error {
|
||||
path := fmt.Sprintf("/notify/%s", agentName)
|
||||
|
||||
// URL encoded form data
|
||||
form := strings.NewReader(fmt.Sprintf("message=%s", message))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, c.BaseURL+path, form)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
if c.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("api error (status %d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
pkg/client/client.go
Normal file
73
pkg/client/client.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package localagent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client represents a client for the LocalAgent API
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new LocalAgent client
|
||||
func NewClient(baseURL string, apiKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
APIKey: apiKey,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeout sets the HTTP client timeout
|
||||
func (c *Client) SetTimeout(timeout time.Duration) {
|
||||
c.HTTPClient.Timeout = timeout
|
||||
}
|
||||
|
||||
// doRequest performs an HTTP request and returns the response
|
||||
func (c *Client) doRequest(method, path string, body interface{}) (*http.Response, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonData, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s%s", c.BaseURL, path)
|
||||
req, err := http.NewRequest(method, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
if c.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
// Read the error response
|
||||
defer resp.Body.Close()
|
||||
errorData, _ := io.ReadAll(resp.Body)
|
||||
return resp, fmt.Errorf("api error (status %d): %s", resp.StatusCode, string(errorData))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
128
pkg/client/responses.go
Normal file
128
pkg/client/responses.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package localagent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequestBody represents the message request to the AI model
|
||||
type RequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Input string `json:"input"`
|
||||
InputMessages []InputMessage `json:"input_messages,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
MaxTokens *int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// InputMessage represents a user input message
|
||||
type InputMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content []ContentItem `json:"content"`
|
||||
}
|
||||
|
||||
// ContentItem represents an item in a content array
|
||||
type ContentItem struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// ResponseBody represents the response from the AI model
|
||||
type ResponseBody struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error interface{} `json:"error,omitempty"`
|
||||
Output []ResponseMessage `json:"output"`
|
||||
}
|
||||
|
||||
// ResponseMessage represents a message in the response
|
||||
type ResponseMessage struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
Content []MessageContentItem `json:"content"`
|
||||
}
|
||||
|
||||
// MessageContentItem represents a content item in a message
|
||||
type MessageContentItem struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// GetAIResponse sends a request to the AI model and returns the response
|
||||
func (c *Client) GetAIResponse(request *RequestBody) (*ResponseBody, error) {
|
||||
resp, err := c.doRequest(http.MethodPost, "/v1/responses", request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response ResponseBody
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||
}
|
||||
|
||||
// Check if there was an error in the response
|
||||
if response.Error != nil {
|
||||
return nil, fmt.Errorf("api error: %v", response.Error)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// SimpleAIResponse is a helper function to get a simple text response from the AI
|
||||
func (c *Client) SimpleAIResponse(agentName, input string) (string, error) {
|
||||
temperature := 0.7
|
||||
request := &RequestBody{
|
||||
Model: agentName,
|
||||
Input: input,
|
||||
Temperature: &temperature,
|
||||
}
|
||||
|
||||
response, err := c.GetAIResponse(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
return content.Text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no text response found")
|
||||
}
|
||||
|
||||
// ChatAIResponse sends chat messages to the AI model
|
||||
func (c *Client) ChatAIResponse(agentName string, messages []InputMessage) (string, error) {
|
||||
temperature := 0.7
|
||||
request := &RequestBody{
|
||||
Model: agentName,
|
||||
InputMessages: messages,
|
||||
Temperature: &temperature,
|
||||
}
|
||||
|
||||
response, err := c.GetAIResponse(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
return content.Text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no text response found")
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// generateAnswer generates an answer for the given text using the OpenAI API
|
||||
@@ -45,3 +46,43 @@ func GenerateJSONFromStruct(ctx context.Context, client *openai.Client, guidance
|
||||
}
|
||||
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{
|
||||
Model: model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: "Generate a character as JSON data. " + guidance,
|
||||
},
|
||||
},
|
||||
Tools: []openai.Tool{
|
||||
{
|
||||
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.FunctionDefinition{
|
||||
Name: "identity",
|
||||
Parameters: i,
|
||||
},
|
||||
},
|
||||
},
|
||||
ToolChoice: "identity",
|
||||
}
|
||||
|
||||
resp, err := client.CreateChatCompletion(ctx, decision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Choices) != 1 {
|
||||
return fmt.Errorf("no choices: %d", len(resp.Choices))
|
||||
}
|
||||
|
||||
msg := resp.Choices[0].Message
|
||||
|
||||
if len(msg.ToolCalls) == 0 {
|
||||
return fmt.Errorf("no tool calls: %d", len(msg.ToolCalls))
|
||||
}
|
||||
|
||||
return json.Unmarshal([]byte(msg.ToolCalls[0].Function.Arguments), dst)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user