Skip to content

Commit

Permalink
Merge pull request #1 from mutablelogic/dev
Browse files Browse the repository at this point in the history
Initial merge into main
  • Loading branch information
djthorpe authored Feb 1, 2025
2 parents e56ab1a + cd5c7d0 commit 410db6e
Show file tree
Hide file tree
Showing 61 changed files with 5,587 additions and 16 deletions.
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

0 comments on commit 410db6e

Please sign in to comment.