Skip to content

Commit 11071c1

Browse files
authored
Merge pull request #15 from mutablelogic/djt/0214/manager
Complete llm refactor
2 parents 347c2e0 + 2f9f710 commit 11071c1

143 files changed

Lines changed: 14537 additions & 5365 deletions

File tree

Some content is hidden

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

.github/workflows/cli.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Build CLI Commands
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
build:
14+
name: Build llm
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
matrix:
18+
include:
19+
- os: macos-latest
20+
arch: arm64
21+
goos: darwin
22+
goarch: arm64
23+
- os: ubuntu-24.04-arm
24+
arch: arm64
25+
goos: linux
26+
goarch: arm64
27+
- os: ubuntu-24.04
28+
arch: amd64
29+
goos: linux
30+
goarch: amd64
31+
- os: windows-latest
32+
arch: amd64
33+
goos: windows
34+
goarch: amd64
35+
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Set up Go
43+
uses: actions/setup-go@v5
44+
with:
45+
go-version: "1.25"
46+
47+
- name: Build llm-client (Unix)
48+
if: matrix.goos != 'windows'
49+
env:
50+
GOOS: ${{ matrix.goos }}
51+
GOARCH: ${{ matrix.goarch }}
52+
run: make llm-client
53+
54+
- name: Build llm-client (Windows)
55+
if: matrix.goos == 'windows'
56+
env:
57+
GOOS: ${{ matrix.goos }}
58+
GOARCH: ${{ matrix.goarch }}
59+
shell: bash
60+
run: |
61+
mkdir -p build
62+
go build -ldflags "-s -w" -tags client -o build/llm.exe ./cmd/llm
63+
64+
- name: Rename binary for release
65+
shell: bash
66+
run: |
67+
if [ "${{ matrix.goos }}" = "windows" ]; then
68+
mv build/llm.exe build/llm-${{ matrix.goos }}-${{ matrix.goarch }}.exe
69+
else
70+
mv build/llm build/llm-${{ matrix.goos }}-${{ matrix.goarch }}
71+
fi
72+
73+
- name: Upload artifact
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: llm-${{ matrix.goos }}-${{ matrix.goarch }}
77+
path: build/llm-${{ matrix.goos }}-${{ matrix.goarch }}*
78+
79+
release:
80+
name: Create Release
81+
needs: build
82+
runs-on: ubuntu-latest
83+
if: startsWith(github.ref, 'refs/tags/')
84+
85+
steps:
86+
- name: Download all artifacts
87+
uses: actions/download-artifact@v4
88+
with:
89+
path: ./artifacts
90+
91+
- name: Create Release
92+
uses: softprops/action-gh-release@v1
93+
with:
94+
files: ./artifacts/**/*
95+
draft: false
96+
prerelease: false
97+
env:
98+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/docker.yaml

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,24 @@ jobs:
1111
name: Build
1212
strategy:
1313
matrix:
14-
arch: [ amd64, arm64 ]
15-
runs-on:
16-
- ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || matrix.arch }}
14+
arch: [amd64, arm64]
15+
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-24.04' || matrix.arch == 'arm64' && 'ubuntu-24.04-arm' }}
1716
env:
1817
OS: linux
1918
ARCH: ${{ matrix.arch }}
20-
DOCKER_REPO: ghcr.io/${{ github.repository }}
21-
DOCKER_SOURCE: https://github.com/${{ github.repository }}
19+
DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/llm
20+
DOCKER_SOURCE: github.com/${{ github.repository }}
2221
outputs:
2322
tag: ${{ steps.build.outputs.tag }}
2423
permissions:
2524
contents: read
26-
packages: write
25+
packages: write
2726
steps:
2827
- name: Install build tools
2928
run: |
3029
sudo apt -y update
3130
sudo apt -y install build-essential git
32-
git config --global advice.detachedHead false
31+
git config --global advice.detachedHead false
3332
- name: Checkout
3433
uses: actions/checkout@v4
3534
with:
@@ -41,7 +40,7 @@ jobs:
4140
username: ${{ github.repository_owner }}
4241
password: ${{ secrets.GITHUB_TOKEN }}
4342
- name: Build and Push
44-
id: build
43+
id: build
4544
run: |
4645
make docker && make docker-push && make docker-version >> "$GITHUB_OUTPUT"
4746
manifest:
@@ -54,7 +53,7 @@ jobs:
5453
- "latest"
5554
runs-on: ubuntu-latest
5655
permissions:
57-
packages: write
56+
packages: write
5857
steps:
5958
- name: Login
6059
uses: docker/login-action@v3
@@ -64,17 +63,17 @@ jobs:
6463
password: ${{ secrets.GITHUB_TOKEN }}
6564
- name: Create
6665
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 }}
66+
docker manifest create ghcr.io/${{ github.repository_owner }}/llm:${{ matrix.tag }} \
67+
--amend ghcr.io/${{ github.repository_owner }}/llm-linux-amd64:${{ needs.build.outputs.tag }} \
68+
--amend ghcr.io/${{ github.repository_owner }}/llm-linux-arm64:${{ needs.build.outputs.tag }}
7069
- name: Annotate
7170
run: |
7271
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 }}
72+
ghcr.io/${{ github.repository_owner }}/llm:${{ matrix.tag }} \
73+
ghcr.io/${{ github.repository_owner }}/llm-linux-amd64:${{ needs.build.outputs.tag }}
7574
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 }}
75+
ghcr.io/${{ github.repository_owner }}/llm:${{ matrix.tag }} \
76+
ghcr.io/${{ github.repository_owner }}/llm-linux-arm64:${{ needs.build.outputs.tag }}
7877
- name: Push
7978
run: |
80-
docker manifest push ghcr.io/${{ github.repository }}:${{ matrix.tag }}
79+
docker manifest push ghcr.io/${{ github.repository_owner }}/llm:${{ matrix.tag }}

.vscode/settings.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

1

Whitespace-only changes.

Makefile

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,20 @@ VERSION ?= $(shell git describe --tags --always | sed 's/^v//')
2020

2121
# Set build flags
2222
BUILD_MODULE = $(shell cat go.mod | head -1 | cut -d ' ' -f 2)
23-
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitSource=${BUILD_MODULE}
2423
BUILD_LD_FLAGS += -X $(BUILD_MODULE)/pkg/version.GitTag=$(shell git describe --tags --always)
2524
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')
2825
BUILD_FLAGS = -ldflags "-s -w ${BUILD_LD_FLAGS}"
2926

3027
# Docker
31-
DOCKER_REPO ?= ghcr.io/mutablelogic/go-llm
28+
DOCKER_REPO ?= ghcr.io/mutablelogic/llm
3229
DOCKER_SOURCE ?= ${BUILD_MODULE}
3330
DOCKER_TAG = ${DOCKER_REPO}-${OS}-${ARCH}:${VERSION}
3431

3532
###############################################################################
3633
# ALL
3734

3835
.PHONY: all
39-
all: clean build
36+
all: build
4037

4138
###############################################################################
4239
# BUILD
@@ -49,12 +46,19 @@ $(CMD_DIR): go-dep mkdir
4946
@echo Build command $(notdir $@) GOOS=${OS} GOARCH=${ARCH}
5047
@GOOS=${OS} GOARCH=${ARCH} ${GO} build ${BUILD_FLAGS} -o ${BUILD_DIR}/$(notdir $@) ./$@
5148

49+
# Build the client-only CLI (no server or telegram)
50+
.PHONY: llm-client
51+
llm-client: go-dep mkdir
52+
@echo Build llm-client GOOS=${OS} GOARCH=${ARCH}
53+
@GOOS=${OS} GOARCH=${ARCH} ${GO} build ${BUILD_FLAGS} -tags client -o ${BUILD_DIR}/llm ./cmd/llm
54+
5255
# Build the docker image
5356
.PHONY: docker
5457
docker: docker-dep
5558
@echo build docker image ${DOCKER_TAG} OS=${OS} ARCH=${ARCH} SOURCE=${DOCKER_SOURCE} VERSION=${VERSION}
5659
@${DOCKER} build \
5760
--tag ${DOCKER_TAG} \
61+
--provenance=false \
5862
--build-arg ARCH=${ARCH} \
5963
--build-arg OS=${OS} \
6064
--build-arg SOURCE=${DOCKER_SOURCE} \

client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ type Downloader interface {
4444
// Generator is an interface for generating response messages and conducting conversations
4545
type Generator interface {
4646
// WithoutSession sends a single message and returns the response (stateless)
47-
WithoutSession(context.Context, schema.Model, *schema.Message, ...opt.Opt) (*schema.Message, error)
47+
WithoutSession(context.Context, schema.Model, *schema.Message, ...opt.Opt) (*schema.Message, *schema.Usage, error)
4848

4949
// WithSession sends a message within a session and returns the response (stateful)
50-
WithSession(context.Context, schema.Model, *schema.Session, *schema.Message, ...opt.Opt) (*schema.Message, error)
50+
WithSession(context.Context, schema.Model, *schema.Conversation, *schema.Message, ...opt.Opt) (*schema.Message, *schema.Usage, error)
5151
}

cmd/llm/ask.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
// Packages
10+
otel "github.com/mutablelogic/go-client/pkg/otel"
11+
httpclient "github.com/mutablelogic/go-llm/pkg/httpclient"
12+
schema "github.com/mutablelogic/go-llm/pkg/schema"
13+
)
14+
15+
///////////////////////////////////////////////////////////////////////////////
16+
// TYPES
17+
18+
type GenerateCommands struct {
19+
Ask AskCommand `cmd:"" name:"ask" help:"Send a stateless text request to a model." group:"GENERATE"`
20+
Chat ChatCommand `cmd:"" name:"chat" help:"Send a message within a session (creates one if needed)." group:"GENERATE"`
21+
Embedding EmbeddingCommand `cmd:"" name:"embedding" help:"Generate embedding vectors from text." group:"GENERATE"`
22+
}
23+
24+
type AskCommand struct {
25+
schema.AskRequest
26+
File []string `help:"Path or glob pattern for files to attach (may be repeated)" optional:""`
27+
URL string `help:"URL to attach as a reference" optional:""`
28+
}
29+
30+
///////////////////////////////////////////////////////////////////////////////
31+
// COMMANDS
32+
33+
func (cmd *AskCommand) Run(ctx *Globals) (err error) {
34+
// Load defaults for model and provider when not explicitly set
35+
if cmd.AskRequest.Model == "" {
36+
cmd.AskRequest.Model = ctx.defaults.GetString("model")
37+
}
38+
if cmd.AskRequest.Provider == "" {
39+
cmd.AskRequest.Provider = ctx.defaults.GetString("provider")
40+
}
41+
if cmd.AskRequest.Model == "" {
42+
return fmt.Errorf("model is required (set with --model or store a default)")
43+
}
44+
45+
// Store model and provider as defaults
46+
if err := ctx.defaults.Set("model", cmd.AskRequest.Model); err != nil {
47+
return err
48+
}
49+
if cmd.AskRequest.Provider != "" {
50+
if err := ctx.defaults.Set("provider", cmd.AskRequest.Provider); err != nil {
51+
return err
52+
}
53+
}
54+
55+
// When format is set, hint the model to reply in JSON
56+
if len(cmd.AskRequest.Format) > 0 {
57+
cmd.AskRequest.SystemPrompt = "Respond with valid JSON only. Do not include any other text or formatting.\n" + cmd.AskRequest.SystemPrompt
58+
}
59+
60+
client, err := ctx.Client()
61+
if err != nil {
62+
return err
63+
}
64+
65+
// OTEL
66+
parent, endSpan := otel.StartSpan(ctx.tracer, ctx.ctx, "AskCommand")
67+
defer func() { endSpan(err) }()
68+
69+
// Build options
70+
var opts []httpclient.AskOpt
71+
for _, pattern := range cmd.File {
72+
matches, err := filepath.Glob(pattern)
73+
if err != nil {
74+
return fmt.Errorf("invalid glob pattern %q: %w", pattern, err)
75+
}
76+
if len(matches) == 0 {
77+
return fmt.Errorf("no files match %q", pattern)
78+
}
79+
for _, path := range matches {
80+
f, err := os.Open(path)
81+
if err != nil {
82+
return err
83+
}
84+
defer f.Close()
85+
opts = append(opts, httpclient.WithFile(f.Name(), f))
86+
}
87+
}
88+
if cmd.URL != "" {
89+
opts = append(opts, httpclient.WithURL(cmd.URL))
90+
}
91+
92+
// Send request
93+
response, err := client.Ask(parent, cmd.AskRequest, opts...)
94+
if err != nil {
95+
return err
96+
}
97+
98+
// Print
99+
if ctx.Debug {
100+
fmt.Println(response)
101+
} else {
102+
// Collect text and thinking from content blocks
103+
var text, thinking string
104+
for _, block := range response.Content {
105+
if block.Text != nil {
106+
text += *block.Text
107+
}
108+
if block.Thinking != nil {
109+
thinking += *block.Thinking
110+
}
111+
}
112+
113+
// If format was set, try to pretty-print as indented JSON
114+
if len(cmd.AskRequest.Format) > 0 {
115+
var raw json.RawMessage
116+
if err := json.Unmarshal([]byte(text), &raw); err == nil {
117+
if indented, err := json.MarshalIndent(raw, "", " "); err == nil {
118+
fmt.Println(string(indented))
119+
return nil
120+
}
121+
}
122+
}
123+
124+
// Print thinking block if present
125+
if thinking != "" {
126+
label := "thinking"
127+
if isTerminal(os.Stdout) {
128+
label = "\033[2m" + label + "\033[0m" // dim
129+
thinking = "\033[2m" + thinking + "\033[0m"
130+
}
131+
fmt.Println(label + ": " + thinking)
132+
fmt.Println()
133+
}
134+
135+
// Prepend role
136+
role := response.Role
137+
if role != "" {
138+
if isTerminal(os.Stdout) {
139+
role = "\033[1m" + role + "\033[0m"
140+
}
141+
text = role + ": " + text
142+
}
143+
fmt.Println(text)
144+
}
145+
return nil
146+
}

0 commit comments

Comments
 (0)