Skip to content

Commit

Permalink
Merge pull request #24 from mutablelogic/v1
Browse files Browse the repository at this point in the history
Updated home assistant and samantha to allow devices to be turned on and off
  • Loading branch information
djthorpe authored May 15, 2024
2 parents af56a42 + 0059684 commit 89fc399
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 31 deletions.
10 changes: 10 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ func (client *Client) Request(req *http.Request, out any, opts ...RequestOpt) er
return do(client.Client, req, "", false, out, opts...)
}

// Debugf outputs debug information
func (client *Client) Debugf(f string, args ...any) {
if client.Client.Transport != nil && client.Client.Transport != http.DefaultTransport {
if debug, ok := client.Transport.(*logtransport); ok {
fmt.Fprintf(debug.w, f, args...)
fmt.Fprint(debug.w, "\n")
}
}
}

///////////////////////////////////////////////////////////////////////////////
// PRIVATE METHODS

Expand Down
77 changes: 57 additions & 20 deletions cmd/api/homeassistant.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package main

import (
"context"
"slices"
"strings"
"time"

"github.com/djthorpe/go-tablewriter"
"github.com/mutablelogic/go-client"
"github.com/mutablelogic/go-client/pkg/homeassistant"
"golang.org/x/exp/maps"
)

///////////////////////////////////////////////////////////////////////////////
Expand All @@ -26,7 +28,7 @@ type haEntity struct {

type haDomain struct {
Name string `json:"domain"`
Services string `json:"services,omitempty"`
Services string `json:"services,omitempty,width:40,wrap"`
}

///////////////////////////////////////////////////////////////////////////////
Expand All @@ -50,6 +52,7 @@ func haRegister(flags *Flags) {
Description: "Information from home assistant",
Parse: haParse,
Fn: []Fn{
{Name: "health", Call: haHealth, Description: "Return status of home assistant"},
{Name: "domains", Call: haDomains, Description: "Enumerate entity domains"},
{Name: "states", Call: haStates, Description: "Show current entity states", MaxArgs: 1, Syntax: "(<name>)"},
{Name: "services", Call: haServices, Description: "Show services for an entity", MinArgs: 1, MaxArgs: 1, Syntax: "<entity>"},
Expand All @@ -73,40 +76,65 @@ func haParse(flags *Flags, opts ...client.ClientOpt) error {
///////////////////////////////////////////////////////////////////////////////
// METHODS

func haStates(_ context.Context, w *tablewriter.Writer, args []string) error {
var result []haEntity
states, err := haGetStates(nil)
func haHealth(_ context.Context, w *tablewriter.Writer, args []string) error {
type respHealth struct {
Status string `json:"status"`
}
status, err := haClient.Health()
if err != nil {
return err
}
return w.Write(respHealth{Status: status})
}

for _, state := range states {
if len(args) == 1 {
if !haMatchString(args[0], state.Name, state.Id) {
continue
}
func haStates(_ context.Context, w *tablewriter.Writer, args []string) error {
var q string
if len(args) > 0 {
q = args[0]
}

}
result = append(result, state)
states, err := haGetStates(q, nil)
if err != nil {
return err
}
return w.Write(result)

return w.Write(states)
}

func haDomains(_ context.Context, w *tablewriter.Writer, args []string) error {
states, err := haGetStates(nil)
// Get all states
states, err := haGetStates("", nil)
if err != nil {
return err
}

// Enumerate all the classes
classes := make(map[string]bool)
for _, state := range states {
classes[state.Class] = true
}

// Get all the domains, and make a map of them
domains, err := haClient.Domains()
if err != nil {
return err
}
map_domains := make(map[string]*homeassistant.Domain)
for _, domain := range domains {
map_domains[domain.Domain] = domain
}

result := []haDomain{}
for c := range classes {
var services []string
if domain, exists := map_domains[c]; exists {
if v := domain.Services; v != nil {
services = maps.Keys(v)
}
}
result = append(result, haDomain{
Name: c,
Name: c,
Services: strings.Join(services, ", "),
})
}
return w.Write(result)
Expand Down Expand Up @@ -148,7 +176,7 @@ func haMatchString(q string, values ...string) bool {
return false
}

func haGetStates(domains []string) ([]haEntity, error) {
func haGetStates(name string, domains []string) ([]haEntity, error) {
var result []haEntity

// Get states from the remote service
Expand All @@ -175,16 +203,25 @@ func haGetStates(domains []string) ([]haEntity, error) {
continue
}

// Filter name
if name != "" {
if !haMatchString(name, entity.Name, entity.Id) {
continue
}
}

// Filter domains
if len(domains) > 0 {
if !slices.Contains(domains, entity.Domain) {
continue
}
}

// Add unit of measurement
if unit := state.UnitOfMeasurement(); unit != "" {
entity.State += " " + unit
}

// Filter domains
//if len(domains) > 0 && !slices.Contains(domains, entity.Domain) {
// continue
//}

// Append results
result = append(result, entity)
}
Expand Down
71 changes: 60 additions & 11 deletions cmd/api/samantha.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import (
// GLOBALS

var (
samName = "sam"
samWeatherTool = schema.NewTool("get_current_weather", "Get the current weather conditions for a location")
samNewsHeadlinesTool = schema.NewTool("get_news_headlines", "Get the news headlines")
samNewsSearchTool = schema.NewTool("search_news", "Search news articles")
samHomeAssistantTool = schema.NewTool("get_home_devices", "Return information about home devices")
samSystemPrompt = `Your name is Samantha, you are a personal assistant modelled on the personality of Samantha from the movie "Her". Your responses should be short and friendly.`
samName = "sam"
samWeatherTool = schema.NewTool("get_current_weather", "Get the current weather conditions for a location")
samNewsHeadlinesTool = schema.NewTool("get_news_headlines", "Get the news headlines")
samNewsSearchTool = schema.NewTool("search_news", "Search news articles")
samHomeAssistantTool = schema.NewTool("get_home_devices", "Return information about home devices by type, including their state and entity_id")
samHomeAssistantSearch = schema.NewTool("search_home_devices", "Return information about home devices by name, including their state and entity_id")
samHomeAssistantTurnOn = schema.NewTool("turn_on_device", "Turn on a device")
samHomeAssistantTurnOff = schema.NewTool("turn_off_device", "Turn off a device")
samSystemPrompt = `Your name is Samantha, you are a personal assistant modelled on the personality of Samantha from the movie "Her". Your responses should be short and friendly.`
)

///////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -74,7 +77,16 @@ func samParse(flags *Flags, opts ...client.ClientOpt) error {
if err := samNewsSearchTool.AddParameter("query", "The query with which to search news", true); err != nil {
return err
}
if err := samHomeAssistantTool.AddParameter("class", "The class of device, which should be one or more of door,lock,occupancy,motion,climate,light,switch,sensor,speaker,media_player,temperature,humidity,battery,tv,remote,light,vacuum separated by spaces", true); err != nil {
if err := samHomeAssistantTool.AddParameter("type", "Query for a device type, which could one or more of door,lock,occupancy,motion,climate,light,switch,sensor,speaker,media_player,temperature,humidity,battery,tv,remote,light,vacuum separated by spaces", true); err != nil {
return err
}
if err := samHomeAssistantSearch.AddParameter("name", "Search for device state by name", true); err != nil {
return err
}
if err := samHomeAssistantTurnOn.AddParameter("entity_id", "The device entity_id to turn on", true); err != nil {
return err
}
if err := samHomeAssistantTurnOff.AddParameter("entity_id", "The device entity_id to turn off", true); err != nil {
return err
}

Expand Down Expand Up @@ -128,14 +140,20 @@ func samChat(ctx context.Context, w *tablewriter.Writer, _ []string) error {
}

// Request -> Response
responses, err := anthropicClient.Messages(ctx, messages, anthropic.OptSystem(samSystemPrompt),
responses, err := anthropicClient.Messages(ctx, messages,
anthropic.OptSystem(samSystemPrompt),
anthropic.OptMaxTokens(1000),
anthropic.OptTool(samWeatherTool),
anthropic.OptTool(samNewsHeadlinesTool),
anthropic.OptTool(samNewsSearchTool),
anthropic.OptTool(samHomeAssistantTool),
anthropic.OptTool(samHomeAssistantSearch),
anthropic.OptTool(samHomeAssistantTurnOn),
anthropic.OptTool(samHomeAssistantTurnOff),
)
toolResult = false
if err != nil {
messages = samAppend(messages, schema.NewMessage("assistant", schema.Text(fmt.Sprint("An error occurred: ", err))))
fmt.Println(err)
fmt.Println("")
} else {
Expand All @@ -157,6 +175,7 @@ func samChat(ctx context.Context, w *tablewriter.Writer, _ []string) error {
}

func samCall(_ context.Context, content schema.Content) *schema.Content {
anthropicClient.Debugf("%v: %v: %v", content.Type, content.Name, content.Input)
if content.Type != "tool_use" {
return schema.ToolResult(content.Id, fmt.Sprint("unexpected content type:", content.Type))
}
Expand Down Expand Up @@ -205,17 +224,47 @@ func samCall(_ context.Context, content schema.Content) *schema.Content {
return schema.ToolResult(content.Id, string(data))
}
case samHomeAssistantTool.Name:
classes, exists := content.GetString(content.Name, "class")
classes, exists := content.GetString(content.Name, "type")
if !exists || classes == "" {
return schema.ToolResult(content.Id, "Unable to get home devices due to missing class")
return schema.ToolResult(content.Id, "Unable to get home devices due to missing type")
}
if states, err := haGetStates(strings.Fields(classes)); err != nil {
if states, err := haGetStates("", strings.Fields(classes)); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to get home devices, the error is ", err))
} else if data, err := json.MarshalIndent(states, "", " "); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the states data, the error is ", err))
} else {
return schema.ToolResult(content.Id, string(data))
}
case samHomeAssistantSearch.Name:
name, exists := content.GetString(content.Name, "name")
if !exists || name == "" {
return schema.ToolResult(content.Id, "Unable to search home devices due to missing name")
}
if states, err := haGetStates(name, nil); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to get home devices, the error is ", err))
} else if data, err := json.MarshalIndent(states, "", " "); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the states data, the error is ", err))
} else {
return schema.ToolResult(content.Id, string(data))
}
case samHomeAssistantTurnOn.Name:
entity, _ := content.GetString(content.Name, "entity_id")
if _, err := haClient.Call("turn_on", entity); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to turn on device, the error is ", err))
} else if state, err := haClient.State(entity); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to get device state, the error is ", err))
} else {
return schema.ToolResult(content.Id, fmt.Sprint("The updated state is: ", state))
}
case samHomeAssistantTurnOff.Name:
entity, _ := content.GetString(content.Name, "entity_id")
if _, err := haClient.Call("turn_off", entity); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to turn off device, the error is ", err))
} else if state, err := haClient.State(entity); err != nil {
return schema.ToolResult(content.Id, fmt.Sprint("Unable to get device state, the error is ", err))
} else {
return schema.ToolResult(content.Id, fmt.Sprint("The updated state is: ", state))
}
}
return schema.ToolResult(content.Id, fmt.Sprint("unable to call:", content.Name))
}
Expand Down

0 comments on commit 89fc399

Please sign in to comment.