Skip to content

Commit 410db6e

Browse files
authored
Merge pull request #1 from mutablelogic/dev
Initial merge into main
2 parents e56ab1a + cd5c7d0 commit 410db6e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+5587
-16
lines changed

.github/workflows/docker.yaml

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Create Docker Image
2+
on:
3+
release:
4+
types:
5+
- created
6+
7+
workflow_dispatch:
8+
9+
jobs:
10+
build:
11+
name: Build
12+
strategy:
13+
matrix:
14+
arch: [ amd64, arm64 ]
15+
runs-on:
16+
- ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || matrix.arch }}
17+
env:
18+
OS: linux
19+
ARCH: ${{ matrix.arch }}
20+
DOCKER_REPO: ghcr.io/${{ github.repository }}
21+
DOCKER_SOURCE: https://github.com/${{ github.repository }}
22+
outputs:
23+
tag: ${{ steps.build.outputs.tag }}
24+
permissions:
25+
contents: read
26+
packages: write
27+
steps:
28+
- name: Install build tools
29+
run: |
30+
sudo apt -y update
31+
sudo apt -y install build-essential git
32+
git config --global advice.detachedHead false
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
- name: Login
38+
uses: docker/login-action@v3
39+
with:
40+
registry: ghcr.io
41+
username: ${{ github.repository_owner }}
42+
password: ${{ secrets.GITHUB_TOKEN }}
43+
- name: Build and Push
44+
id: build
45+
run: |
46+
make docker && make docker-push && make docker-version >> "$GITHUB_OUTPUT"
47+
manifest:
48+
name: Manifest
49+
needs: build
50+
strategy:
51+
matrix:
52+
tag:
53+
- ${{ needs.build.outputs.tag }}
54+
- "latest"
55+
runs-on: ubuntu-latest
56+
permissions:
57+
packages: write
58+
steps:
59+
- name: Login
60+
uses: docker/login-action@v3
61+
with:
62+
registry: ghcr.io
63+
username: ${{ github.repository_owner }}
64+
password: ${{ secrets.GITHUB_TOKEN }}
65+
- name: Create
66+
run: |
67+
docker manifest create ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
68+
--amend ghcr.io/${{ github.repository }}-linux-amd64:${{ needs.build.outputs.tag }} \
69+
--amend ghcr.io/${{ github.repository }}-linux-arm64:${{ needs.build.outputs.tag }}
70+
- name: Annotate
71+
run: |
72+
docker manifest annotate --arch amd64 --os linux \
73+
ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
74+
ghcr.io/${{ github.repository }}-linux-amd64:${{ needs.build.outputs.tag }}
75+
docker manifest annotate --arch arm64 --os linux \
76+
ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
77+
ghcr.io/${{ github.repository }}-linux-arm64:${{ needs.build.outputs.tag }}
78+
- name: Push
79+
run: |
80+
docker manifest push ghcr.io/${{ github.repository }}:${{ matrix.tag }}

.gitignore

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
1-
# If you prefer the allow list template instead of the deny list, see community template:
2-
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3-
#
4-
# Binaries for programs and plugins
51
*.exe
62
*.exe~
73
*.dll
84
*.so
95
*.dylib
10-
11-
# Test binary, built with `go test -c`
126
*.test
13-
14-
# Output of the go coverage tool, specifically when used with LiteIDE
157
*.out
16-
17-
# Dependency directories (remove the comment below to include it)
18-
# vendor/
19-
20-
# Go workspace file
218
go.work
229
go.work.sum
23-
24-
# env file
25-
.env
10+
vendor/
11+
build/
12+
.DS_Store

Makefile

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Executables
2+
GO ?= $(shell which go 2>/dev/null)
3+
DOCKER ?= $(shell which docker 2>/dev/null)
4+
5+
# Locations
6+
BUILD_DIR ?= build
7+
CMD_DIR := $(wildcard cmd/*)
8+
9+
# VERBOSE=1
10+
ifneq ($(VERBOSE),)
11+
VERBOSE_FLAG = -v
12+
else
13+
VERBOSE_FLAG =
14+
endif
15+
16+
# Set OS and Architecture
17+
ARCH ?= $(shell arch | tr A-Z a-z | sed 's/x86_64/amd64/' | sed 's/i386/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/')
18+
OS ?= $(shell uname | tr A-Z a-z)
19+
VERSION ?= $(shell git describe --tags --always | sed 's/^v//')
20+
21+
# Set build flags
22+
BUILD_MODULE = $(shell cat go.mod | head -1 | cut -d ' ' -f 2)
23+
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitSource=${BUILD_MODULE}
24+
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitTag=$(shell git describe --tags --always)
25+
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitBranch=$(shell git name-rev HEAD --name-only --always)
26+
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitHash=$(shell git rev-parse HEAD)
27+
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GoBuildTime=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
28+
BUILD_FLAGS = -ldflags "-s -w ${BUILD_LD_FLAGS}"
29+
30+
# Docker
31+
DOCKER_REPO ?= ghcr.io/mutablelogic/go-llm
32+
DOCKER_SOURCE ?= ${BUILD_MODULE}
33+
DOCKER_TAG = ${DOCKER_REPO}-${OS}-${ARCH}:${VERSION}
34+
35+
###############################################################################
36+
# ALL
37+
38+
.PHONY: all
39+
all: clean build
40+
41+
###############################################################################
42+
# BUILD
43+
44+
# Build the commands in the cmd directory
45+
.PHONY: build
46+
build: tidy $(CMD_DIR)
47+
48+
$(CMD_DIR): go-dep mkdir
49+
@echo Build command $(notdir $@) GOOS=${OS} GOARCH=${ARCH}
50+
@GOOS=${OS} GOARCH=${ARCH} ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@
51+
52+
# Build the docker image
53+
.PHONY: docker
54+
docker: docker-dep
55+
@echo build docker image ${DOCKER_TAG} OS=${OS} ARCH=${ARCH} SOURCE=${DOCKER_SOURCE} VERSION=${VERSION}
56+
@${DOCKER} build \
57+
--tag ${DOCKER_TAG} \
58+
--build-arg ARCH=${ARCH} \
59+
--build-arg OS=${OS} \
60+
--build-arg SOURCE=${DOCKER_SOURCE} \
61+
--build-arg VERSION=${VERSION} \
62+
-f etc/docker/Dockerfile .
63+
64+
# Push docker container
65+
.PHONY: docker-push
66+
docker-push: docker-dep
67+
@echo push docker image: ${DOCKER_TAG}
68+
@${DOCKER} push ${DOCKER_TAG}
69+
70+
# Print out the version
71+
.PHONY: docker-version
72+
docker-version: docker-dep
73+
@echo "tag=${VERSION}"
74+
75+
###############################################################################
76+
# TEST
77+
78+
.PHONY: test
79+
test: unit-test coverage-test
80+
81+
.PHONY: unit-test
82+
unit-test: go-dep
83+
@echo Unit Tests
84+
@${GO} test ${VERBOSE_FLAG} ./pkg/...
85+
86+
.PHONY: coverage-test
87+
coverage-test: go-dep mkdir
88+
@echo Test Coverage
89+
@${GO} test -coverprofile ${BUILD_DIR}/coverprofile.out ./pkg/...
90+
91+
###############################################################################
92+
# CLEAN
93+
94+
.PHONY: tidy
95+
tidy:
96+
@echo Running go mod tidy
97+
@${GO} mod tidy
98+
99+
.PHONY: mkdir
100+
mkdir:
101+
@install -d ${BUILD_DIR}
102+
103+
.PHONY: clean
104+
clean:
105+
@echo Clean
106+
@rm -fr $(BUILD_DIR)
107+
@${GO} clean
108+
109+
###############################################################################
110+
# DEPENDENCIES
111+
112+
.PHONY: go-dep
113+
go-dep:
114+
@test -f "${GO}" && test -x "${GO}" || (echo "Missing go binary" && exit 1)
115+
116+
.PHONY: docker-dep
117+
docker-dep:
118+
@test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1)

agent.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package llm
2+
3+
import (
4+
"context"
5+
)
6+
7+
// An LLM Agent is a client for the LLM service
8+
type Agent interface {
9+
// Return the name of the agent
10+
Name() string
11+
12+
// Return the models
13+
Models(context.Context) ([]Model, error)
14+
}

attachment.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package llm
2+
3+
import (
4+
"io"
5+
"os"
6+
)
7+
8+
///////////////////////////////////////////////////////////////////////////////
9+
// TYPES
10+
11+
// Attachment for messages
12+
type Attachment struct {
13+
filename string
14+
data []byte
15+
}
16+
17+
////////////////////////////////////////////////////////////////////////////////
18+
// LIFECYCLE
19+
20+
// ReadAttachment returns an attachment from a reader object.
21+
// It is the responsibility of the caller to close the reader.
22+
func ReadAttachment(r io.Reader) (*Attachment, error) {
23+
var filename string
24+
data, err := io.ReadAll(r)
25+
if err != nil {
26+
return nil, err
27+
}
28+
if f, ok := r.(*os.File); ok {
29+
filename = f.Name()
30+
}
31+
return &Attachment{filename: filename, data: data}, nil
32+
}
33+
34+
////////////////////////////////////////////////////////////////////////////////
35+
// PUBLIC METHODS
36+
37+
func (a *Attachment) Filename() string {
38+
return a.filename
39+
}
40+
41+
func (a *Attachment) Data() []byte {
42+
return a.data
43+
}

cmd/agent/chat.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
// Packages
11+
llm "github.com/mutablelogic/go-llm"
12+
agent "github.com/mutablelogic/go-llm/pkg/agent"
13+
)
14+
15+
////////////////////////////////////////////////////////////////////////////////
16+
// TYPES
17+
18+
type ChatCmd struct {
19+
Model string `arg:"" help:"Model name"`
20+
NoStream bool `flag:"nostream" help:"Disable streaming"`
21+
System string `flag:"system" help:"Set the system prompt"`
22+
}
23+
24+
////////////////////////////////////////////////////////////////////////////////
25+
// PUBLIC METHODS
26+
27+
func (cmd *ChatCmd) Run(globals *Globals) error {
28+
return runagent(globals, func(ctx context.Context, client llm.Agent) error {
29+
// Get the model
30+
a, ok := client.(*agent.Agent)
31+
if !ok {
32+
return fmt.Errorf("No agents found")
33+
}
34+
model, err := a.GetModel(ctx, cmd.Model)
35+
if err != nil {
36+
return err
37+
}
38+
39+
// Set the options
40+
opts := []llm.Opt{}
41+
if !cmd.NoStream {
42+
opts = append(opts, llm.WithStream(func(cc llm.ContextContent) {
43+
if text := cc.Text(); text != "" {
44+
fmt.Println(text)
45+
}
46+
}))
47+
}
48+
if cmd.System != "" {
49+
opts = append(opts, llm.WithSystemPrompt(cmd.System))
50+
}
51+
if globals.toolkit != nil {
52+
opts = append(opts, llm.WithToolKit(globals.toolkit))
53+
}
54+
55+
// Create a session
56+
session := model.Context(opts...)
57+
58+
// Continue looping until end of input
59+
for {
60+
input, err := globals.term.ReadLine(model.Name() + "> ")
61+
if errors.Is(err, io.EOF) {
62+
return nil
63+
} else if err != nil {
64+
return err
65+
}
66+
67+
// Ignore empty input
68+
input = strings.TrimSpace(input)
69+
if input == "" {
70+
continue
71+
}
72+
73+
// Feed input into the model
74+
if err := session.FromUser(ctx, input); err != nil {
75+
return err
76+
}
77+
78+
// Repeat call tools until no more calls are made
79+
for {
80+
calls := session.ToolCalls()
81+
if len(calls) == 0 {
82+
break
83+
}
84+
if session.Text() != "" {
85+
globals.term.Println(session.Text())
86+
} else {
87+
var names []string
88+
for _, call := range calls {
89+
names = append(names, call.Name())
90+
}
91+
globals.term.Println("Calling ", strings.Join(names, ", "))
92+
}
93+
if results, err := globals.toolkit.Run(ctx, calls...); err != nil {
94+
return err
95+
} else if err := session.FromTool(ctx, results...); err != nil {
96+
return err
97+
}
98+
}
99+
100+
// Print the response
101+
globals.term.Println("\n" + session.Text() + "\n")
102+
}
103+
})
104+
}

0 commit comments

Comments
 (0)