Skip to content

Commit 916e045

Browse files
committed
Added home assistant
1 parent d3fe9f8 commit 916e045

22 files changed

Lines changed: 1374 additions & 5 deletions

cmd/llm/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type Globals struct {
3636
AnthropicAPIKey string `name:"anthropic-api-key" env:"ANTHROPIC_API_KEY" help:"Anthropic API key"`
3737
NewsAPIKey string `name:"news-api-key" env:"NEWS_API_KEY" help:"NewsAPI key"`
3838
WeatherAPIKey string `name:"weather-api-key" env:"WEATHER_API_KEY" help:"WeatherAPI key"`
39+
HAEndpoint string `name:"ha-endpoint" env:"HA_ENDPOINT" help:"Home Assistant endpoint URL"`
40+
HAToken string `name:"ha-token" env:"HA_TOKEN" help:"Home Assistant long-lived access token"`
3941

4042
// Tool options
4143
FsDir string `name:"fs" env:"FS_DIR" help:"Root directory for filesystem tools" type:"existingdir"`

cmd/llm/message.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import (
2020
// GLOBALS
2121

2222
const (
23-
ansiDim = "\033[2m" // Dim text for thinking
24-
ansiReset = "\033[0m" // Reset formatting
25-
ansiCyan = "\033[36m" // Cyan for thinking label
23+
ansiDim = "\033[2m" // Dim text for thinking
24+
ansiReset = "\033[0m" // Reset formatting
25+
ansiCyan = "\033[36m" // Cyan for thinking label
26+
ansiYellow = "\033[33m" // Yellow for tool calls
2627
)
2728

2829
///////////////////////////////////////////////////////////////////////////////
@@ -277,10 +278,12 @@ func streamCallback() opt.StreamFn {
277278
switch role {
278279
case "thinking":
279280
fmt.Print(ansiDim + ansiCyan)
281+
case "tool":
282+
fmt.Print(ansiReset + ansiDim + ansiYellow)
280283
default:
281284
fmt.Print(ansiReset)
282-
if lastRole == "thinking" {
283-
fmt.Println() // newline between thinking and assistant
285+
if lastRole == "thinking" || lastRole == "tool" {
286+
fmt.Println() // newline between thinking/tool and assistant
284287
}
285288
}
286289
lastRole = role

cmd/llm/tools.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
// Packages
88
fstool "github.com/mutablelogic/go-llm/pkg/fstool"
9+
homeassistant "github.com/mutablelogic/go-llm/pkg/homeassistant"
910
newsapi "github.com/mutablelogic/go-llm/pkg/newsapi"
1011
tool "github.com/mutablelogic/go-llm/pkg/tool"
1112
weatherapi "github.com/mutablelogic/go-llm/pkg/weatherapi"
@@ -68,6 +69,15 @@ func (g *Globals) Toolkit() (*tool.Toolkit, error) {
6869
tools = append(tools, fsTools...)
6970
}
7071

72+
// Add Home Assistant tools if endpoint and token are set
73+
if g.HAEndpoint != "" && g.HAToken != "" {
74+
haTools, err := homeassistant.NewTools(g.HAEndpoint, g.HAToken)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to create Home Assistant tools: %w", err)
77+
}
78+
tools = append(tools, haTools...)
79+
}
80+
7181
return tool.NewToolkit(tools...)
7282
}
7383

pkg/agent/generator.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ func (a *agent) WithSession(ctx context.Context, model schema.Model, session *sc
9898
break
9999
}
100100

101+
// Report tool calls via the streaming callback
102+
if streamfn := o.GetStream(); streamfn != nil {
103+
for _, call := range calls {
104+
streamfn("tool", tk.Feedback(call)+"\n")
105+
}
106+
}
107+
101108
// Execute each tool call and collect result blocks
102109
results := make([]schema.ContentBlock, 0, len(calls))
103110
for _, call := range calls {

pkg/homeassistant/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Home Assistant API Client
2+
3+
This package provides a client for the Home Assistant API, which is used to interact with the Home Assistant home automation system.
4+
5+
References:
6+
7+
- API <https://developers.home-assistant.io/docs/api/rest/>
8+
- Package <https://pkg.go.dev/github.com/mutablelogic/go-llm/pkg/homeassistant>

pkg/homeassistant/client.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
homeassistant implements an API client for Home Assistant API
3+
https://developers.home-assistant.io/docs/api/rest/
4+
*/
5+
package homeassistant
6+
7+
import (
8+
// Packages
9+
"github.com/mutablelogic/go-client"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// TYPES
14+
15+
type Client struct {
16+
*client.Client
17+
}
18+
19+
///////////////////////////////////////////////////////////////////////////////
20+
// LIFECYCLE
21+
22+
func New(endPoint, apiKey string, opts ...client.ClientOpt) (*Client, error) {
23+
// Add a final slash to the endpoint
24+
if len(endPoint) > 0 && endPoint[len(endPoint)-1] != '/' {
25+
endPoint += "/"
26+
}
27+
28+
// Create client
29+
client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptReqToken(client.Token{
30+
Scheme: client.Bearer,
31+
Value: apiKey,
32+
}))...)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
// Return the client
38+
return &Client{client}, nil
39+
}

pkg/homeassistant/client_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package homeassistant_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client"
9+
homeassistant "github.com/mutablelogic/go-llm/pkg/homeassistant"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_client_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
t.Log(client)
19+
}
20+
21+
///////////////////////////////////////////////////////////////////////////////
22+
// ENVIRONMENT
23+
24+
func GetApiKey(t *testing.T) string {
25+
key := os.Getenv("HA_TOKEN")
26+
if key == "" {
27+
t.Skip("HA_TOKEN not set")
28+
t.SkipNow()
29+
}
30+
return key
31+
}
32+
33+
func GetEndPoint(t *testing.T) string {
34+
key := os.Getenv("HA_ENDPOINT")
35+
if key == "" {
36+
t.Skip("HA_ENDPOINT not set")
37+
t.SkipNow()
38+
}
39+
return key
40+
}

pkg/homeassistant/config.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package homeassistant
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
// Packages
8+
"github.com/mutablelogic/go-client"
9+
)
10+
11+
///////////////////////////////////////////////////////////////////////////////
12+
// TYPES
13+
14+
// Config represents the Home Assistant server configuration.
15+
type Config struct {
16+
Components []string `json:"components"`
17+
ConfigDir string `json:"config_dir"`
18+
Elevation float64 `json:"elevation"`
19+
Latitude float64 `json:"latitude"`
20+
Longitude float64 `json:"longitude"`
21+
LocationName string `json:"location_name"`
22+
TimeZone string `json:"time_zone"`
23+
UnitSystem map[string]any `json:"unit_system"`
24+
Version string `json:"version"`
25+
ExternalDirs []string `json:"whitelist_external_dirs,omitempty"`
26+
AllowlistDirs []string `json:"allowlist_external_dirs,omitempty"`
27+
AllowlistURLs []string `json:"allowlist_external_urls,omitempty"`
28+
Currency string `json:"currency,omitempty"`
29+
Country string `json:"country,omitempty"`
30+
Language string `json:"language,omitempty"`
31+
SafeMode bool `json:"safe_mode,omitempty"`
32+
State string `json:"state,omitempty"`
33+
InternalURL string `json:"internal_url,omitempty"`
34+
ExternalURL string `json:"external_url,omitempty"`
35+
RecoveryMode bool `json:"recovery_mode,omitempty"`
36+
}
37+
38+
///////////////////////////////////////////////////////////////////////////////
39+
// API CALLS
40+
41+
// Config returns the current server configuration.
42+
func (c *Client) Config(ctx context.Context) (*Config, error) {
43+
var response Config
44+
if err := c.DoWithContext(ctx, nil, &response, client.OptPath("config")); err != nil {
45+
return nil, err
46+
}
47+
return &response, nil
48+
}
49+
50+
///////////////////////////////////////////////////////////////////////////////
51+
// STRINGIFY
52+
53+
func (v Config) String() string {
54+
data, _ := json.MarshalIndent(v, "", " ")
55+
return string(data)
56+
}

pkg/homeassistant/config_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package homeassistant_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
// Packages
9+
opts "github.com/mutablelogic/go-client"
10+
homeassistant "github.com/mutablelogic/go-llm/pkg/homeassistant"
11+
assert "github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_config_001(t *testing.T) {
15+
assert := assert.New(t)
16+
client, err := homeassistant.New(GetEndPoint(t), GetApiKey(t), opts.OptTrace(os.Stderr, true))
17+
assert.NoError(err)
18+
assert.NotNil(client)
19+
20+
config, err := client.Config(context.Background())
21+
assert.NoError(err)
22+
assert.NotNil(config)
23+
24+
t.Log(config)
25+
assert.NotEmpty(config.Version)
26+
assert.NotEmpty(config.LocationName)
27+
}

pkg/homeassistant/events.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package homeassistant
2+
3+
import (
4+
// Packages
5+
"context"
6+
7+
"github.com/mutablelogic/go-client"
8+
)
9+
10+
///////////////////////////////////////////////////////////////////////////////
11+
// TYPES
12+
13+
type Event struct {
14+
Event string `json:"event"`
15+
Listeners uint `json:"listener_count"`
16+
}
17+
18+
///////////////////////////////////////////////////////////////////////////////
19+
// API CALLS
20+
21+
// Events returns all the events and number of listeners
22+
func (c *Client) Events(ctx context.Context) ([]Event, error) {
23+
var response []Event
24+
if err := c.DoWithContext(ctx, nil, &response, client.OptPath("events")); err != nil {
25+
return nil, err
26+
}
27+
28+
// Return success
29+
return response, nil
30+
}
31+
32+
// FireEvent fires an event with the given type. Optional event data can be
33+
// passed as a map. Returns a confirmation message.
34+
func (c *Client) FireEvent(ctx context.Context, eventType string, eventData map[string]any) (string, error) {
35+
type responseMessage struct {
36+
Message string `json:"message"`
37+
}
38+
39+
var payload client.Payload
40+
var err error
41+
if len(eventData) > 0 {
42+
payload, err = client.NewJSONRequest(eventData)
43+
if err != nil {
44+
return "", err
45+
}
46+
} else {
47+
payload, err = client.NewJSONRequest(struct{}{})
48+
if err != nil {
49+
return "", err
50+
}
51+
}
52+
53+
var response responseMessage
54+
if err := c.DoWithContext(ctx, payload, &response, client.OptPath("events", eventType)); err != nil {
55+
return "", err
56+
}
57+
58+
return response.Message, nil
59+
}

0 commit comments

Comments
 (0)