Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Create Docker Image
on:
release:
types:
- created

workflow_dispatch:

jobs:
build:
name: Build
strategy:
matrix:
arch: [ amd64, arm64 ]
runs-on:
- ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || matrix.arch }}
env:
OS: linux
ARCH: ${{ matrix.arch }}
DOCKER_REPO: ghcr.io/${{ github.repository }}
DOCKER_SOURCE: https://github.com/${{ github.repository }}
outputs:
tag: ${{ steps.build.outputs.tag }}
permissions:
contents: read
packages: write
steps:
- name: Install build tools
run: |
sudo apt -y update
sudo apt -y install build-essential git
git config --global advice.detachedHead false
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
id: build
run: |
make docker && make docker-push && make docker-version >> "$GITHUB_OUTPUT"
manifest:
name: Manifest
needs: build
strategy:
matrix:
tag:
- ${{ needs.build.outputs.tag }}
- "latest"
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create
run: |
docker manifest create ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
--amend ghcr.io/${{ github.repository }}-linux-amd64:${{ needs.build.outputs.tag }} \
--amend ghcr.io/${{ github.repository }}-linux-arm64:${{ needs.build.outputs.tag }}
- name: Annotate
run: |
docker manifest annotate --arch amd64 --os linux \
ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
ghcr.io/${{ github.repository }}-linux-amd64:${{ needs.build.outputs.tag }}
docker manifest annotate --arch arm64 --os linux \
ghcr.io/${{ github.repository }}:${{ matrix.tag }} \
ghcr.io/${{ github.repository }}-linux-arm64:${{ needs.build.outputs.tag }}
- name: Push
run: |
docker manifest push ghcr.io/${{ github.repository }}:${{ matrix.tag }}
19 changes: 3 additions & 16 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env
vendor/
build/
.DS_Store
118 changes: 118 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Executables
GO ?= $(shell which go 2>/dev/null)
DOCKER ?= $(shell which docker 2>/dev/null)

# Locations
BUILD_DIR ?= build
CMD_DIR := $(wildcard cmd/*)

# VERBOSE=1
ifneq ($(VERBOSE),)
VERBOSE_FLAG = -v
else
VERBOSE_FLAG =
endif

# Set OS and Architecture
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/')
OS ?= $(shell uname | tr A-Z a-z)
VERSION ?= $(shell git describe --tags --always | sed 's/^v//')

# Set build flags
BUILD_MODULE = $(shell cat go.mod | head -1 | cut -d ' ' -f 2)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitSource=${BUILD_MODULE}
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitTag=$(shell git describe --tags --always)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitBranch=$(shell git name-rev HEAD --name-only --always)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitHash=$(shell git rev-parse HEAD)
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GoBuildTime=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
BUILD_FLAGS = -ldflags "-s -w ${BUILD_LD_FLAGS}"

# Docker
DOCKER_REPO ?= ghcr.io/mutablelogic/go-llm
DOCKER_SOURCE ?= ${BUILD_MODULE}
DOCKER_TAG = ${DOCKER_REPO}-${OS}-${ARCH}:${VERSION}

###############################################################################
# ALL

.PHONY: all
all: clean build

###############################################################################
# BUILD

# Build the commands in the cmd directory
.PHONY: build
build: tidy $(CMD_DIR)

$(CMD_DIR): go-dep mkdir
@echo Build command $(notdir $@) GOOS=${OS} GOARCH=${ARCH}
@GOOS=${OS} GOARCH=${ARCH} ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@

# Build the docker image
.PHONY: docker
docker: docker-dep
@echo build docker image ${DOCKER_TAG} OS=${OS} ARCH=${ARCH} SOURCE=${DOCKER_SOURCE} VERSION=${VERSION}
@${DOCKER} build \
--tag ${DOCKER_TAG} \
--build-arg ARCH=${ARCH} \
--build-arg OS=${OS} \
--build-arg SOURCE=${DOCKER_SOURCE} \
--build-arg VERSION=${VERSION} \
-f etc/docker/Dockerfile .

# Push docker container
.PHONY: docker-push
docker-push: docker-dep
@echo push docker image: ${DOCKER_TAG}
@${DOCKER} push ${DOCKER_TAG}

# Print out the version
.PHONY: docker-version
docker-version: docker-dep
@echo "tag=${VERSION}"

###############################################################################
# TEST

.PHONY: test
test: unit-test coverage-test

.PHONY: unit-test
unit-test: go-dep
@echo Unit Tests
@${GO} test ${VERBOSE_FLAG} ./pkg/...

.PHONY: coverage-test
coverage-test: go-dep mkdir
@echo Test Coverage
@${GO} test -coverprofile ${BUILD_DIR}/coverprofile.out ./pkg/...

###############################################################################
# CLEAN

.PHONY: tidy
tidy:
@echo Running go mod tidy
@${GO} mod tidy

.PHONY: mkdir
mkdir:
@install -d ${BUILD_DIR}

.PHONY: clean
clean:
@echo Clean
@rm -fr $(BUILD_DIR)
@${GO} clean

###############################################################################
# DEPENDENCIES

.PHONY: go-dep
go-dep:
@test -f "${GO}" && test -x "${GO}" || (echo "Missing go binary" && exit 1)

.PHONY: docker-dep
docker-dep:
@test -f "${DOCKER}" && test -x "${DOCKER}" || (echo "Missing docker binary" && exit 1)
14 changes: 14 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package llm

import (
"context"
)

// An LLM Agent is a client for the LLM service
type Agent interface {
// Return the name of the agent
Name() string

// Return the models
Models(context.Context) ([]Model, error)
}
43 changes: 43 additions & 0 deletions attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package llm

import (
"io"
"os"
)

///////////////////////////////////////////////////////////////////////////////
// TYPES

// Attachment for messages
type Attachment struct {
filename string
data []byte
}

////////////////////////////////////////////////////////////////////////////////
// LIFECYCLE

// ReadAttachment returns an attachment from a reader object.
// It is the responsibility of the caller to close the reader.
func ReadAttachment(r io.Reader) (*Attachment, error) {
var filename string
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
if f, ok := r.(*os.File); ok {
filename = f.Name()
}
return &Attachment{filename: filename, data: data}, nil
}

////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

func (a *Attachment) Filename() string {
return a.filename
}

func (a *Attachment) Data() []byte {
return a.data
}
104 changes: 104 additions & 0 deletions cmd/agent/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"strings"

// Packages
llm "github.com/mutablelogic/go-llm"
agent "github.com/mutablelogic/go-llm/pkg/agent"
)

////////////////////////////////////////////////////////////////////////////////
// TYPES

type ChatCmd struct {
Model string `arg:"" help:"Model name"`
NoStream bool `flag:"nostream" help:"Disable streaming"`
System string `flag:"system" help:"Set the system prompt"`
}

////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS

func (cmd *ChatCmd) Run(globals *Globals) error {
return runagent(globals, func(ctx context.Context, client llm.Agent) error {
// Get the model
a, ok := client.(*agent.Agent)
if !ok {
return fmt.Errorf("No agents found")
}
model, err := a.GetModel(ctx, cmd.Model)
if err != nil {
return err
}

// Set the options
opts := []llm.Opt{}
if !cmd.NoStream {
opts = append(opts, llm.WithStream(func(cc llm.ContextContent) {
if text := cc.Text(); text != "" {
fmt.Println(text)
}
}))
}
if cmd.System != "" {
opts = append(opts, llm.WithSystemPrompt(cmd.System))
}
if globals.toolkit != nil {
opts = append(opts, llm.WithToolKit(globals.toolkit))
}

// Create a session
session := model.Context(opts...)

// Continue looping until end of input
for {
input, err := globals.term.ReadLine(model.Name() + "> ")
if errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
}

// Ignore empty input
input = strings.TrimSpace(input)
if input == "" {
continue
}

// Feed input into the model
if err := session.FromUser(ctx, input); err != nil {
return err
}

// Repeat call tools until no more calls are made
for {
calls := session.ToolCalls()
if len(calls) == 0 {
break
}
if session.Text() != "" {
globals.term.Println(session.Text())
} else {
var names []string
for _, call := range calls {
names = append(names, call.Name())
}
globals.term.Println("Calling ", strings.Join(names, ", "))
}
if results, err := globals.toolkit.Run(ctx, calls...); err != nil {
return err
} else if err := session.FromTool(ctx, results...); err != nil {
return err
}
}

// Print the response
globals.term.Println("\n" + session.Text() + "\n")
}
})
}
Loading